feat: complete Epic 0 — foundation migration & infrastructure setup
Stories 0.2-0.5: rename folders→declarations (backend+frontend), configure Redis for cache/queue/sessions, add foundation database migrations (permissions, archived_at), replace DeclarationStatus enum with architecture lifecycle values, create DeclarationObserver for status transition validation and auto-archive, fix controller status transitions to respect observer rules. 93 tests pass (240 assertions). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,16 +16,16 @@ class ConfirmController extends Controller
|
||||
*/
|
||||
public function show(Request $request, string $token): Response
|
||||
{
|
||||
$invitation = $request->attributes->get('folder_invitation');
|
||||
$folder = $invitation->folder;
|
||||
$invitation = $request->attributes->get('declaration_invitation');
|
||||
$declaration = $invitation->declaration;
|
||||
|
||||
$folder->load(['client']);
|
||||
$declaration->load(['client']);
|
||||
|
||||
return Inertia::render('client/Confirm', [
|
||||
'folder' => [
|
||||
'id' => $folder->id,
|
||||
'title' => $folder->title,
|
||||
'client_name' => $folder->client->company_name,
|
||||
'declaration' => [
|
||||
'id' => $declaration->id,
|
||||
'title' => $declaration->title,
|
||||
'client_name' => $declaration->client->company_name,
|
||||
],
|
||||
'token' => $token,
|
||||
'submitUrl' => route('client.confirm.store', ['token' => $token]),
|
||||
@@ -37,19 +37,19 @@ class ConfirmController extends Controller
|
||||
*/
|
||||
public function store(Request $request, string $token): RedirectResponse
|
||||
{
|
||||
$invitation = $request->attributes->get('folder_invitation');
|
||||
$folder = $invitation->folder;
|
||||
$invitation = $request->attributes->get('declaration_invitation');
|
||||
$declaration = $invitation->declaration;
|
||||
|
||||
$request->validate([
|
||||
'signature' => ['required', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$folder->update([
|
||||
$declaration->update([
|
||||
'validated_at' => now(),
|
||||
'confirmed_by_type' => ActorType::Client,
|
||||
'confirmed_by_id' => $folder->client_id,
|
||||
'confirmed_by_id' => $declaration->client_id,
|
||||
'confirmation_signature' => $request->input('signature'),
|
||||
'status' => \App\Enums\FolderStatus::Validated,
|
||||
'status' => \App\Enums\DeclarationStatus::EnCours,
|
||||
]);
|
||||
|
||||
return back()->with('flash', ['type' => 'success', 'message' => 'Validation enregistrée. Merci.']);
|
||||
|
||||
@@ -15,16 +15,16 @@ class RefuseController extends Controller
|
||||
*/
|
||||
public function show(Request $request, string $token): Response
|
||||
{
|
||||
$invitation = $request->attributes->get('folder_invitation');
|
||||
$folder = $invitation->folder;
|
||||
$invitation = $request->attributes->get('declaration_invitation');
|
||||
$declaration = $invitation->declaration;
|
||||
|
||||
$folder->load(['client']);
|
||||
$declaration->load(['client']);
|
||||
|
||||
return Inertia::render('client/Refuse', [
|
||||
'folder' => [
|
||||
'id' => $folder->id,
|
||||
'title' => $folder->title,
|
||||
'client_name' => $folder->client->company_name,
|
||||
'declaration' => [
|
||||
'id' => $declaration->id,
|
||||
'title' => $declaration->title,
|
||||
'client_name' => $declaration->client->company_name,
|
||||
],
|
||||
'token' => $token,
|
||||
'submitUrl' => route('client.refuse.store', ['token' => $token]),
|
||||
@@ -36,14 +36,14 @@ class RefuseController extends Controller
|
||||
*/
|
||||
public function store(Request $request, string $token): RedirectResponse
|
||||
{
|
||||
$invitation = $request->attributes->get('folder_invitation');
|
||||
$folder = $invitation->folder;
|
||||
$invitation = $request->attributes->get('declaration_invitation');
|
||||
$declaration = $invitation->declaration;
|
||||
|
||||
$request->validate([
|
||||
'reason' => ['nullable', 'string', 'max:65535'],
|
||||
]);
|
||||
|
||||
$folder->update([
|
||||
$declaration->update([
|
||||
'refused_at' => now(),
|
||||
'refusal_reason' => $request->input('reason'),
|
||||
]);
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
namespace App\Http\Controllers\Client;
|
||||
|
||||
use App\Enums\ActorType;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Enums\MessageType;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Mail\FolderTextMessageMail;
|
||||
use App\Mail\DeclarationTextMessageMail;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
@@ -20,11 +20,11 @@ class UploadController extends Controller
|
||||
*/
|
||||
public function show(Request $request, string $token): Response
|
||||
{
|
||||
$invitation = $request->attributes->get('folder_invitation');
|
||||
$folder = $invitation->folder;
|
||||
$invitation = $request->attributes->get('declaration_invitation');
|
||||
$declaration = $invitation->declaration;
|
||||
|
||||
$folder->load(['client']);
|
||||
$documents = $folder->getMedia('documents')->map(fn ($m) => [
|
||||
$declaration->load(['client']);
|
||||
$documents = $declaration->getMedia('documents')->map(fn ($m) => [
|
||||
'id' => $m->id,
|
||||
'name' => $m->name,
|
||||
'file_name' => $m->file_name,
|
||||
@@ -33,10 +33,10 @@ class UploadController extends Controller
|
||||
])->values()->all();
|
||||
|
||||
return Inertia::render('client/Upload', [
|
||||
'folder' => [
|
||||
'id' => $folder->id,
|
||||
'title' => $folder->title,
|
||||
'client_name' => $folder->client->company_name,
|
||||
'declaration' => [
|
||||
'id' => $declaration->id,
|
||||
'title' => $declaration->title,
|
||||
'client_name' => $declaration->client->company_name,
|
||||
],
|
||||
'token' => $token,
|
||||
'documents' => $documents,
|
||||
@@ -50,38 +50,42 @@ class UploadController extends Controller
|
||||
*/
|
||||
public function store(Request $request, string $token): RedirectResponse
|
||||
{
|
||||
$invitation = $request->attributes->get('folder_invitation');
|
||||
$folder = $invitation->folder;
|
||||
$invitation = $request->attributes->get('declaration_invitation');
|
||||
$declaration = $invitation->declaration;
|
||||
|
||||
$request->validate([
|
||||
'files' => ['required', 'array', 'min:1'],
|
||||
'files.*' => ['file', 'max:10240'],
|
||||
]);
|
||||
|
||||
$message = $folder->messages()->create([
|
||||
$message = $declaration->messages()->create([
|
||||
'type' => MessageType::Text,
|
||||
'body' => 'Documents déposés par le client.',
|
||||
'sent_by_type' => ActorType::Client,
|
||||
'sent_by_id' => $folder->client_id,
|
||||
'sent_by_id' => $declaration->client_id,
|
||||
'metadata' => ['invitation_id' => $invitation->id],
|
||||
]);
|
||||
|
||||
foreach ($request->file('files') as $file) {
|
||||
$folder->addMedia($file)
|
||||
$declaration->addMedia($file)
|
||||
->withCustomProperties([
|
||||
'message_id' => $message->id,
|
||||
'uploaded_by_type' => ActorType::Client,
|
||||
'uploaded_by_id' => $folder->client_id,
|
||||
'uploaded_by_id' => $declaration->client_id,
|
||||
])
|
||||
->toMediaCollection('documents');
|
||||
}
|
||||
|
||||
$folder->update(['status' => FolderStatus::DocumentsReceived]);
|
||||
// Only transition to en_cours if the current status allows it
|
||||
$allowed = DeclarationStatus::allowedTransitions()[$declaration->status->value] ?? [];
|
||||
if (in_array(DeclarationStatus::EnCours, $allowed)) {
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
}
|
||||
|
||||
$recipient = $folder->assignee ?? $folder->creator;
|
||||
$recipient = $declaration->assignee ?? $declaration->creator;
|
||||
if ($recipient?->email) {
|
||||
Mail::to($recipient->email)->send(
|
||||
new FolderTextMessageMail($folder, 'Le client a déposé des documents.', null)
|
||||
new DeclarationTextMessageMail($declaration, 'Le client a déposé des documents.', null)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,7 @@ class ClientController extends Controller
|
||||
|
||||
$client->load(['internalResponsible', 'contacts']);
|
||||
|
||||
$folders = $client->folders()
|
||||
$declarations = $client->declarations()
|
||||
->with(['assignee'])
|
||||
->latest()
|
||||
->limit(50)
|
||||
@@ -153,18 +153,18 @@ class ClientController extends Controller
|
||||
'status' => $f->status->value,
|
||||
'due_date' => $f->due_date?->format('Y-m-d'),
|
||||
'created_at' => $f->created_at->format('Y-m-d'),
|
||||
'showUrl' => route('folders.show', $f),
|
||||
'showUrl' => route('declarations.show', $f),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$allFolders = $client->folders()->get();
|
||||
$allDeclarations = $client->declarations()->get();
|
||||
$stats = [
|
||||
'total' => $allFolders->count(),
|
||||
'by_status' => $allFolders->groupBy(fn ($f) => $f->status->value)
|
||||
'total' => $allDeclarations->count(),
|
||||
'by_status' => $allDeclarations->groupBy(fn ($f) => $f->status->value)
|
||||
->map->count()
|
||||
->all(),
|
||||
'by_type' => $allFolders->groupBy(fn ($f) => $f->type->value)
|
||||
'by_type' => $allDeclarations->groupBy(fn ($f) => $f->type->value)
|
||||
->map->count()
|
||||
->all(),
|
||||
];
|
||||
@@ -185,11 +185,11 @@ class ClientController extends Controller
|
||||
'status' => $client->status?->value,
|
||||
'internal_notes' => $client->internal_notes,
|
||||
],
|
||||
'folders' => $folders,
|
||||
'declarations' => $declarations,
|
||||
'stats' => $stats,
|
||||
'indexUrl' => route('clients.index'),
|
||||
'editUrl' => route('clients.edit', $client),
|
||||
'createFolderUrl' => route('folders.create', ['client_id' => $client->id]),
|
||||
'createDeclarationUrl' => route('declarations.create', ['client_id' => $client->id]),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Models\Folder;
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
@@ -12,7 +12,7 @@ use Inertia\Response;
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display the dashboard with assigned folders and notifications.
|
||||
* Display the dashboard with assigned declarations and notifications.
|
||||
*/
|
||||
public function __invoke(Request $request): Response
|
||||
{
|
||||
@@ -20,18 +20,18 @@ class DashboardController extends Controller
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
$workspace = $workspaceId ? Workspace::query()->find($workspaceId) : null;
|
||||
|
||||
$assignedFolders = [];
|
||||
$assignedDeclarations = [];
|
||||
$notifications = [];
|
||||
|
||||
if ($workspace && $user) {
|
||||
$assignedFolders = $workspace->folders()
|
||||
$assignedDeclarations = $workspace->declarations()
|
||||
->where('assigned_to', $user->id)
|
||||
->whereNotIn('status', [FolderStatus::Closed, FolderStatus::Cancelled])
|
||||
->whereNotIn('status', [DeclarationStatus::Ferme])
|
||||
->with('client:id,company_name')
|
||||
->orderByRaw('CASE WHEN due_date IS NULL THEN 1 ELSE 0 END, due_date ASC')
|
||||
->limit(50)
|
||||
->get()
|
||||
->map(fn (Folder $f) => [
|
||||
->map(fn (Declaration $f) => [
|
||||
'id' => $f->id,
|
||||
'title' => $f->title,
|
||||
'type' => $f->type->value,
|
||||
@@ -39,71 +39,71 @@ class DashboardController extends Controller
|
||||
'status' => $f->status->value,
|
||||
'due_date' => $f->due_date?->format('Y-m-d'),
|
||||
'priority' => $f->priority?->value,
|
||||
'showUrl' => route('folders.show', $f),
|
||||
'showUrl' => route('declarations.show', $f),
|
||||
])
|
||||
->all();
|
||||
|
||||
$overdue = $workspace->folders()
|
||||
$overdue = $workspace->declarations()
|
||||
->where('assigned_to', $user->id)
|
||||
->where('due_date', '<', now()->startOfDay())
|
||||
->whereNotIn('status', [FolderStatus::Closed, FolderStatus::Cancelled])
|
||||
->whereNotIn('status', [DeclarationStatus::Ferme])
|
||||
->with('client:id,company_name')
|
||||
->orderBy('due_date')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn (Folder $f) => [
|
||||
->map(fn (Declaration $f) => [
|
||||
'id' => $f->id,
|
||||
'title' => $f->title,
|
||||
'client_name' => $f->client->company_name,
|
||||
'due_date' => $f->due_date?->format('Y-m-d'),
|
||||
'showUrl' => route('folders.show', $f),
|
||||
'showUrl' => route('declarations.show', $f),
|
||||
])
|
||||
->all();
|
||||
|
||||
$dueSoon = $workspace->folders()
|
||||
$dueSoon = $workspace->declarations()
|
||||
->where('assigned_to', $user->id)
|
||||
->whereBetween('due_date', [now()->startOfDay(), now()->addDays(7)->endOfDay()])
|
||||
->whereNotIn('status', [FolderStatus::Closed, FolderStatus::Cancelled])
|
||||
->whereNotIn('status', [DeclarationStatus::Ferme])
|
||||
->with('client:id,company_name')
|
||||
->orderBy('due_date')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn (Folder $f) => [
|
||||
->map(fn (Declaration $f) => [
|
||||
'id' => $f->id,
|
||||
'title' => $f->title,
|
||||
'client_name' => $f->client->company_name,
|
||||
'due_date' => $f->due_date?->format('Y-m-d'),
|
||||
'showUrl' => route('folders.show', $f),
|
||||
'showUrl' => route('declarations.show', $f),
|
||||
])
|
||||
->all();
|
||||
|
||||
$documentsReceived = $workspace->folders()
|
||||
$documentsReceived = $workspace->declarations()
|
||||
->where('assigned_to', $user->id)
|
||||
->where('status', FolderStatus::DocumentsReceived)
|
||||
->where('status', DeclarationStatus::EnCours)
|
||||
->with('client:id,company_name')
|
||||
->orderBy('updated_at', 'desc')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn (Folder $f) => [
|
||||
->map(fn (Declaration $f) => [
|
||||
'id' => $f->id,
|
||||
'title' => $f->title,
|
||||
'client_name' => $f->client->company_name,
|
||||
'showUrl' => route('folders.show', $f),
|
||||
'showUrl' => route('declarations.show', $f),
|
||||
])
|
||||
->all();
|
||||
|
||||
$awaitingValidation = $workspace->folders()
|
||||
$awaitingValidation = $workspace->declarations()
|
||||
->where('assigned_to', $user->id)
|
||||
->where('status', FolderStatus::WaitingClientValidation)
|
||||
->where('status', DeclarationStatus::EnAttenteClient)
|
||||
->with('client:id,company_name')
|
||||
->orderBy('confirmation_requested_at', 'desc')
|
||||
->limit(10)
|
||||
->get()
|
||||
->map(fn (Folder $f) => [
|
||||
->map(fn (Declaration $f) => [
|
||||
'id' => $f->id,
|
||||
'title' => $f->title,
|
||||
'client_name' => $f->client->company_name,
|
||||
'showUrl' => route('folders.show', $f),
|
||||
'showUrl' => route('declarations.show', $f),
|
||||
])
|
||||
->all();
|
||||
|
||||
@@ -116,10 +116,10 @@ class DashboardController extends Controller
|
||||
}
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'assignedFolders' => $assignedFolders,
|
||||
'assignedDeclarations' => $assignedDeclarations,
|
||||
'notifications' => $notifications,
|
||||
'workspaceName' => $workspace?->name ?? null,
|
||||
'foldersUrl' => $workspace ? route('folders.index') : null,
|
||||
'declarationsUrl' => $workspace ? route('declarations.index') : null,
|
||||
'clientsUrl' => $workspace ? route('clients.index') : null,
|
||||
]);
|
||||
}
|
||||
|
||||
332
app/Http/Controllers/DeclarationController.php
Normal file
332
app/Http/Controllers/DeclarationController.php
Normal file
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\DeclarationPriority;
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Enums\DeclarationType;
|
||||
use App\Http\Requests\StoreDeclarationRequest;
|
||||
use App\Http\Requests\UpdateDeclarationRequest;
|
||||
use App\Models\Client;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\MediaDownload;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class DeclarationController extends Controller
|
||||
{
|
||||
protected function declarationTypeLabels(): array
|
||||
{
|
||||
return [
|
||||
DeclarationType::VAT => 'TVA',
|
||||
DeclarationType::VatMonthly => 'TVA mensuelle',
|
||||
DeclarationType::VatQuarterly => 'TVA trimestrielle',
|
||||
DeclarationType::CorporateTax => 'IS',
|
||||
DeclarationType::IncomeTax => 'IR',
|
||||
DeclarationType::CNSS => 'CNSS',
|
||||
DeclarationType::AnnualBalance => 'Bilan',
|
||||
DeclarationType::Other => 'Autre',
|
||||
];
|
||||
}
|
||||
|
||||
protected function declarationStatusLabels(): array
|
||||
{
|
||||
return DeclarationStatus::labels();
|
||||
}
|
||||
|
||||
protected function declarationPriorityLabels(): array
|
||||
{
|
||||
return [
|
||||
DeclarationPriority::Low => 'Basse',
|
||||
DeclarationPriority::Medium => 'Normale',
|
||||
DeclarationPriority::High => 'Haute',
|
||||
];
|
||||
}
|
||||
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the declarations.
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 10), 10), 100);
|
||||
|
||||
$declarations = $workspace->declarations()
|
||||
->with(['client', 'assignee'])
|
||||
->latest()
|
||||
->paginate($perPage)
|
||||
->through(fn (Declaration $declaration) => [
|
||||
'id' => $declaration->id,
|
||||
'title' => $declaration->title,
|
||||
'type' => $declaration->type->value,
|
||||
'client_name' => $declaration->client->company_name,
|
||||
'status' => $declaration->status->value,
|
||||
'due_date' => $declaration->due_date?->format('Y-m-d'),
|
||||
'showUrl' => route('declarations.show', $declaration),
|
||||
'editUrl' => route('declarations.edit', $declaration),
|
||||
'destroyUrl' => route('declarations.destroy', $declaration),
|
||||
]);
|
||||
|
||||
return Inertia::render('declarations/Index', [
|
||||
'declarations' => $declarations,
|
||||
'createUrl' => route('declarations.create'),
|
||||
'workspaceName' => $workspace->name,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new declaration.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$initialClientId = $request->integer('client_id', 0) ?: null;
|
||||
|
||||
return Inertia::render('declarations/Create', [
|
||||
'indexUrl' => route('declarations.index'),
|
||||
'storeUrl' => route('declarations.store'),
|
||||
'initialClientId' => $initialClientId,
|
||||
'declarationTypeLabels' => $this->declarationTypeLabels(),
|
||||
'declarationStatusLabels' => $this->declarationStatusLabels(),
|
||||
'declarationPriorityLabels' => $this->declarationPriorityLabels(),
|
||||
'clients' => $workspace->clients()->orderBy('company_name')->get(['id', 'company_name'])->map(fn (Client $c) => [
|
||||
'id' => $c->id,
|
||||
'company_name' => $c->company_name,
|
||||
])->values()->all(),
|
||||
'workspaceUsers' => $workspace->users()
|
||||
->orderBy('users.name')
|
||||
->select('users.id', 'users.name', 'users.email')
|
||||
->get()
|
||||
->map(fn ($u) => [
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'email' => $u->email,
|
||||
])->values()->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created declaration in storage.
|
||||
*/
|
||||
public function store(StoreDeclarationRequest $request): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$data = $request->validated();
|
||||
$data['workspace_id'] = $workspace->id;
|
||||
$data['created_by'] = $request->user()?->id;
|
||||
$data['status'] = $data['status'] ?? DeclarationStatus::Created;
|
||||
|
||||
if (($data['type'] ?? '') === 'vat_monthly') {
|
||||
$data['period_quarter'] = null;
|
||||
} elseif (($data['type'] ?? '') === 'vat_quarterly') {
|
||||
$data['period_month'] = null;
|
||||
}
|
||||
|
||||
Declaration::query()->create($data);
|
||||
|
||||
return to_route('declarations.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified declaration.
|
||||
*/
|
||||
public function show(Request $request, Declaration $declaration): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
|
||||
$declaration->load(['client', 'creator', 'assignee', 'messages' => fn ($q) => $q->with(['senderUser', 'senderClient'])->latest()]);
|
||||
|
||||
$allMedia = $declaration->getMedia('documents');
|
||||
$downloadedMediaIds = MediaDownload::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->whereIn('media_id', $allMedia->pluck('id'))
|
||||
->pluck('media_id')
|
||||
->all();
|
||||
|
||||
$messages = $declaration->messages->map(function ($m) use ($declaration, $allMedia, $downloadedMediaIds) {
|
||||
$attachments = $allMedia
|
||||
->filter(fn ($media) => $media->getCustomProperty('message_id') === $m->id)
|
||||
->map(fn ($media) => [
|
||||
'id' => $media->id,
|
||||
'file_name' => $media->file_name,
|
||||
'mime_type' => $media->mime_type,
|
||||
'size' => $media->human_readable_size,
|
||||
'downloadUrl' => route('declarations.media.download', ['declaration' => $declaration, 'mediaId' => $media->id]),
|
||||
'is_downloaded' => in_array($media->id, $downloadedMediaIds),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$confirmationStatus = null;
|
||||
if ($m->type->value === 'confirmation') {
|
||||
$confirmationStatus = $declaration->refused_at ? 'refused' : ($declaration->validated_at ? 'confirmed' : 'pending');
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $m->id,
|
||||
'type' => $m->type->value,
|
||||
'body' => $m->body,
|
||||
'sent_by_type' => $m->sent_by_type->value,
|
||||
'sender_name' => $m->sender_name,
|
||||
'created_at' => $m->created_at->format('Y-m-d H:i'),
|
||||
'attachments' => $attachments,
|
||||
'confirmation_status' => $confirmationStatus,
|
||||
];
|
||||
})->values()->all();
|
||||
|
||||
$documents = $allMedia->map(fn ($m) => [
|
||||
'id' => $m->id,
|
||||
'name' => $m->name,
|
||||
'file_name' => $m->file_name,
|
||||
'size' => $m->human_readable_size,
|
||||
'created_at' => $m->created_at->format('d/m/Y H:i'),
|
||||
'uploaded_by' => $m->getCustomProperty('uploaded_by_type') === 'user' ? 'Comptable' : 'Client',
|
||||
'downloadUrl' => route('declarations.media.download', ['declaration' => $declaration, 'mediaId' => $m->id]),
|
||||
'is_downloaded' => in_array($m->id, $downloadedMediaIds),
|
||||
])->values()->all();
|
||||
|
||||
return Inertia::render('declarations/Show', [
|
||||
'declaration' => [
|
||||
'id' => $declaration->id,
|
||||
'title' => $declaration->title,
|
||||
'type' => $declaration->type->value,
|
||||
'client_id' => $declaration->client_id,
|
||||
'client_name' => $declaration->client->company_name,
|
||||
'period_year' => $declaration->period_year,
|
||||
'period_month' => $declaration->period_month,
|
||||
'period_quarter' => $declaration->period_quarter,
|
||||
'due_date' => $declaration->due_date?->format('Y-m-d'),
|
||||
'status' => $declaration->status->value,
|
||||
'priority' => $declaration->priority?->value,
|
||||
'assigned_to' => $declaration->assigned_to,
|
||||
'assignee_name' => $declaration->assignee?->name,
|
||||
'validated_at' => $declaration->validated_at?->format('Y-m-d H:i'),
|
||||
'closed_at' => $declaration->closed_at?->format('Y-m-d H:i'),
|
||||
'notes_internal' => $declaration->notes_internal,
|
||||
'notes_client' => $declaration->notes_client,
|
||||
'created_at' => $declaration->created_at?->format('Y-m-d H:i'),
|
||||
],
|
||||
'messages' => $messages,
|
||||
'documents' => $documents,
|
||||
'messagesStoreUrl' => route('declarations.messages.store', $declaration),
|
||||
'mediaStoreUrl' => route('declarations.media.store', $declaration),
|
||||
'messageTypeLabels' => [
|
||||
'invite' => 'Invitation',
|
||||
'situation' => 'Situation',
|
||||
'file_request' => 'Demande de pièces',
|
||||
'confirmation' => 'Demande de validation',
|
||||
'text' => 'Message',
|
||||
],
|
||||
'indexUrl' => route('declarations.index'),
|
||||
'editUrl' => route('declarations.edit', $declaration),
|
||||
'workspaceUsers' => $workspace->users()->orderBy('users.name')
|
||||
->select('users.id', 'users.name', 'users.email')
|
||||
->get()->map(fn ($u) => ['id' => $u->id, 'name' => $u->name])
|
||||
->values()->all(),
|
||||
'mentionStoreUrl' => route('declarations.mentions.store', $declaration),
|
||||
'canMention' => in_array(
|
||||
$workspace->users()->where('users.id', $request->user()->id)->first()?->pivot?->role?->value,
|
||||
['owner', 'manager']
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified declaration.
|
||||
*/
|
||||
public function edit(Request $request, Declaration $declaration): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
|
||||
return Inertia::render('declarations/Edit', [
|
||||
'declaration' => [
|
||||
'id' => $declaration->id,
|
||||
'title' => $declaration->title,
|
||||
'type' => $declaration->type->value,
|
||||
'client_id' => $declaration->client_id,
|
||||
'period_year' => $declaration->period_year,
|
||||
'period_month' => $declaration->period_month,
|
||||
'period_quarter' => $declaration->period_quarter,
|
||||
'due_date' => $declaration->due_date?->format('Y-m-d'),
|
||||
'status' => $declaration->status->value,
|
||||
'priority' => $declaration->priority?->value,
|
||||
'assigned_to' => $declaration->assigned_to,
|
||||
'notes_internal' => $declaration->notes_internal,
|
||||
'notes_client' => $declaration->notes_client,
|
||||
'created_at' => $declaration->created_at?->format('Y-m-d H:i'),
|
||||
],
|
||||
'indexUrl' => route('declarations.index'),
|
||||
'updateUrl' => route('declarations.update', $declaration),
|
||||
'declarationTypeLabels' => $this->declarationTypeLabels(),
|
||||
'declarationStatusLabels' => $this->declarationStatusLabels(),
|
||||
'declarationPriorityLabels' => $this->declarationPriorityLabels(),
|
||||
'clients' => $workspace->clients()->orderBy('company_name')->get(['id', 'company_name'])->map(fn (Client $c) => [
|
||||
'id' => $c->id,
|
||||
'company_name' => $c->company_name,
|
||||
])->values()->all(),
|
||||
'workspaceUsers' => $workspace->users()
|
||||
->orderBy('users.name')
|
||||
->select('users.id', 'users.name', 'users.email')
|
||||
->get()
|
||||
->map(fn ($u) => [
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'email' => $u->email,
|
||||
])->values()->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified declaration in storage.
|
||||
*/
|
||||
public function update(UpdateDeclarationRequest $request, Declaration $declaration): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
|
||||
$data = $request->validated();
|
||||
|
||||
if (($data['type'] ?? '') === 'vat_monthly') {
|
||||
$data['period_quarter'] = null;
|
||||
} elseif (($data['type'] ?? '') === 'vat_quarterly') {
|
||||
$data['period_month'] = null;
|
||||
}
|
||||
|
||||
$declaration->update($data);
|
||||
|
||||
return to_route('declarations.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified declaration from storage.
|
||||
*/
|
||||
public function destroy(Request $request, Declaration $declaration): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
|
||||
$declaration->delete();
|
||||
|
||||
return to_route('declarations.index');
|
||||
}
|
||||
|
||||
protected function authorizeDeclaration(Workspace $workspace, Declaration $declaration): void
|
||||
{
|
||||
if ($declaration->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\ActorType;
|
||||
use App\Models\Folder;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\MediaDownload;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -11,7 +11,7 @@ use Illuminate\Http\Request;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class FolderMediaController extends Controller
|
||||
class DeclarationMediaController extends Controller
|
||||
{
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
@@ -23,11 +23,11 @@ class FolderMediaController extends Controller
|
||||
/**
|
||||
* Store a newly uploaded file.
|
||||
*/
|
||||
public function store(Request $request, Folder $folder): RedirectResponse
|
||||
public function store(Request $request, Declaration $declaration): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
if ($folder->workspace_id !== $workspace->id) {
|
||||
if ($declaration->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class FolderMediaController extends Controller
|
||||
$user = $request->user();
|
||||
|
||||
foreach ($request->file('files') as $file) {
|
||||
$folder->addMedia($file)
|
||||
$declaration->addMedia($file)
|
||||
->withCustomProperties([
|
||||
'uploaded_by_type' => ActorType::User,
|
||||
'uploaded_by_id' => $user->id,
|
||||
@@ -53,17 +53,17 @@ class FolderMediaController extends Controller
|
||||
/**
|
||||
* Download a media file.
|
||||
*/
|
||||
public function download(Request $request, Folder $folder, int $mediaId): Response
|
||||
public function download(Request $request, Declaration $declaration, int $mediaId): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
if ($folder->workspace_id !== $workspace->id) {
|
||||
if ($declaration->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$media = Media::query()
|
||||
->where('model_type', Folder::class)
|
||||
->where('model_id', $folder->id)
|
||||
->where('model_type', Declaration::class)
|
||||
->where('model_id', $declaration->id)
|
||||
->where('id', $mediaId)
|
||||
->firstOrFail();
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreFolderMentionRequest;
|
||||
use App\Models\Folder;
|
||||
use App\Http\Requests\StoreDeclarationMentionRequest;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Notifications\FolderMentionNotification;
|
||||
use App\Notifications\DeclarationMentionNotification;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class FolderMentionController extends Controller
|
||||
class DeclarationMentionController extends Controller
|
||||
{
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
@@ -20,17 +20,17 @@ class FolderMentionController extends Controller
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
}
|
||||
|
||||
protected function authorizeFolder(Workspace $workspace, Folder $folder): void
|
||||
protected function authorizeDeclaration(Workspace $workspace, Declaration $declaration): void
|
||||
{
|
||||
if ($folder->workspace_id !== $workspace->id) {
|
||||
if ($declaration->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(StoreFolderMentionRequest $request, Folder $folder): RedirectResponse
|
||||
public function store(StoreDeclarationMentionRequest $request, Declaration $declaration): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeFolder($workspace, $folder);
|
||||
$this->authorizeDeclaration($workspace, $declaration);
|
||||
|
||||
$userRole = $workspace->users()
|
||||
->where('users.id', $request->user()->id)
|
||||
@@ -46,8 +46,8 @@ class FolderMentionController extends Controller
|
||||
$validated = $request->validated();
|
||||
$targetUser = User::findOrFail($validated['user_id']);
|
||||
|
||||
$targetUser->notify(new FolderMentionNotification(
|
||||
$folder,
|
||||
$targetUser->notify(new DeclarationMentionNotification(
|
||||
$declaration,
|
||||
$request->user(),
|
||||
$validated['message'],
|
||||
));
|
||||
156
app/Http/Controllers/DeclarationMessageController.php
Normal file
156
app/Http/Controllers/DeclarationMessageController.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\ActorType;
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Enums\MessageType;
|
||||
use App\Http\Requests\StoreDeclarationMessageRequest;
|
||||
use App\Mail\DeclarationConfirmationMail;
|
||||
use App\Mail\DeclarationFileRequestMail;
|
||||
use App\Mail\DeclarationInviteMail;
|
||||
use App\Mail\DeclarationSituationMail;
|
||||
use App\Mail\DeclarationTextMessageMail;
|
||||
use App\Models\Declaration;
|
||||
use App\Models\DeclarationInvitation;
|
||||
use App\Models\Message;
|
||||
use App\Models\Workspace;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class DeclarationMessageController extends Controller
|
||||
{
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created message.
|
||||
*/
|
||||
public function store(StoreDeclarationMessageRequest $request, Declaration $declaration): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
if ($declaration->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
$type = MessageType::fromValue($request->input('type'));
|
||||
$body = $request->input('body');
|
||||
|
||||
$invitation = $type->is(MessageType::Invite)
|
||||
? $this->createInvitation($declaration)
|
||||
: $this->getOrCreateInvitation($declaration);
|
||||
|
||||
$metadata = ['invitation_id' => $invitation->id];
|
||||
$message = $declaration->messages()->create([
|
||||
'type' => $type,
|
||||
'body' => $body,
|
||||
'sent_by_type' => ActorType::User,
|
||||
'sent_by_id' => $user->id,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
$mediaIds = [];
|
||||
|
||||
if ($request->hasFile('files')) {
|
||||
foreach ($request->file('files') as $file) {
|
||||
$media = $declaration->addMedia($file)
|
||||
->withCustomProperties([
|
||||
'message_id' => $message->id,
|
||||
'uploaded_by_type' => ActorType::User,
|
||||
'uploaded_by_id' => $user->id,
|
||||
])
|
||||
->toMediaCollection('documents');
|
||||
$mediaIds[] = $media->id;
|
||||
}
|
||||
$message->update(['metadata' => array_merge($metadata, ['media_ids' => $mediaIds])]);
|
||||
}
|
||||
|
||||
$this->updateDeclarationStatusAndConfirmation($declaration, $type, $mediaIds);
|
||||
|
||||
$emailSent = $this->sendEmailForMessage($declaration, $invitation, $message, $body, $type);
|
||||
$flashMessage = $emailSent
|
||||
? 'Message envoyé.'
|
||||
: 'Message enregistré, mais l\'email du client n\'est pas configuré.';
|
||||
|
||||
return back()->with('flash', ['type' => 'success', 'message' => $flashMessage]);
|
||||
}
|
||||
|
||||
protected function createInvitation(Declaration $declaration): DeclarationInvitation
|
||||
{
|
||||
$declaration->load('client.primaryContact');
|
||||
|
||||
return $declaration->invitations()->create([
|
||||
'email' => $declaration->client->primary_contact_email,
|
||||
'expires_at' => Carbon::now()->addDays(7),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getOrCreateInvitation(Declaration $declaration): DeclarationInvitation
|
||||
{
|
||||
$invitation = $declaration->invitations()
|
||||
->where('expires_at', '>', now())
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($invitation) {
|
||||
return $invitation;
|
||||
}
|
||||
|
||||
return $this->createInvitation($declaration);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $mediaIds
|
||||
*/
|
||||
protected function updateDeclarationStatusAndConfirmation(Declaration $declaration, MessageType $type, array $mediaIds): void
|
||||
{
|
||||
// Transition through en_cours first if declaration is still in created status,
|
||||
// since created → en_attente_client is not a valid direct transition.
|
||||
if ($declaration->status->is(DeclarationStatus::Created)) {
|
||||
$declaration->update(['status' => DeclarationStatus::EnCours]);
|
||||
$declaration->refresh();
|
||||
}
|
||||
|
||||
match ($type->value) {
|
||||
'invite' => $declaration->update(['status' => DeclarationStatus::EnAttenteClient]),
|
||||
'situation', 'file_request' => $declaration->update(['status' => DeclarationStatus::EnAttenteClient]),
|
||||
'confirmation' => $declaration->update([
|
||||
'status' => DeclarationStatus::EnAttenteClient,
|
||||
'confirmation_requested_at' => now(),
|
||||
'confirmation_media_id' => $mediaIds[0] ?? null,
|
||||
]),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
protected function sendEmailForMessage(Declaration $declaration, DeclarationInvitation $invitation, Message $message, string $body, MessageType $type): bool
|
||||
{
|
||||
$declaration->load('client.primaryContact');
|
||||
$clientEmail = $declaration->client->primary_contact_email;
|
||||
|
||||
if (empty($clientEmail)) {
|
||||
\Illuminate\Support\Facades\Log::warning("No primary contact email for client #{$declaration->client_id}, skipping email.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
match ($type->value) {
|
||||
'invite' => Mail::to($clientEmail)->send(new DeclarationInviteMail($declaration, $invitation)),
|
||||
'situation' => Mail::to($clientEmail)->send(new DeclarationSituationMail($declaration, $invitation, $body)),
|
||||
'file_request' => Mail::to($clientEmail)->send(new DeclarationFileRequestMail($declaration, $invitation, $body)),
|
||||
'confirmation' => Mail::to($clientEmail)->send(new DeclarationConfirmationMail($declaration, $invitation, $body)),
|
||||
'text' => Mail::to($clientEmail)->send(new DeclarationTextMessageMail($declaration, $body, $invitation->token)),
|
||||
default => null,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\FolderPriority;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\FolderType;
|
||||
use App\Http\Requests\StoreFolderRequest;
|
||||
use App\Http\Requests\UpdateFolderRequest;
|
||||
use App\Models\Client;
|
||||
use App\Models\Folder;
|
||||
use App\Models\MediaDownload;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class FolderController extends Controller
|
||||
{
|
||||
protected function folderTypeLabels(): array
|
||||
{
|
||||
return [
|
||||
FolderType::VAT => 'TVA',
|
||||
FolderType::VatMonthly => 'TVA mensuelle',
|
||||
FolderType::VatQuarterly => 'TVA trimestrielle',
|
||||
FolderType::CorporateTax => 'IS',
|
||||
FolderType::IncomeTax => 'IR',
|
||||
FolderType::CNSS => 'CNSS',
|
||||
FolderType::AnnualBalance => 'Bilan',
|
||||
FolderType::Other => 'Autre',
|
||||
];
|
||||
}
|
||||
|
||||
protected function folderStatusLabels(): array
|
||||
{
|
||||
return [
|
||||
FolderStatus::Draft => 'Brouillon',
|
||||
FolderStatus::WaitingDocuments => 'En attente documents',
|
||||
FolderStatus::DocumentsReceived => 'Documents reçus',
|
||||
FolderStatus::Processing => 'En cours de traitement',
|
||||
FolderStatus::AdditionalDocumentsRequested => 'Pièces complémentaires demandées',
|
||||
FolderStatus::WaitingClientValidation => 'En attente validation client',
|
||||
FolderStatus::Validated => 'Validé',
|
||||
FolderStatus::Closed => 'Clôturé',
|
||||
FolderStatus::Cancelled => 'Annulé',
|
||||
];
|
||||
}
|
||||
|
||||
protected function folderPriorityLabels(): array
|
||||
{
|
||||
return [
|
||||
FolderPriority::Low => 'Basse',
|
||||
FolderPriority::Medium => 'Normale',
|
||||
FolderPriority::High => 'Haute',
|
||||
];
|
||||
}
|
||||
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the folders.
|
||||
*/
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
$perPage = min(max((int) $request->input('per_page', 10), 10), 100);
|
||||
|
||||
$folders = $workspace->folders()
|
||||
->with(['client', 'assignee'])
|
||||
->latest()
|
||||
->paginate($perPage)
|
||||
->through(fn (Folder $folder) => [
|
||||
'id' => $folder->id,
|
||||
'title' => $folder->title,
|
||||
'type' => $folder->type->value,
|
||||
'client_name' => $folder->client->company_name,
|
||||
'status' => $folder->status->value,
|
||||
'due_date' => $folder->due_date?->format('Y-m-d'),
|
||||
'showUrl' => route('folders.show', $folder),
|
||||
'editUrl' => route('folders.edit', $folder),
|
||||
'destroyUrl' => route('folders.destroy', $folder),
|
||||
]);
|
||||
|
||||
return Inertia::render('folders/Index', [
|
||||
'folders' => $folders,
|
||||
'createUrl' => route('folders.create'),
|
||||
'workspaceName' => $workspace->name,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new folder.
|
||||
*/
|
||||
public function create(Request $request): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$initialClientId = $request->integer('client_id', 0) ?: null;
|
||||
|
||||
return Inertia::render('folders/Create', [
|
||||
'indexUrl' => route('folders.index'),
|
||||
'storeUrl' => route('folders.store'),
|
||||
'initialClientId' => $initialClientId,
|
||||
'folderTypeLabels' => $this->folderTypeLabels(),
|
||||
'folderStatusLabels' => $this->folderStatusLabels(),
|
||||
'folderPriorityLabels' => $this->folderPriorityLabels(),
|
||||
'clients' => $workspace->clients()->orderBy('company_name')->get(['id', 'company_name'])->map(fn (Client $c) => [
|
||||
'id' => $c->id,
|
||||
'company_name' => $c->company_name,
|
||||
])->values()->all(),
|
||||
'workspaceUsers' => $workspace->users()
|
||||
->orderBy('users.name')
|
||||
->select('users.id', 'users.name', 'users.email')
|
||||
->get()
|
||||
->map(fn ($u) => [
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'email' => $u->email,
|
||||
])->values()->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created folder in storage.
|
||||
*/
|
||||
public function store(StoreFolderRequest $request): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$data = $request->validated();
|
||||
$data['workspace_id'] = $workspace->id;
|
||||
$data['created_by'] = $request->user()?->id;
|
||||
$data['status'] = $data['status'] ?? FolderStatus::Draft->value;
|
||||
|
||||
Folder::query()->create($data);
|
||||
|
||||
return to_route('folders.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified folder.
|
||||
*/
|
||||
public function show(Request $request, Folder $folder): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeFolder($workspace, $folder);
|
||||
|
||||
$folder->load(['client', 'creator', 'assignee', 'messages' => fn ($q) => $q->with(['senderUser', 'senderClient'])->latest()]);
|
||||
|
||||
$allMedia = $folder->getMedia('documents');
|
||||
$downloadedMediaIds = MediaDownload::query()
|
||||
->where('user_id', $request->user()->id)
|
||||
->whereIn('media_id', $allMedia->pluck('id'))
|
||||
->pluck('media_id')
|
||||
->all();
|
||||
|
||||
$messages = $folder->messages->map(function ($m) use ($folder, $allMedia, $downloadedMediaIds) {
|
||||
$attachments = $allMedia
|
||||
->filter(fn ($media) => $media->getCustomProperty('message_id') === $m->id)
|
||||
->map(fn ($media) => [
|
||||
'id' => $media->id,
|
||||
'file_name' => $media->file_name,
|
||||
'mime_type' => $media->mime_type,
|
||||
'size' => $media->human_readable_size,
|
||||
'downloadUrl' => route('folders.media.download', ['folder' => $folder, 'mediaId' => $media->id]),
|
||||
'is_downloaded' => in_array($media->id, $downloadedMediaIds),
|
||||
])
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$confirmationStatus = null;
|
||||
if ($m->type->value === 'confirmation') {
|
||||
$confirmationStatus = $folder->refused_at ? 'refused' : ($folder->validated_at ? 'confirmed' : 'pending');
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $m->id,
|
||||
'type' => $m->type->value,
|
||||
'body' => $m->body,
|
||||
'sent_by_type' => $m->sent_by_type->value,
|
||||
'sender_name' => $m->sender_name,
|
||||
'created_at' => $m->created_at->format('Y-m-d H:i'),
|
||||
'attachments' => $attachments,
|
||||
'confirmation_status' => $confirmationStatus,
|
||||
];
|
||||
})->values()->all();
|
||||
|
||||
$documents = $allMedia->map(fn ($m) => [
|
||||
'id' => $m->id,
|
||||
'name' => $m->name,
|
||||
'file_name' => $m->file_name,
|
||||
'size' => $m->human_readable_size,
|
||||
'created_at' => $m->created_at->format('d/m/Y H:i'),
|
||||
'uploaded_by' => $m->getCustomProperty('uploaded_by_type') === 'user' ? 'Comptable' : 'Client',
|
||||
'downloadUrl' => route('folders.media.download', ['folder' => $folder, 'mediaId' => $m->id]),
|
||||
'is_downloaded' => in_array($m->id, $downloadedMediaIds),
|
||||
])->values()->all();
|
||||
|
||||
return Inertia::render('folders/Show', [
|
||||
'folder' => [
|
||||
'id' => $folder->id,
|
||||
'title' => $folder->title,
|
||||
'type' => $folder->type->value,
|
||||
'client_id' => $folder->client_id,
|
||||
'client_name' => $folder->client->company_name,
|
||||
'period_year' => $folder->period_year,
|
||||
'period_month' => $folder->period_month,
|
||||
'period_quarter' => $folder->period_quarter,
|
||||
'due_date' => $folder->due_date?->format('Y-m-d'),
|
||||
'status' => $folder->status->value,
|
||||
'priority' => $folder->priority?->value,
|
||||
'assigned_to' => $folder->assigned_to,
|
||||
'assignee_name' => $folder->assignee?->name,
|
||||
'validated_at' => $folder->validated_at?->format('Y-m-d H:i'),
|
||||
'closed_at' => $folder->closed_at?->format('Y-m-d H:i'),
|
||||
'notes_internal' => $folder->notes_internal,
|
||||
'notes_client' => $folder->notes_client,
|
||||
'created_at' => $folder->created_at?->format('Y-m-d H:i'),
|
||||
],
|
||||
'messages' => $messages,
|
||||
'documents' => $documents,
|
||||
'messagesStoreUrl' => route('folders.messages.store', $folder),
|
||||
'mediaStoreUrl' => route('folders.media.store', $folder),
|
||||
'messageTypeLabels' => [
|
||||
'invite' => 'Invitation',
|
||||
'situation' => 'Situation',
|
||||
'file_request' => 'Demande de pièces',
|
||||
'confirmation' => 'Demande de validation',
|
||||
'text' => 'Message',
|
||||
],
|
||||
'indexUrl' => route('folders.index'),
|
||||
'editUrl' => route('folders.edit', $folder),
|
||||
'workspaceUsers' => $workspace->users()->orderBy('users.name')
|
||||
->select('users.id', 'users.name', 'users.email')
|
||||
->get()->map(fn ($u) => ['id' => $u->id, 'name' => $u->name])
|
||||
->values()->all(),
|
||||
'mentionStoreUrl' => route('folders.mentions.store', $folder),
|
||||
'canMention' => in_array(
|
||||
$workspace->users()->where('users.id', $request->user()->id)->first()?->pivot?->role?->value,
|
||||
['owner', 'manager']
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified folder.
|
||||
*/
|
||||
public function edit(Request $request, Folder $folder): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeFolder($workspace, $folder);
|
||||
|
||||
return Inertia::render('folders/Edit', [
|
||||
'folder' => [
|
||||
'id' => $folder->id,
|
||||
'title' => $folder->title,
|
||||
'type' => $folder->type->value,
|
||||
'client_id' => $folder->client_id,
|
||||
'period_year' => $folder->period_year,
|
||||
'period_month' => $folder->period_month,
|
||||
'period_quarter' => $folder->period_quarter,
|
||||
'due_date' => $folder->due_date?->format('Y-m-d'),
|
||||
'status' => $folder->status->value,
|
||||
'priority' => $folder->priority?->value,
|
||||
'assigned_to' => $folder->assigned_to,
|
||||
'notes_internal' => $folder->notes_internal,
|
||||
'notes_client' => $folder->notes_client,
|
||||
'created_at' => $folder->created_at?->format('Y-m-d H:i'),
|
||||
],
|
||||
'indexUrl' => route('folders.index'),
|
||||
'updateUrl' => route('folders.update', $folder),
|
||||
'folderTypeLabels' => $this->folderTypeLabels(),
|
||||
'folderStatusLabels' => $this->folderStatusLabels(),
|
||||
'folderPriorityLabels' => $this->folderPriorityLabels(),
|
||||
'clients' => $workspace->clients()->orderBy('company_name')->get(['id', 'company_name'])->map(fn (Client $c) => [
|
||||
'id' => $c->id,
|
||||
'company_name' => $c->company_name,
|
||||
])->values()->all(),
|
||||
'workspaceUsers' => $workspace->users()
|
||||
->orderBy('users.name')
|
||||
->select('users.id', 'users.name', 'users.email')
|
||||
->get()
|
||||
->map(fn ($u) => [
|
||||
'id' => $u->id,
|
||||
'name' => $u->name,
|
||||
'email' => $u->email,
|
||||
])->values()->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified folder in storage.
|
||||
*/
|
||||
public function update(UpdateFolderRequest $request, Folder $folder): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeFolder($workspace, $folder);
|
||||
|
||||
$folder->update($request->validated());
|
||||
|
||||
return to_route('folders.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified folder from storage.
|
||||
*/
|
||||
public function destroy(Request $request, Folder $folder): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeFolder($workspace, $folder);
|
||||
|
||||
$folder->delete();
|
||||
|
||||
return to_route('folders.index');
|
||||
}
|
||||
|
||||
protected function authorizeFolder(Workspace $workspace, Folder $folder): void
|
||||
{
|
||||
if ($folder->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\ActorType;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\MessageType;
|
||||
use App\Http\Requests\StoreFolderMessageRequest;
|
||||
use App\Mail\FolderConfirmationMail;
|
||||
use App\Mail\FolderFileRequestMail;
|
||||
use App\Mail\FolderInviteMail;
|
||||
use App\Mail\FolderSituationMail;
|
||||
use App\Mail\FolderTextMessageMail;
|
||||
use App\Models\Folder;
|
||||
use App\Models\FolderInvitation;
|
||||
use App\Models\Message;
|
||||
use App\Models\Workspace;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
class FolderMessageController extends Controller
|
||||
{
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created message.
|
||||
*/
|
||||
public function store(StoreFolderMessageRequest $request, Folder $folder): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
if ($folder->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$user = $request->user();
|
||||
$type = MessageType::fromValue($request->input('type'));
|
||||
$body = $request->input('body');
|
||||
|
||||
$invitation = $type->is(MessageType::Invite)
|
||||
? $this->createInvitation($folder)
|
||||
: $this->getOrCreateInvitation($folder);
|
||||
|
||||
$metadata = ['invitation_id' => $invitation->id];
|
||||
$message = $folder->messages()->create([
|
||||
'type' => $type,
|
||||
'body' => $body,
|
||||
'sent_by_type' => ActorType::User,
|
||||
'sent_by_id' => $user->id,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
$mediaIds = [];
|
||||
|
||||
if ($request->hasFile('files')) {
|
||||
foreach ($request->file('files') as $file) {
|
||||
$media = $folder->addMedia($file)
|
||||
->withCustomProperties([
|
||||
'message_id' => $message->id,
|
||||
'uploaded_by_type' => ActorType::User,
|
||||
'uploaded_by_id' => $user->id,
|
||||
])
|
||||
->toMediaCollection('documents');
|
||||
$mediaIds[] = $media->id;
|
||||
}
|
||||
$message->update(['metadata' => array_merge($metadata, ['media_ids' => $mediaIds])]);
|
||||
}
|
||||
|
||||
$this->updateFolderStatusAndConfirmation($folder, $type, $mediaIds);
|
||||
|
||||
$emailSent = $this->sendEmailForMessage($folder, $invitation, $message, $body, $type);
|
||||
$flashMessage = $emailSent
|
||||
? 'Message envoyé.'
|
||||
: 'Message enregistré, mais l\'email du client n\'est pas configuré.';
|
||||
|
||||
return back()->with('flash', ['type' => 'success', 'message' => $flashMessage]);
|
||||
}
|
||||
|
||||
protected function createInvitation(Folder $folder): FolderInvitation
|
||||
{
|
||||
$folder->load('client.primaryContact');
|
||||
|
||||
return $folder->invitations()->create([
|
||||
'email' => $folder->client->primary_contact_email,
|
||||
'expires_at' => Carbon::now()->addDays(7),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getOrCreateInvitation(Folder $folder): FolderInvitation
|
||||
{
|
||||
$invitation = $folder->invitations()
|
||||
->where('expires_at', '>', now())
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if ($invitation) {
|
||||
return $invitation;
|
||||
}
|
||||
|
||||
return $this->createInvitation($folder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int> $mediaIds
|
||||
*/
|
||||
protected function updateFolderStatusAndConfirmation(Folder $folder, MessageType $type, array $mediaIds): void
|
||||
{
|
||||
match ($type->value) {
|
||||
'invite' => $folder->update(['status' => FolderStatus::WaitingDocuments]),
|
||||
'situation', 'file_request' => $folder->update(['status' => FolderStatus::AdditionalDocumentsRequested]),
|
||||
'confirmation' => $folder->update([
|
||||
'status' => FolderStatus::WaitingClientValidation,
|
||||
'confirmation_requested_at' => now(),
|
||||
'confirmation_media_id' => $mediaIds[0] ?? null,
|
||||
]),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
protected function sendEmailForMessage(Folder $folder, FolderInvitation $invitation, Message $message, string $body, MessageType $type): bool
|
||||
{
|
||||
$folder->load('client.primaryContact');
|
||||
$clientEmail = $folder->client->primary_contact_email;
|
||||
|
||||
if (empty($clientEmail)) {
|
||||
\Illuminate\Support\Facades\Log::warning("No primary contact email for client #{$folder->client_id}, skipping email.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
match ($type->value) {
|
||||
'invite' => Mail::to($clientEmail)->send(new FolderInviteMail($folder, $invitation)),
|
||||
'situation' => Mail::to($clientEmail)->send(new FolderSituationMail($folder, $invitation, $body)),
|
||||
'file_request' => Mail::to($clientEmail)->send(new FolderFileRequestMail($folder, $invitation, $body)),
|
||||
'confirmation' => Mail::to($clientEmail)->send(new FolderConfirmationMail($folder, $invitation, $body)),
|
||||
'text' => Mail::to($clientEmail)->send(new FolderTextMessageMail($folder, $body, $invitation->token)),
|
||||
default => null,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -81,20 +81,19 @@ class WorkspaceController extends Controller
|
||||
$workspace->load('users');
|
||||
|
||||
$clientsCount = $workspace->clients()->count();
|
||||
$foldersCount = $workspace->folders()->count();
|
||||
$foldersByStatus = $workspace->folders()
|
||||
$declarationsCount = $workspace->declarations()->count();
|
||||
$declarationsByStatus = $workspace->declarations()
|
||||
->selectRaw('status, count(*) as count')
|
||||
->groupBy('status')
|
||||
->pluck('count', 'status')
|
||||
->all();
|
||||
$foldersThisMonth = $workspace->folders()
|
||||
$declarationsThisMonth = $workspace->declarations()
|
||||
->whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->count();
|
||||
$foldersNeedingAttention = $workspace->folders()
|
||||
$declarationsNeedingAttention = $workspace->declarations()
|
||||
->whereIn('status', [
|
||||
\App\Enums\FolderStatus::WaitingDocuments,
|
||||
\App\Enums\FolderStatus::WaitingClientValidation,
|
||||
\App\Enums\DeclarationStatus::EnAttenteClient,
|
||||
])
|
||||
->count();
|
||||
|
||||
@@ -112,10 +111,10 @@ class WorkspaceController extends Controller
|
||||
],
|
||||
'stats' => [
|
||||
'clients' => $clientsCount,
|
||||
'folders' => $foldersCount,
|
||||
'folders_by_status' => $foldersByStatus,
|
||||
'folders_this_month' => $foldersThisMonth,
|
||||
'folders_needing_attention' => $foldersNeedingAttention,
|
||||
'declarations' => $declarationsCount,
|
||||
'declarations_by_status' => $declarationsByStatus,
|
||||
'declarations_this_month' => $declarationsThisMonth,
|
||||
'declarations_needing_attention' => $declarationsNeedingAttention,
|
||||
],
|
||||
'indexUrl' => route('workspaces.index'),
|
||||
'editUrl' => route('workspaces.edit', $workspace),
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\FolderInvitation;
|
||||
use App\Models\DeclarationInvitation;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ValidateFolderInvitation
|
||||
class ValidateClientPortalToken
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
@@ -18,16 +18,16 @@ class ValidateFolderInvitation
|
||||
{
|
||||
$token = $request->route('token');
|
||||
|
||||
$invitation = FolderInvitation::query()
|
||||
$invitation = DeclarationInvitation::query()
|
||||
->where('token', $token)
|
||||
->with(['folder.client', 'folder.assignee', 'folder.creator'])
|
||||
->with(['declaration.client', 'declaration.assignee', 'declaration.creator'])
|
||||
->first();
|
||||
|
||||
if (! $invitation || ! $invitation->isValid()) {
|
||||
abort(404, 'Lien invalide ou expiré.');
|
||||
}
|
||||
|
||||
$request->attributes->set('folder_invitation', $invitation);
|
||||
$request->attributes->set('declaration_invitation', $invitation);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
@@ -5,7 +5,7 @@ namespace App\Http\Requests;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreFolderMentionRequest extends FormRequest
|
||||
class StoreDeclarationMentionRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
@@ -6,7 +6,7 @@ use App\Enums\MessageType;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreFolderMessageRequest extends FormRequest
|
||||
class StoreDeclarationMessageRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\FolderPriority;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\FolderType;
|
||||
use App\Enums\DeclarationPriority;
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Enums\DeclarationType;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class StoreFolderRequest extends FormRequest
|
||||
class StoreDeclarationRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
@@ -59,13 +59,13 @@ class StoreFolderRequest extends FormRequest
|
||||
Rule::exists('clients', 'id')->where('workspace_id', $workspaceId),
|
||||
],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', new EnumValue(FolderType::class)],
|
||||
'type' => ['required', new EnumValue(DeclarationType::class)],
|
||||
'period_year' => ['required', 'integer', 'min:2000', 'max:2100'],
|
||||
'period_month' => ['nullable', 'integer', 'min:1', 'max:12'],
|
||||
'period_quarter' => ['nullable', 'integer', 'min:1', 'max:4'],
|
||||
'due_date' => ['nullable', 'date'],
|
||||
'status' => ['nullable', Rule::in(FolderStatus::getValues())],
|
||||
'priority' => ['nullable', Rule::in(FolderPriority::getValues())],
|
||||
'status' => ['nullable', Rule::in(DeclarationStatus::getValues())],
|
||||
'priority' => ['nullable', Rule::in(DeclarationPriority::getValues())],
|
||||
'assigned_to' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
@@ -31,7 +31,7 @@ class StoreWorkspaceRequest extends FormRequest
|
||||
'user_ids' => ['array'],
|
||||
'user_ids.*' => ['integer', 'exists:users,id'],
|
||||
'user_roles' => ['nullable', 'array'],
|
||||
'user_roles.*' => ['string', 'in:' . implode(',', WorkspaceUserRole::getValues())],
|
||||
'user_roles.*' => ['string', 'in:'.implode(',', WorkspaceUserRole::getValues())],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\FolderPriority;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\FolderType;
|
||||
use App\Enums\DeclarationPriority;
|
||||
use App\Enums\DeclarationStatus;
|
||||
use App\Enums\DeclarationType;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class UpdateFolderRequest extends FormRequest
|
||||
class UpdateDeclarationRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
@@ -59,13 +59,13 @@ class UpdateFolderRequest extends FormRequest
|
||||
Rule::exists('clients', 'id')->where('workspace_id', $workspaceId),
|
||||
],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', new EnumValue(FolderType::class)],
|
||||
'type' => ['required', new EnumValue(DeclarationType::class)],
|
||||
'period_year' => ['required', 'integer', 'min:2000', 'max:2100'],
|
||||
'period_month' => ['nullable', 'integer', 'min:1', 'max:12'],
|
||||
'period_quarter' => ['nullable', 'integer', 'min:1', 'max:4'],
|
||||
'due_date' => ['nullable', 'date'],
|
||||
'status' => ['nullable', Rule::in(FolderStatus::getValues())],
|
||||
'priority' => ['nullable', Rule::in(FolderPriority::getValues())],
|
||||
'status' => ['nullable', Rule::in(DeclarationStatus::getValues())],
|
||||
'priority' => ['nullable', Rule::in(DeclarationPriority::getValues())],
|
||||
'assigned_to' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
@@ -34,7 +34,7 @@ class UpdateWorkspaceRequest extends FormRequest
|
||||
'user_ids' => ['array'],
|
||||
'user_ids.*' => ['integer', 'exists:users,id'],
|
||||
'user_roles' => ['nullable', 'array'],
|
||||
'user_roles.*' => ['string', 'in:' . implode(',', WorkspaceUserRole::getValues())],
|
||||
'user_roles.*' => ['string', 'in:'.implode(',', WorkspaceUserRole::getValues())],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user