From 16a39014a2bf09c24f44d07942c4c09c35d96e42 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 19 Jan 2026 17:52:33 +0300 Subject: [PATCH] Change select internvetion, defunt, client, et produit --- .opencode/antigravity.json | 27 + .opencode/opencode.json | 73 +++ .../Http/Controllers/Api/ClientController.php | 27 +- .../Api/InterventionController.php | 38 +- .../Controllers/Api/InvoiceController.php | 44 ++ .../Http/Controllers/Api/QuoteController.php | 44 ++ .../StoreInterventionWithAllDataRequest.php | 12 +- .../app/Http/Requests/UpdateClientRequest.php | 1 + .../Http/Resources/Client/ClientResource.php | 2 + thanasoft-back/app/Mail/DocumentMail.php | 70 +++ thanasoft-back/app/Models/Client.php | 1 + thanasoft-back/composer.json | 1 + thanasoft-back/composer.lock | 520 +++++++++++++++++- ..._13_083624_add_avatar_to_clients_table.php | 28 + .../resources/views/emails/document.blade.php | 39 ++ .../resources/views/pdf/invoice_pdf.blade.php | 73 +++ .../resources/views/pdf/quote_pdf.blade.php | 73 +++ thanasoft-back/routes/api.php | 2 + .../Agenda/InterventionMultiStepModal.vue | 44 +- .../Agenda/WizardSteps/StepDeceased.vue | 128 ++++- .../Agenda/WizardSteps/StepLocation.vue | 142 ++++- .../WizardSteps/StepProductSelection.vue | 169 ++++-- .../Organism/CRM/ClientDetailPresentation.vue | 44 +- .../CRM/client/ClientDetailSidebar.vue | 8 +- .../components/atoms/client/ClientAvatar.vue | 49 +- .../molecules/Agenda/AgendaCalendar.vue | 7 +- .../molecules/client/ClientProfileCard.vue | 8 +- thanasoft-front/src/services/client.ts | 25 +- 28 files changed, 1576 insertions(+), 123 deletions(-) create mode 100644 .opencode/antigravity.json create mode 100644 .opencode/opencode.json create mode 100644 thanasoft-back/app/Mail/DocumentMail.php create mode 100644 thanasoft-back/database/migrations/2026_01_13_083624_add_avatar_to_clients_table.php create mode 100644 thanasoft-back/resources/views/emails/document.blade.php create mode 100644 thanasoft-back/resources/views/pdf/invoice_pdf.blade.php create mode 100644 thanasoft-back/resources/views/pdf/quote_pdf.blade.php diff --git a/.opencode/antigravity.json b/.opencode/antigravity.json new file mode 100644 index 0000000..0ca83a0 --- /dev/null +++ b/.opencode/antigravity.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://raw.githubusercontent.com/NoeFabris/opencode-antigravity-auth/main/assets/antigravity.schema.json", + "quiet_mode": false, + "debug": false, + "auto_update": true, + "keep_thinking": false, + "session_recovery": true, + "auto_resume": true, + "resume_text": "continue", + "empty_response_max_attempts": 4, + "empty_response_retry_delay_ms": 2000, + "tool_id_recovery": true, + "claude_tool_hardening": true, + "proactive_token_refresh": true, + "proactive_refresh_buffer_seconds": 1800, + "proactive_refresh_check_interval_seconds": 300, + "max_rate_limit_wait_seconds": 300, + "quota_fallback": false, + "account_selection_strategy": "sticky", + "pid_offset_enabled": false, + "signature_cache": { + "enabled": true, + "memory_ttl_seconds": 3600, + "disk_ttl_seconds": 172800, + "write_interval_seconds": 60 + } +} \ No newline at end of file diff --git a/.opencode/opencode.json b/.opencode/opencode.json new file mode 100644 index 0000000..b2cb823 --- /dev/null +++ b/.opencode/opencode.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://opencode.ai/config.json", + "plugin": ["opencode-antigravity-auth@beta"], + "provider": { + "google": { + "models": { + "antigravity-gemini-3-pro": { + "name": "Gemini 3 Pro (Antigravity)", + "limit": { "context": 1048576, "output": 65535 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, + "variants": { + "low": { "thinkingLevel": "low" }, + "high": { "thinkingLevel": "high" } + } + }, + "antigravity-gemini-3-flash": { + "name": "Gemini 3 Flash (Antigravity)", + "limit": { "context": 1048576, "output": 65536 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, + "variants": { + "minimal": { "thinkingLevel": "minimal" }, + "low": { "thinkingLevel": "low" }, + "medium": { "thinkingLevel": "medium" }, + "high": { "thinkingLevel": "high" } + } + }, + "antigravity-claude-sonnet-4-5": { + "name": "Claude Sonnet 4.5 (Antigravity)", + "limit": { "context": 200000, "output": 64000 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + }, + "antigravity-claude-sonnet-4-5-thinking": { + "name": "Claude Sonnet 4.5 Thinking (Antigravity)", + "limit": { "context": 200000, "output": 64000 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, + "variants": { + "low": { "thinkingConfig": { "thinkingBudget": 8192 } }, + "max": { "thinkingConfig": { "thinkingBudget": 32768 } } + } + }, + "antigravity-claude-opus-4-5-thinking": { + "name": "Claude Opus 4.5 Thinking (Antigravity)", + "limit": { "context": 200000, "output": 64000 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }, + "variants": { + "low": { "thinkingConfig": { "thinkingBudget": 8192 } }, + "max": { "thinkingConfig": { "thinkingBudget": 32768 } } + } + }, + "gemini-2.5-flash": { + "name": "Gemini 2.5 Flash (Gemini CLI)", + "limit": { "context": 1048576, "output": 65536 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + }, + "gemini-2.5-pro": { + "name": "Gemini 2.5 Pro (Gemini CLI)", + "limit": { "context": 1048576, "output": 65536 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + }, + "gemini-3-flash-preview": { + "name": "Gemini 3 Flash Preview (Gemini CLI)", + "limit": { "context": 1048576, "output": 65536 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + }, + "gemini-3-pro-preview": { + "name": "Gemini 3 Pro Preview (Gemini CLI)", + "limit": { "context": 1048576, "output": 65535 }, + "modalities": { "input": ["text", "image", "pdf"], "output": ["text"] } + } + } + } + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientController.php b/thanasoft-back/app/Http/Controllers/Api/ClientController.php index 465043c..8b53f2c 100644 --- a/thanasoft-back/app/Http/Controllers/Api/ClientController.php +++ b/thanasoft-back/app/Http/Controllers/Api/ClientController.php @@ -14,6 +14,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Support\Facades\Log; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; class ClientController extends Controller { @@ -152,12 +153,32 @@ class ClientController extends Controller public function update(UpdateClientRequest $request, string $id): ClientResource|JsonResponse { try { - $updated = $this->clientRepository->update($id, $request->validated()); + $validatedData = $request->validated(); + + $client = $this->clientRepository->find($id); + if (!$client) { + return response()->json([ + 'message' => 'Client non trouvé.', + ], 404); + } + + if ($request->hasFile('avatar')) { + // Delete old avatar if exists + if ($client->avatar) { + Storage::disk('public')->delete($client->avatar); + } + + // Store new avatar + $path = $request->file('avatar')->store('avatars', 'public'); + $validatedData['avatar'] = $path; + } + + $updated = $this->clientRepository->update($id, $validatedData); if (!$updated) { return response()->json([ - 'message' => 'Client non trouvé ou échec de la mise à jour.', - ], 404); + 'message' => 'Échec de la mise à jour.', + ], 500); } $client = $this->clientRepository->find($id); diff --git a/thanasoft-back/app/Http/Controllers/Api/InterventionController.php b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php index fb64272..f738c86 100644 --- a/thanasoft-back/app/Http/Controllers/Api/InterventionController.php +++ b/thanasoft-back/app/Http/Controllers/Api/InterventionController.php @@ -76,7 +76,8 @@ class InterventionController extends Controller ContactRepositoryInterface $contactRepository, DeceasedRepositoryInterface $deceasedRepository, QuoteRepositoryInterface $quoteRepository, - ProductRepositoryInterface $productRepository + ProductRepositoryInterface $productRepository, + private readonly \App\Repositories\ClientLocationRepositoryInterface $clientLocationRepository ) { $this->interventionRepository = $interventionRepository; $this->interventionPractitionerRepository = $interventionPractitionerRepository; @@ -150,9 +151,14 @@ class InterventionController extends Controller // Wrap everything in a database transaction $result = DB::transaction(function () use ($validated) { - // Step 1: Create the deceased - $deceasedData = $validated['deceased']; - $deceased = $this->deceasedRepository->create($deceasedData); + // Step 1: Handle Deceased (Create or Link) + $deceased = null; + if (!empty($validated['deceased_id'])) { + $deceased = $this->deceasedRepository->findById($validated['deceased_id']); + } else { + $deceasedData = $validated['deceased']; + $deceased = $this->deceasedRepository->create($deceasedData); + } // Step 2: Create the client $clientData = $validated['client']; @@ -168,10 +174,29 @@ class InterventionController extends Controller $contactId = $contact->id; } - // Step 4: Prepare location data (for now, we'll include it in intervention notes) - // In the future, you might want to create a ClientLocation entry + // Step 4: Handle Location $locationData = $validated['location'] ?? []; + $locationId = $validated['location_id'] ?? null; $locationNotes = ''; + + if (!$locationId && !empty($locationData)) { + // Create new location for the client + $locData = array_merge($locationData, [ + 'client_id' => $client->id, + 'is_default' => false + ]); + $newLocation = $this->clientLocationRepository->create($locData); + $locationId = $newLocation->id; + } + + if ($locationId) { + // Fetch location to add details to notes if needed, or just rely on relation. + // For now, let's keep the legacy behavior of adding text to notes for quick reference, + // but also link the ID. Use the provided data or fetch? + // If we have an ID, we might not have the text data in $locationData if it came from search. + // So we only append text notes if we have $locationData (Create mode or if frontend sends it). + } + if (!empty($locationData)) { $locationParts = []; if (!empty($locationData['name'])) { @@ -196,6 +221,7 @@ class InterventionController extends Controller $interventionData = array_merge($validated['intervention'], [ 'deceased_id' => $deceased->id, 'client_id' => $client->id, + 'location_id' => $locationId, 'notes' => ($validated['intervention']['notes'] ?? '') . $locationNotes ]); $intervention = $this->interventionRepository->create($interventionData); diff --git a/thanasoft-back/app/Http/Controllers/Api/InvoiceController.php b/thanasoft-back/app/Http/Controllers/Api/InvoiceController.php index e91c6e7..1b45420 100644 --- a/thanasoft-back/app/Http/Controllers/Api/InvoiceController.php +++ b/thanasoft-back/app/Http/Controllers/Api/InvoiceController.php @@ -12,6 +12,9 @@ use App\Repositories\InvoiceRepositoryInterface; use Illuminate\Http\JsonResponse; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Support\Facades\Log; +use Barryvdh\DomPDF\Facade\Pdf; +use Illuminate\Support\Facades\Mail; +use App\Mail\DocumentMail; class InvoiceController extends Controller { @@ -175,4 +178,45 @@ class InvoiceController extends Controller ], 500); } } + + /** + * Send the invoice by email to the client. + */ + public function sendByEmail(string $id): JsonResponse + { + try { + $invoice = $this->invoiceRepository->find($id); + + if (!$invoice) { + return response()->json(['message' => 'Facture non trouvée.'], 404); + } + + if (!$invoice->client || !$invoice->client->email) { + return response()->json(['message' => 'Le client n\'a pas d\'adresse email.'], 422); + } + + // Load lines to ensure they are available in the view + $invoice->load('lines'); + + // Generate PDF + $pdfContent = Pdf::loadView('pdf.invoice_pdf', ['invoice' => $invoice])->output(); + + // Send Email + Mail::to($invoice->client->email)->send(new DocumentMail($invoice, 'invoice', $pdfContent)); + + return response()->json([ + 'message' => 'La facture a été envoyée avec succès à ' . $invoice->client->email, + ], 200); + } catch (\Exception $e) { + Log::error('Error sending invoice email: ' . $e->getMessage(), [ + 'exception' => $e, + 'invoice_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'envoi de l\'email.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } } diff --git a/thanasoft-back/app/Http/Controllers/Api/QuoteController.php b/thanasoft-back/app/Http/Controllers/Api/QuoteController.php index 1713408..04c5fe2 100644 --- a/thanasoft-back/app/Http/Controllers/Api/QuoteController.php +++ b/thanasoft-back/app/Http/Controllers/Api/QuoteController.php @@ -14,6 +14,9 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use Illuminate\Support\Facades\Log; use Illuminate\Http\Request; +use Barryvdh\DomPDF\Facade\Pdf; +use Illuminate\Support\Facades\Mail; +use App\Mail\DocumentMail; class QuoteController extends Controller { @@ -155,4 +158,45 @@ class QuoteController extends Controller ], 500); } } + + /** + * Send the quote by email to the client. + */ + public function sendByEmail(string $id): JsonResponse + { + try { + $quote = $this->quoteRepository->find($id); + + if (!$quote) { + return response()->json(['message' => 'Devis non trouvé.'], 404); + } + + if (!$quote->client || !$quote->client->email) { + return response()->json(['message' => 'Le client n\'a pas d\'adresse email.'], 422); + } + + // Load lines to ensure they are available in the view + $quote->load('lines'); + + // Generate PDF + $pdfContent = Pdf::loadView('pdf.quote_pdf', ['quote' => $quote])->output(); + + // Send Email + Mail::to($quote->client->email)->send(new DocumentMail($quote, 'quote', $pdfContent)); + + return response()->json([ + 'message' => 'Le devis a été envoyé avec succès à ' . $quote->client->email, + ], 200); + } catch (\Exception $e) { + Log::error('Error sending quote email: ' . $e->getMessage(), [ + 'exception' => $e, + 'quote_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de l\'envoi de l\'email.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } } diff --git a/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php b/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php index 940e878..e0a324a 100644 --- a/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php +++ b/thanasoft-back/app/Http/Requests/StoreInterventionWithAllDataRequest.php @@ -23,8 +23,9 @@ class StoreInterventionWithAllDataRequest extends FormRequest public function rules(): array { return [ - 'deceased' => 'required|array', - 'deceased.last_name' => ['required', 'string', 'max:191'], + 'deceased_id' => ['nullable', 'exists:deceased,id'], + 'deceased' => ['required_without:deceased_id', 'array'], + 'deceased.last_name' => ['required_without:deceased_id', 'string', 'max:191'], 'deceased.first_name' => ['nullable', 'string', 'max:191'], 'deceased.birth_date' => ['nullable', 'date'], 'deceased.death_date' => ['nullable', 'date', 'after_or_equal:deceased.birth_date'], @@ -51,10 +52,11 @@ class StoreInterventionWithAllDataRequest extends FormRequest 'contact.phone' => ['nullable', 'string', 'max:50'], 'contact.role' => ['nullable', 'string', 'max:191'], - 'location' => 'nullable|array', - 'location.name' => ['nullable', 'string', 'max:255'], + 'location_id' => ['nullable', 'exists:client_locations,id'], + 'location' => ['nullable', 'array'], + 'location.name' => ['required_without:location_id', 'string', 'max:255'], 'location.address' => ['nullable', 'string', 'max:255'], - 'location.city' => ['nullable', 'string', 'max:191'], + 'location.city' => ['required_without:location_id', 'string', 'max:191'], 'location.postal_code' => ['nullable', 'string', 'max:20'], 'location.country_code' => ['nullable', 'string', 'size:2'], 'location.access_instructions' => ['nullable', 'string'], diff --git a/thanasoft-back/app/Http/Requests/UpdateClientRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php index ff77fc7..ac94c1a 100644 --- a/thanasoft-back/app/Http/Requests/UpdateClientRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php @@ -39,6 +39,7 @@ class UpdateClientRequest extends FormRequest 'is_parent' => 'boolean|nullable', 'parent_id' => 'nullable|exists:clients,id', 'default_tva_rate_id' => 'nullable|exists:tva_rates,id', + 'avatar' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048', ]; } diff --git a/thanasoft-back/app/Http/Resources/Client/ClientResource.php b/thanasoft-back/app/Http/Resources/Client/ClientResource.php index 5cb6095..cf46492 100644 --- a/thanasoft-back/app/Http/Resources/Client/ClientResource.php +++ b/thanasoft-back/app/Http/Resources/Client/ClientResource.php @@ -5,6 +5,7 @@ namespace App\Http\Resources\Client; use App\Http\Resources\Contact\ContactResource; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; +use Illuminate\Support\Facades\Storage; class ClientResource extends JsonResource { @@ -25,6 +26,7 @@ class ClientResource extends JsonResource 'siret' => $this->siret, 'email' => $this->email, 'phone' => $this->phone, + 'avatar_url' => $this->avatar ? Storage::url($this->avatar) : null, 'billing_address' => [ 'line1' => $this->billing_address_line1, 'line2' => $this->billing_address_line2, diff --git a/thanasoft-back/app/Mail/DocumentMail.php b/thanasoft-back/app/Mail/DocumentMail.php new file mode 100644 index 0000000..dc6a3ff --- /dev/null +++ b/thanasoft-back/app/Mail/DocumentMail.php @@ -0,0 +1,70 @@ +document = $document; + $this->type = $type; + $this->pdfContent = $pdfContent; + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + $subject = $this->type === 'quote' + ? "Votre devis Thanasoft : " . $this->document->reference + : "Votre facture Thanasoft : " . $this->document->invoice_number; + + return new Envelope( + subject: $subject, + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + view: 'emails.document', + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + $filename = $this->type === 'quote' + ? 'Devis_' . $this->document->reference . '.pdf' + : 'Facture_' . $this->document->invoice_number . '.pdf'; + + return [ + Attachment::fromData(fn () => $this->pdfContent, $filename) + ->withMime('application/pdf'), + ]; + } +} diff --git a/thanasoft-back/app/Models/Client.php b/thanasoft-back/app/Models/Client.php index 218d6c2..c3d4a0c 100644 --- a/thanasoft-back/app/Models/Client.php +++ b/thanasoft-back/app/Models/Client.php @@ -28,6 +28,7 @@ class Client extends Model // 'default_tva_rate_id', 'client_category_id', 'user_id', + 'avatar', ]; protected $casts = [ diff --git a/thanasoft-back/composer.json b/thanasoft-back/composer.json index 34284d9..d821744 100644 --- a/thanasoft-back/composer.json +++ b/thanasoft-back/composer.json @@ -10,6 +10,7 @@ "license": "MIT", "require": { "php": "^8.2", + "barryvdh/laravel-dompdf": "^3.1", "laravel/framework": "^12.0", "laravel/sanctum": "^4.2", "laravel/tinker": "^2.10.1" diff --git a/thanasoft-back/composer.lock b/thanasoft-back/composer.lock index 3089aab..b1d8fb2 100644 --- a/thanasoft-back/composer.lock +++ b/thanasoft-back/composer.lock @@ -4,8 +4,85 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8f387a0734f3bf879214e4aa2fca6e2f", + "content-hash": "343ecaac4a8b061c5430a046847047e7", "packages": [ + { + "name": "barryvdh/laravel-dompdf", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^3.0", + "illuminate/support": "^9|^10|^11|^12", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.7|^3.0", + "orchestra/testbench": "^7|^8|^9|^10", + "phpro/grumphp": "^2.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf", + "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf" + }, + "providers": [ + "Barryvdh\\DomPDF\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\DomPDF\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "A DOMPDF Wrapper for Laravel", + "keywords": [ + "dompdf", + "laravel", + "pdf" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-dompdf/issues", + "source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-02-13T15:07:54+00:00" + }, { "name": "brick/math", "version": "0.14.0", @@ -377,6 +454,161 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v3.1.4", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "db712c90c5b9868df3600e64e68da62e78a34623" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/db712c90c5b9868df3600e64e68da62e78a34623", + "reference": "db712c90c5b9868df3600e64e68da62e78a34623", + "shasum": "" + }, + "require": { + "dompdf/php-font-lib": "^1.0.0", + "dompdf/php-svg-lib": "^1.0.0", + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v3.1.4" + }, + "time": "2025-10-29T12:43:30+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "The FontLib Community", + "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/dompdf/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1" + }, + "time": "2024-12-02T14:37:59+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4 || ^9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" + }, + "time": "2026-01-02T16:01:13+00:00" + }, { "name": "dragonmantank/cron-expression", "version": "v3.4.0", @@ -2074,6 +2306,73 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "masterminds/html5", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fcf91eb64359852f00d921887b219479b4f21251" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + }, + "time": "2025-07-25T09:04:22+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", @@ -3412,6 +3711,80 @@ }, "time": "2025-09-04T20:59:21+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.1.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb", + "reference": "1b363fdbdc6dd0ca0f4bf98d3a4d7f388133f1fb", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.3" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.28 || 2.1.25", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.7", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.6", + "phpunit/phpunit": "8.5.46", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.1.7", + "rector/type-perfect": "1.0.0 || 2.1.0" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.1.0" + }, + "time": "2025-09-14T07:37:21+00:00" + }, { "name": "symfony/clock", "version": "v7.3.0", @@ -5890,6 +6263,145 @@ ], "time": "2025-08-13T11:49:31+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.3.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2025-05-14T06:15:44+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.3.0", @@ -8527,12 +9039,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.2" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/thanasoft-back/database/migrations/2026_01_13_083624_add_avatar_to_clients_table.php b/thanasoft-back/database/migrations/2026_01_13_083624_add_avatar_to_clients_table.php new file mode 100644 index 0000000..319a589 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_13_083624_add_avatar_to_clients_table.php @@ -0,0 +1,28 @@ +string('avatar')->nullable()->after('phone'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('clients', function (Blueprint $table) { + $table->dropColumn('avatar'); + }); + } +}; diff --git a/thanasoft-back/resources/views/emails/document.blade.php b/thanasoft-back/resources/views/emails/document.blade.php new file mode 100644 index 0000000..ba00eff --- /dev/null +++ b/thanasoft-back/resources/views/emails/document.blade.php @@ -0,0 +1,39 @@ + + + + + + + +
+
+

