chore: add BMAD framework modules, folder features, and tooling configs
Includes BMAD bmb/bmm/cis/tea workflow modules, folder (declaration) feature implementation (controllers, models, enums, views, tests), claude/cursor command configs, and email templates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
14
app/Enums/FolderPriority.php
Normal file
14
app/Enums/FolderPriority.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use BenSampo\Enum\Enum;
|
||||
|
||||
final class FolderPriority extends Enum
|
||||
{
|
||||
const Low = 'low';
|
||||
|
||||
const Medium = 'medium';
|
||||
|
||||
const High = 'high';
|
||||
}
|
||||
26
app/Enums/FolderStatus.php
Normal file
26
app/Enums/FolderStatus.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use BenSampo\Enum\Enum;
|
||||
|
||||
final class FolderStatus extends Enum
|
||||
{
|
||||
const Draft = 'draft';
|
||||
|
||||
const WaitingDocuments = 'waiting_documents';
|
||||
|
||||
const DocumentsReceived = 'documents_received';
|
||||
|
||||
const Processing = 'processing';
|
||||
|
||||
const AdditionalDocumentsRequested = 'additional_documents_requested';
|
||||
|
||||
const WaitingClientValidation = 'waiting_client_validation';
|
||||
|
||||
const Validated = 'validated';
|
||||
|
||||
const Closed = 'closed';
|
||||
|
||||
const Cancelled = 'cancelled';
|
||||
}
|
||||
24
app/Enums/FolderType.php
Normal file
24
app/Enums/FolderType.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use BenSampo\Enum\Enum;
|
||||
|
||||
final class FolderType extends Enum
|
||||
{
|
||||
const VAT = 'vat';
|
||||
|
||||
const VatMonthly = 'vat_monthly';
|
||||
|
||||
const VatQuarterly = 'vat_quarterly';
|
||||
|
||||
const CorporateTax = 'corporate_tax';
|
||||
|
||||
const IncomeTax = 'income_tax';
|
||||
|
||||
const CNSS = 'cnss';
|
||||
|
||||
const AnnualBalance = 'annual_balance';
|
||||
|
||||
const Other = 'other';
|
||||
}
|
||||
328
app/Http/Controllers/FolderController.php
Normal file
328
app/Http/Controllers/FolderController.php
Normal file
@@ -0,0 +1,328 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
app/Http/Controllers/FolderMediaController.php
Normal file
81
app/Http/Controllers/FolderMediaController.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\ActorType;
|
||||
use App\Models\Folder;
|
||||
use App\Models\MediaDownload;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Spatie\MediaLibrary\MediaCollections\Models\Media;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class FolderMediaController extends Controller
|
||||
{
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly uploaded file.
|
||||
*/
|
||||
public function store(Request $request, Folder $folder): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
if ($folder->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'files' => ['required', 'array', 'min:1'],
|
||||
'files.*' => ['file', 'max:10240'],
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
foreach ($request->file('files') as $file) {
|
||||
$folder->addMedia($file)
|
||||
->withCustomProperties([
|
||||
'uploaded_by_type' => ActorType::User,
|
||||
'uploaded_by_id' => $user->id,
|
||||
])
|
||||
->toMediaCollection('documents');
|
||||
}
|
||||
|
||||
return back()->with('flash', ['type' => 'success', 'message' => 'Fichier(s) téléchargé(s).']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a media file.
|
||||
*/
|
||||
public function download(Request $request, Folder $folder, int $mediaId): Response
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
|
||||
if ($folder->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$media = Media::query()
|
||||
->where('model_type', Folder::class)
|
||||
->where('model_id', $folder->id)
|
||||
->where('id', $mediaId)
|
||||
->firstOrFail();
|
||||
|
||||
try {
|
||||
MediaDownload::query()->updateOrCreate(
|
||||
['media_id' => $media->id, 'user_id' => $request->user()->id],
|
||||
['downloaded_at' => now()],
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// Tracking failure must never block the download
|
||||
}
|
||||
|
||||
return $media->toResponse($request);
|
||||
}
|
||||
}
|
||||
59
app/Http/Controllers/FolderMentionController.php
Normal file
59
app/Http/Controllers/FolderMentionController.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreFolderMentionRequest;
|
||||
use App\Models\Folder;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Notifications\FolderMentionNotification;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class FolderMentionController extends Controller
|
||||
{
|
||||
protected function currentWorkspace(Request $request): Workspace
|
||||
{
|
||||
$workspaceId = $request->session()->get('current_workspace_id');
|
||||
|
||||
return Workspace::query()->findOrFail($workspaceId);
|
||||
}
|
||||
|
||||
protected function authorizeFolder(Workspace $workspace, Folder $folder): void
|
||||
{
|
||||
if ($folder->workspace_id !== $workspace->id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function store(StoreFolderMentionRequest $request, Folder $folder): RedirectResponse
|
||||
{
|
||||
$workspace = $this->currentWorkspace($request);
|
||||
$this->authorizeFolder($workspace, $folder);
|
||||
|
||||
$userRole = $workspace->users()
|
||||
->where('users.id', $request->user()->id)
|
||||
->first()
|
||||
?->pivot
|
||||
?->role
|
||||
?->value;
|
||||
|
||||
if (! in_array($userRole, ['owner', 'manager'])) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validated();
|
||||
$targetUser = User::findOrFail($validated['user_id']);
|
||||
|
||||
$targetUser->notify(new FolderMentionNotification(
|
||||
$folder,
|
||||
$request->user(),
|
||||
$validated['message'],
|
||||
));
|
||||
|
||||
Cache::forget("user:{$targetUser->id}:unread_notifications");
|
||||
|
||||
return back()->with('flash', ['type' => 'success', 'message' => 'Notification envoyée.']);
|
||||
}
|
||||
}
|
||||
149
app/Http/Controllers/FolderMessageController.php
Normal file
149
app/Http/Controllers/FolderMessageController.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
34
app/Http/Middleware/ValidateFolderInvitation.php
Normal file
34
app/Http/Middleware/ValidateFolderInvitation.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\FolderInvitation;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ValidateFolderInvitation
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$token = $request->route('token');
|
||||
|
||||
$invitation = FolderInvitation::query()
|
||||
->where('token', $token)
|
||||
->with(['folder.client', 'folder.assignee', 'folder.creator'])
|
||||
->first();
|
||||
|
||||
if (! $invitation || ! $invitation->isValid()) {
|
||||
abort(404, 'Lien invalide ou expiré.');
|
||||
}
|
||||
|
||||
$request->attributes->set('folder_invitation', $invitation);
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
37
app/Http/Requests/StoreFolderMentionRequest.php
Normal file
37
app/Http/Requests/StoreFolderMentionRequest.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoreFolderMentionRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$workspaceId = $this->session()->get('current_workspace_id');
|
||||
|
||||
return [
|
||||
'user_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('workspace_user', 'user_id')
|
||||
->where('workspace_id', $workspaceId),
|
||||
],
|
||||
'message' => ['required', 'string', 'max:500'],
|
||||
];
|
||||
}
|
||||
}
|
||||
54
app/Http/Requests/StoreFolderMessageRequest.php
Normal file
54
app/Http/Requests/StoreFolderMessageRequest.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\MessageType;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreFolderMessageRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = [
|
||||
'type' => ['required', new EnumValue(MessageType::class)],
|
||||
'body' => ['required', 'string', 'max:65535'],
|
||||
'files' => ['nullable', 'array'],
|
||||
'files.*' => ['file', 'max:10240'], // 10MB per file
|
||||
];
|
||||
|
||||
$type = $this->input('type');
|
||||
|
||||
if (in_array($type, ['situation', 'confirmation'])) {
|
||||
$rules['files'] = ['required', 'array', 'min:1'];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom attributes for validator errors.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function attributes(): array
|
||||
{
|
||||
return [
|
||||
'body' => 'message',
|
||||
'files' => 'fichiers',
|
||||
];
|
||||
}
|
||||
}
|
||||
111
app/Http/Requests/StoreFolderRequest.php
Normal file
111
app/Http/Requests/StoreFolderRequest.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\FolderPriority;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\FolderType;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class StoreFolderRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$merge = [];
|
||||
|
||||
if ($this->has('assigned_to') && $this->assigned_to === '') {
|
||||
$merge['assigned_to'] = null;
|
||||
}
|
||||
|
||||
if ($this->filled('period_month') && (int) $this->period_month === 0) {
|
||||
$merge['period_month'] = null;
|
||||
}
|
||||
if ($this->filled('period_quarter') && (int) $this->period_quarter === 0) {
|
||||
$merge['period_quarter'] = null;
|
||||
}
|
||||
|
||||
if ($merge !== []) {
|
||||
$this->merge($merge);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$workspaceId = $this->session()->get('current_workspace_id');
|
||||
|
||||
return [
|
||||
'client_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('clients', 'id')->where('workspace_id', $workspaceId),
|
||||
],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', new EnumValue(FolderType::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())],
|
||||
'assigned_to' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('users', 'id'),
|
||||
],
|
||||
'notes_internal' => ['nullable', 'string', 'max:65535'],
|
||||
'notes_client' => ['nullable', 'string', 'max:65535'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator instance.
|
||||
*/
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator) {
|
||||
$type = $this->input('type');
|
||||
|
||||
if ($type === 'vat') {
|
||||
$validator->errors()->add(
|
||||
'type',
|
||||
'Veuillez sélectionner TVA mensuelle ou TVA trimestrielle.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($type === 'vat_monthly') {
|
||||
$month = $this->input('period_month');
|
||||
if ($month === null || $month === '') {
|
||||
$validator->errors()->add('period_month', 'Le mois est requis pour la TVA mensuelle.');
|
||||
}
|
||||
$this->merge(['period_quarter' => null]);
|
||||
}
|
||||
|
||||
if ($type === 'vat_quarterly') {
|
||||
$quarter = $this->input('period_quarter');
|
||||
if ($quarter === null || $quarter === '') {
|
||||
$validator->errors()->add('period_quarter', 'Le trimestre est requis pour la TVA trimestrielle.');
|
||||
}
|
||||
$this->merge(['period_month' => null]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
111
app/Http/Requests/UpdateFolderRequest.php
Normal file
111
app/Http/Requests/UpdateFolderRequest.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\FolderPriority;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\FolderType;
|
||||
use BenSampo\Enum\Rules\EnumValue;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class UpdateFolderRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Prepare the data for validation.
|
||||
*/
|
||||
protected function prepareForValidation(): void
|
||||
{
|
||||
$merge = [];
|
||||
|
||||
if ($this->has('assigned_to') && $this->assigned_to === '') {
|
||||
$merge['assigned_to'] = null;
|
||||
}
|
||||
|
||||
if ($this->filled('period_month') && (int) $this->period_month === 0) {
|
||||
$merge['period_month'] = null;
|
||||
}
|
||||
if ($this->filled('period_quarter') && (int) $this->period_quarter === 0) {
|
||||
$merge['period_quarter'] = null;
|
||||
}
|
||||
|
||||
if ($merge !== []) {
|
||||
$this->merge($merge);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*/
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
$workspaceId = $this->session()->get('current_workspace_id');
|
||||
|
||||
return [
|
||||
'client_id' => [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('clients', 'id')->where('workspace_id', $workspaceId),
|
||||
],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', new EnumValue(FolderType::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())],
|
||||
'assigned_to' => [
|
||||
'nullable',
|
||||
'integer',
|
||||
Rule::exists('users', 'id'),
|
||||
],
|
||||
'notes_internal' => ['nullable', 'string', 'max:65535'],
|
||||
'notes_client' => ['nullable', 'string', 'max:65535'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the validator instance.
|
||||
*/
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator) {
|
||||
$type = $this->input('type');
|
||||
|
||||
if ($type === 'vat') {
|
||||
$validator->errors()->add(
|
||||
'type',
|
||||
'Veuillez sélectionner TVA mensuelle ou TVA trimestrielle.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($type === 'vat_monthly') {
|
||||
$month = $this->input('period_month');
|
||||
if ($month === null || $month === '') {
|
||||
$validator->errors()->add('period_month', 'Le mois est requis pour la TVA mensuelle.');
|
||||
}
|
||||
$this->merge(['period_quarter' => null]);
|
||||
}
|
||||
|
||||
if ($type === 'vat_quarterly') {
|
||||
$quarter = $this->input('period_quarter');
|
||||
if ($quarter === null || $quarter === '') {
|
||||
$validator->errors()->add('period_quarter', 'Le trimestre est requis pour la TVA trimestrielle.');
|
||||
}
|
||||
$this->merge(['period_month' => null]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
52
app/Mail/FolderConfirmationMail.php
Normal file
52
app/Mail/FolderConfirmationMail.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Folder;
|
||||
use App\Models\FolderInvitation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FolderConfirmationMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Folder $folder,
|
||||
public FolderInvitation $invitation,
|
||||
public string $body
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Demande de validation - '.$this->folder->title,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.folder-confirmation',
|
||||
with: [
|
||||
'folderTitle' => $this->folder->title,
|
||||
'body' => $this->body,
|
||||
'confirmUrl' => route('client.confirm', ['token' => $this->invitation->token]),
|
||||
'refuseUrl' => route('client.refuse', ['token' => $this->invitation->token]),
|
||||
'expiresAt' => $this->invitation->expires_at->format('d/m/Y à H:i'),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
51
app/Mail/FolderFileRequestMail.php
Normal file
51
app/Mail/FolderFileRequestMail.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Folder;
|
||||
use App\Models\FolderInvitation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FolderFileRequestMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Folder $folder,
|
||||
public FolderInvitation $invitation,
|
||||
public string $body
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Documents complémentaires demandés - '.$this->folder->title,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.folder-file-request',
|
||||
with: [
|
||||
'folderTitle' => $this->folder->title,
|
||||
'body' => $this->body,
|
||||
'uploadUrl' => route('client.upload', ['token' => $this->invitation->token]),
|
||||
'expiresAt' => $this->invitation->expires_at->format('d/m/Y à H:i'),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
49
app/Mail/FolderInviteMail.php
Normal file
49
app/Mail/FolderInviteMail.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Folder;
|
||||
use App\Models\FolderInvitation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FolderInviteMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Folder $folder,
|
||||
public FolderInvitation $invitation
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Dépôt de documents - '.$this->folder->title,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.folder-invite',
|
||||
with: [
|
||||
'folderTitle' => $this->folder->title,
|
||||
'uploadUrl' => route('client.upload', ['token' => $this->invitation->token]),
|
||||
'expiresAt' => $this->invitation->expires_at->format('d/m/Y à H:i'),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
51
app/Mail/FolderSituationMail.php
Normal file
51
app/Mail/FolderSituationMail.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Folder;
|
||||
use App\Models\FolderInvitation;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FolderSituationMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(
|
||||
public Folder $folder,
|
||||
public FolderInvitation $invitation,
|
||||
public string $body
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Situation mise à jour - '.$this->folder->title,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.folder-situation',
|
||||
with: [
|
||||
'folderTitle' => $this->folder->title,
|
||||
'body' => $this->body,
|
||||
'uploadUrl' => route('client.upload', ['token' => $this->invitation->token]),
|
||||
'expiresAt' => $this->invitation->expires_at->format('d/m/Y à H:i'),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
55
app/Mail/FolderTextMessageMail.php
Normal file
55
app/Mail/FolderTextMessageMail.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Folder;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class FolderTextMessageMail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @param string|null $token When set, recipient is client (use token-based URL). When null, recipient is comptable (use folders.show).
|
||||
*/
|
||||
public function __construct(
|
||||
public Folder $folder,
|
||||
public string $body,
|
||||
public ?string $token = null
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Nouveau message - '.$this->folder->title,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
$messagesUrl = $this->token
|
||||
? route('client.upload', ['token' => $this->token])
|
||||
: route('folders.show', ['folder' => $this->folder]).'?tab=messages';
|
||||
|
||||
return new Content(
|
||||
markdown: 'emails.folder-text-message',
|
||||
with: [
|
||||
'folderTitle' => $this->folder->title,
|
||||
'body' => $this->body,
|
||||
'messagesUrl' => $messagesUrl,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
149
app/Models/Folder.php
Normal file
149
app/Models/Folder.php
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\FolderPriority;
|
||||
use App\Enums\FolderStatus;
|
||||
use App\Enums\FolderType;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
|
||||
class Folder extends Model implements HasMedia
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\FolderFactory> */
|
||||
use HasFactory, InteractsWithMedia, LogsActivity, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'workspace_id',
|
||||
'client_id',
|
||||
'created_by',
|
||||
'title',
|
||||
'type',
|
||||
'period_year',
|
||||
'period_month',
|
||||
'period_quarter',
|
||||
'due_date',
|
||||
'status',
|
||||
'priority',
|
||||
'assigned_to',
|
||||
'validated_at',
|
||||
'closed_at',
|
||||
'confirmation_requested_at',
|
||||
'confirmation_media_id',
|
||||
'confirmed_by_type',
|
||||
'confirmed_by_id',
|
||||
'confirmation_signature',
|
||||
'refused_at',
|
||||
'refusal_reason',
|
||||
'notes_internal',
|
||||
'notes_client',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => FolderType::class,
|
||||
'status' => FolderStatus::class,
|
||||
'priority' => FolderPriority::class,
|
||||
'validated_at' => 'datetime',
|
||||
'closed_at' => 'datetime',
|
||||
'confirmation_requested_at' => 'datetime',
|
||||
'refused_at' => 'datetime',
|
||||
'due_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register media collections.
|
||||
*/
|
||||
public function registerMediaCollections(): void
|
||||
{
|
||||
$this->addMediaCollection('documents')->useDisk('local');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workspace that owns the folder.
|
||||
*
|
||||
* @return BelongsTo<Workspace, $this>
|
||||
*/
|
||||
public function workspace(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the client that owns the folder.
|
||||
*
|
||||
* @return BelongsTo<Client, $this>
|
||||
*/
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Client::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user who created the folder.
|
||||
*
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function creator(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user assigned to the folder.
|
||||
*
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function assignee(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'assigned_to');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the messages for the folder.
|
||||
*
|
||||
* @return HasMany<Message>
|
||||
*/
|
||||
public function messages(): HasMany
|
||||
{
|
||||
return $this->hasMany(Message::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the invitations for the folder.
|
||||
*
|
||||
* @return HasMany<FolderInvitation>
|
||||
*/
|
||||
public function invitations(): HasMany
|
||||
{
|
||||
return $this->hasMany(FolderInvitation::class);
|
||||
}
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logFillable()
|
||||
->logOnlyDirty()
|
||||
->dontSubmitEmptyLogs();
|
||||
}
|
||||
}
|
||||
72
app/Models/FolderInvitation.php
Normal file
72
app/Models/FolderInvitation.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class FolderInvitation extends Model
|
||||
{
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'folder_id',
|
||||
'token',
|
||||
'email',
|
||||
'expires_at',
|
||||
'used_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'expires_at' => 'datetime',
|
||||
'used_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot the model.
|
||||
*/
|
||||
protected static function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::creating(function (FolderInvitation $invitation) {
|
||||
if (empty($invitation->token)) {
|
||||
$invitation->token = Str::uuid()->toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the folder that owns the invitation.
|
||||
*
|
||||
* @return BelongsTo<Folder, $this>
|
||||
*/
|
||||
public function folder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Folder::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the invitation is valid (not expired, not used).
|
||||
*/
|
||||
public function isValid(): bool
|
||||
{
|
||||
if ($this->used_at !== null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->expires_at->isFuture();
|
||||
}
|
||||
}
|
||||
56
app/Notifications/FolderMentionNotification.php
Normal file
56
app/Notifications/FolderMentionNotification.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifications;
|
||||
|
||||
use App\Models\Folder;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class FolderMentionNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public function __construct(
|
||||
public Folder $folder,
|
||||
public User $mentionedBy,
|
||||
public string $message,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function via(object $notifiable): array
|
||||
{
|
||||
return ['database', 'mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toDatabase(object $notifiable): array
|
||||
{
|
||||
return [
|
||||
'folder_id' => $this->folder->id,
|
||||
'folder_title' => $this->folder->title,
|
||||
'mentioned_by_id' => $this->mentionedBy->id,
|
||||
'mentioned_by_name' => $this->mentionedBy->name,
|
||||
'message' => $this->message,
|
||||
'url' => route('folders.show', $this->folder),
|
||||
];
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('Vous avez été mentionné - '.$this->folder->title)
|
||||
->markdown('emails.folder-mention', [
|
||||
'folderTitle' => $this->folder->title,
|
||||
'mentionedByName' => $this->mentionedBy->name,
|
||||
'message' => $this->message,
|
||||
'url' => route('folders.show', $this->folder),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user