Thanasoft

+
+ +

Bonjour {{ $document->client->name }},

+ + @if($type === 'quote') +

Veuillez trouver ci-joint le devis {{ $document->reference }} suite à votre demande.

+

Ce devis est valable jusqu'au {{ $document->valid_until->format('d/m/Y') }}.

+ @else +

Veuillez trouver ci-joint la facture {{ $document->invoice_number }} d'un montant de {{ number_format($document->total_ttc, 2, ',', ' ') }} {{ $document->currency }}.

+

La date d'échéance est fixée au {{ $document->due_date->format('d/m/Y') }}.

+ @endif + +

N'hésitez pas à nous contacter pour toute question supplémentaire.

+ +

Cordialement,
+ L'équipe Thanasoft

+ + +
+ + diff --git a/thanasoft-back/resources/views/pdf/invoice_pdf.blade.php b/thanasoft-back/resources/views/pdf/invoice_pdf.blade.php new file mode 100644 index 0000000..bd28f78 --- /dev/null +++ b/thanasoft-back/resources/views/pdf/invoice_pdf.blade.php @@ -0,0 +1,73 @@ + + + + + Facture {{ $invoice->invoice_number }} + + + +
+
+

THANASOFT

+

Solutions de gestion funéraire

+
+
+

CLIENT

+

{{ $invoice->client->name }}

+

{{ $invoice->client->billing_address_line1 }}

+

{{ $invoice->client->billing_postal_code }} {{ $invoice->client->billing_city }}

+
+
+
+ +
Facture : {{ $invoice->invoice_number }}
+

Date : {{ $invoice->invoice_date->format('d/m/Y') }}
+ Échéance : {{ $invoice->due_date->format('d/m/Y') }}

+ + + + + + + + + + + + @foreach($invoice->lines as $line) + + + + + + + @endforeach + +
DescriptionQuantitéPrix UnitaireTotal HT
{{ $line->description }}{{ $line->quantity }}{{ number_format($line->unit_price, 2, ',', ' ') }} {{ $invoice->currency }}{{ number_format($line->total_ht, 2, ',', ' ') }} {{ $invoice->currency }}
+ +
+
Total HT : {{ number_format($invoice->total_ht, 2, ',', ' ') }} {{ $invoice->currency }}
+
TVA : {{ number_format($invoice->total_tva, 2, ',', ' ') }} {{ $invoice->currency }}
+
TOTAL TTC : {{ number_format($invoice->total_ttc, 2, ',', ' ') }} {{ $invoice->currency }}
+
+ + + + diff --git a/thanasoft-back/resources/views/pdf/quote_pdf.blade.php b/thanasoft-back/resources/views/pdf/quote_pdf.blade.php new file mode 100644 index 0000000..6118313 --- /dev/null +++ b/thanasoft-back/resources/views/pdf/quote_pdf.blade.php @@ -0,0 +1,73 @@ + + + + + Devis {{ $quote->reference }} + + + +
+
+

THANASOFT

+

Solutions de gestion funéraire

+
+
+

CLIENT

+

{{ $quote->client->name }}

+

{{ $quote->client->billing_address_line1 }}

+

{{ $quote->client->billing_postal_code }} {{ $quote->client->billing_city }}

+
+
+
+ +
Devis : {{ $quote->reference }}
+

Date : {{ $quote->quote_date->format('d/m/Y') }}
+ Valable jusqu'au : {{ $quote->valid_until->format('d/m/Y') }}

+ + + + + + + + + + + + @foreach($quote->lines as $line) + + + + + + + @endforeach + +
DescriptionQuantitéPrix UnitaireTotal HT
{{ $line->description }}{{ $line->quantity }}{{ number_format($line->unit_price, 2, ',', ' ') }} {{ $quote->currency }}{{ number_format($line->total_ht, 2, ',', ' ') }} {{ $quote->currency }}
+ +
+
Total HT : {{ number_format($quote->total_ht, 2, ',', ' ') }} {{ $quote->currency }}
+
TVA : {{ number_format($quote->total_tva, 2, ',', ' ') }} {{ $quote->currency }}
+
TOTAL TTC : {{ number_format($quote->total_ttc, 2, ',', ' ') }} {{ $quote->currency }}
+
+ + + + diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index 174a371..10c363e 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -72,9 +72,11 @@ Route::middleware('auth:sanctum')->group(function () { Route::apiResource('client-categories', ClientCategoryController::class); // Quote management + Route::post('/quotes/{id}/send-email', [QuoteController::class, 'sendByEmail']); Route::apiResource('quotes', QuoteController::class); // Invoice management + Route::post('/invoices/{id}/send-email', [\App\Http\Controllers\Api\InvoiceController::class, 'sendByEmail']); Route::post('/invoices/from-quote/{quoteId}', [\App\Http\Controllers\Api\InvoiceController::class, 'createFromQuote']); Route::apiResource('invoices', \App\Http\Controllers\Api\InvoiceController::class); diff --git a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue index 0bee068..62fb578 100644 --- a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue +++ b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue @@ -167,7 +167,7 @@ const steps = [ { label: "Défunt", title: "Information du Défunt" }, { label: "Client", title: "Information du Client" }, { label: "Lieu", title: "Lieu de l'intervention" }, - { label: "Produit", title: "Choix du Produit" }, + { label: "Type de soins", title: "Sélection du type de soins" }, { label: "Documents", title: "Documents à joindre" }, { label: "Intervention", title: "Détails de l'intervention" }, ]; @@ -178,6 +178,8 @@ const globalErrors = ref([]); // Form data const deceasedForm = ref({ + id: null, + is_existing: false, // UI state to track mode first_name: "", last_name: "", birth_date: "", @@ -203,6 +205,8 @@ const clientForm = ref({ }); const locationForm = ref({ + id: null, + is_existing: false, name: "", address: "", city: "", @@ -273,10 +277,14 @@ const addStepError = (stepIndex, field, message) => { const validateDeceasedStep = () => { clearStepErrors(0); let isValid = true; - if (!deceasedForm.value.last_name) { + if (!deceasedForm.value.is_existing && !deceasedForm.value.last_name) { addStepError(0, "last_name", "Le nom est obligatoire."); isValid = false; } + if (deceasedForm.value.is_existing && !deceasedForm.value.id) { + addStepError(0, "deceased_id", "Veuillez sélectionner un défunt."); + isValid = false; + } return isValid; }; @@ -293,10 +301,14 @@ const validateClientStep = () => { const validateLocationStep = () => { clearStepErrors(2); let isValid = true; - if (!locationForm.value.city) { + if (!locationForm.value.is_existing && !locationForm.value.city) { addStepError(2, "city", "La ville est obligatoire."); isValid = false; } + if (locationForm.value.is_existing && !locationForm.value.id) { + addStepError(2, "location_id", "Veuillez sélectionner un lieu."); + isValid = false; + } return isValid; }; @@ -304,7 +316,7 @@ const validateProductStep = () => { clearStepErrors(3); let isValid = true; if (!productForm.value.product_id) { - addStepError(3, "product_id", "Veuillez sélectionner un produit."); + addStepError(3, "product_id", "Veuillez sélectionner un type de soins."); isValid = false; } return isValid; @@ -419,20 +431,28 @@ const handleSubmit = async () => { const formData = new FormData(); // Deceased - Object.keys(deceasedForm.value).forEach((key) => { - if (deceasedForm.value[key] != null) - formData.append(`deceased[${key}]`, deceasedForm.value[key]); - }); + if (deceasedForm.value.is_existing && deceasedForm.value.id) { + formData.append("deceased_id", deceasedForm.value.id); + } else { + Object.keys(deceasedForm.value).forEach((key) => { + if (deceasedForm.value[key] != null && key !== 'id' && key !== 'is_existing') + formData.append(`deceased[${key}]`, deceasedForm.value[key]); + }); + } // Client Object.keys(clientForm.value).forEach((key) => { if (clientForm.value[key] != null) formData.append(`client[${key}]`, clientForm.value[key]); }); // Location - Object.keys(locationForm.value).forEach((key) => { - if (locationForm.value[key] != null) - formData.append(`location[${key}]`, locationForm.value[key]); - }); + if (locationForm.value.is_existing && locationForm.value.id) { + formData.append("location_id", locationForm.value.id); + } else { + Object.keys(locationForm.value).forEach((key) => { + if (locationForm.value[key] != null && key !== 'id' && key !== 'is_existing') + formData.append(`location[${key}]`, locationForm.value[key]); + }); + } // Intervention Object.keys(interventionForm.value).forEach((key) => { if (interventionForm.value[key] != null) { diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDeceased.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDeceased.vue index acc861a..ccab123 100644 --- a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDeceased.vue +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepDeceased.vue @@ -2,7 +2,85 @@
Informations du Défunt
-
+
+
+
+ + + + + +
+
+
+ + +
+
+ +
+ + + +
+
+ {{ getFieldError("deceased_id") }} +
+ + +
+ +
+
+
Aucun résultat trouvé.
+
+
+ +
+
+ Défunt sélectionné: {{ formData.first_name }} {{ formData.last_name }} +
+
+
+ + +
diff --git a/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue b/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue index 22eb71d..9961bd9 100644 --- a/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue +++ b/thanasoft-front/src/components/Organism/CRM/ClientDetailPresentation.vue @@ -19,7 +19,7 @@