From 50f79a8040a2a840019ff1f5d5b79ff6d31005ea Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 7 Jan 2026 18:17:11 +0300 Subject: [PATCH] feat: Introduce client group management and enhance quote creation and detail views. --- .../app/Http/Requests/UpdateQuoteRequest.php | 4 +- .../Http/Resources/ClientGroupResource.php | 25 + .../DocumentStatusHistoryResource.php | 26 ++ .../app/Http/Resources/QuoteResource.php | 2 + .../app/Models/DocumentStatusHistory.php | 44 ++ thanasoft-back/app/Models/Quote.php | 12 + .../app/Repositories/QuoteRepository.php | 60 ++- ...8_create_document_status_history_table.php | 36 ++ .../Agenda/InterventionMultiStepModal.vue | 6 +- .../Agenda/WizardSteps/StepClient.vue | 7 +- .../Agenda/WizardSteps/StepDeceased.vue | 2 +- .../Agenda/WizardSteps/StepDocuments.vue | 25 +- .../Agenda/WizardSteps/StepIntervention.vue | 9 +- .../Agenda/WizardSteps/StepLocation.vue | 2 +- .../WizardSteps/StepProductSelection.vue | 42 +- .../ClientGroupDetailPresentation.vue | 112 +++++ .../ClientGroupFormPresentation.vue | 96 ++++ .../ClientGroupListPresentation.vue | 68 +++ .../InterventionPresentation.vue | 2 +- .../intervention/AddPractitionerModal.vue | 16 +- .../InterventionDetailContent.vue | 6 +- .../Organism/Location/LocationManager.vue | 14 +- .../ProductCategories/ProductCategoryList.vue | 19 +- .../ProductCategoryModal.vue | 42 +- .../Product/ProductDetailsSection.vue | 2 +- .../Quote/QuoteCreationPresentation.vue | 271 ++++++----- .../Quote/QuoteDetailPresentation.vue | 222 ++++++--- .../Organism/Quote/QuoteListPresentation.vue | 61 ++- .../Organism/Stock/AddProductPresentation.vue | 20 +- .../Stock/ProductDetailsPresentation.vue | 6 +- .../atoms/Agenda/AddInterventionButton.vue | 2 +- .../atoms/Location/LocationDisplay.vue | 4 +- .../atoms/Location/LocationSearchInput.vue | 8 +- .../components/atoms/input/ModalSearch.vue | 2 +- .../molecules/ClientGroup/ClientGroupForm.vue | 94 ++++ .../ClientGroup/ClientGroupListControls.vue | 30 ++ .../molecules/Defunts/DefuntDetailContent.vue | 12 +- .../molecules/Defunts/DefuntsList.vue | 4 +- .../Interventions/DocumentManagement.vue | 24 +- .../Interventions/interventionDetails.vue | 20 +- .../molecules/Location/LocationSearchForm.vue | 4 +- .../molecules/Product/SupplierInfo.vue | 2 +- .../molecules/Quote/ProductLineItem.vue | 232 ++++----- .../molecules/Quote/QuoteBillingInfo.vue | 55 +++ .../molecules/Quote/QuoteHeader.vue | 43 ++ .../molecules/Quote/QuoteInfoCard.vue | 102 ++-- .../molecules/Quote/QuoteLinesTable.vue | 85 +++- .../molecules/Quote/QuoteListControls.vue | 95 ++++ .../molecules/Quote/QuoteSummary.vue | 40 ++ .../molecules/Quote/QuoteTimeline.vue | 110 +++++ .../molecules/Quote/QuoteTotalsCard.vue | 49 +- .../molecules/Stock/ProductCategoryModal.vue | 2 +- .../Tables/ClientGroup/ClientGroupTable.vue | 159 +++++++ .../molecules/Tables/Ventes/QuoteTable.vue | 440 +++++++----------- .../employee/EmployeeDocumentsTab.vue | 14 +- .../molecules/employee/EmployeeInfoTab.vue | 4 +- .../molecules/employee/EmployeeOverview.vue | 2 +- .../intervention/AssignPractitionerModal.vue | 2 +- .../PractitionerSearchInput.vue | 20 +- .../templates/Quote/CreateQuoteTemplate.vue | 35 +- .../templates/Quote/QuoteDetailTemplate.vue | 60 +-- .../src/examples/BotMessageConfigurator.vue | 2 +- .../src/examples/Sidenav/SidenavList.vue | 6 + thanasoft-front/src/router/index.js | 24 +- thanasoft-front/src/services/clientGroup.ts | 107 +++++ thanasoft-front/src/services/quote.ts | 4 +- .../src/stores/clientGroupStore.ts | 221 +++++++++ .../src/stores/productCategoryStore.ts | 28 +- .../views/pages/Clients/ClientGroupDetail.vue | 12 + .../src/views/pages/Clients/ClientGroups.vue | 7 + .../views/pages/Clients/NewClientGroup.vue | 12 + .../pages/Parametrage/ProductCategories.vue | 26 +- .../pages/Stock/ProductCategoryDetails.vue | 2 +- .../src/views/pages/Ventes/Devis.vue | 2 +- .../src/views/pages/Ventes/NewQuote.vue | 2 +- .../src/views/pages/Ventes/QuoteDetail.vue | 6 +- 76 files changed, 2544 insertions(+), 931 deletions(-) create mode 100644 thanasoft-back/app/Http/Resources/ClientGroupResource.php create mode 100644 thanasoft-back/app/Http/Resources/DocumentStatusHistoryResource.php create mode 100644 thanasoft-back/app/Models/DocumentStatusHistory.php create mode 100644 thanasoft-back/database/migrations/2026_01_07_081428_create_document_status_history_table.php create mode 100644 thanasoft-front/src/components/Organism/ClientGroup/ClientGroupDetailPresentation.vue create mode 100644 thanasoft-front/src/components/Organism/ClientGroup/ClientGroupFormPresentation.vue create mode 100644 thanasoft-front/src/components/Organism/ClientGroup/ClientGroupListPresentation.vue create mode 100644 thanasoft-front/src/components/molecules/ClientGroup/ClientGroupForm.vue create mode 100644 thanasoft-front/src/components/molecules/ClientGroup/ClientGroupListControls.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/QuoteBillingInfo.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/QuoteHeader.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/QuoteListControls.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/QuoteSummary.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/QuoteTimeline.vue create mode 100644 thanasoft-front/src/components/molecules/Tables/ClientGroup/ClientGroupTable.vue create mode 100644 thanasoft-front/src/services/clientGroup.ts create mode 100644 thanasoft-front/src/stores/clientGroupStore.ts create mode 100644 thanasoft-front/src/views/pages/Clients/ClientGroupDetail.vue create mode 100644 thanasoft-front/src/views/pages/Clients/ClientGroups.vue create mode 100644 thanasoft-front/src/views/pages/Clients/NewClientGroup.vue diff --git a/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php index a99361b..6fdd10c 100644 --- a/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php @@ -21,10 +21,12 @@ class UpdateQuoteRequest extends FormRequest */ public function rules(): array { + $quoteId = $this->route('quote'); + return [ 'client_id' => 'sometimes|exists:clients,id', 'group_id' => 'nullable|exists:client_groups,id', - 'reference' => 'sometimes|string|max:191|unique:quotes,reference,' . $this->quote->id, + 'reference' => 'sometimes|string|max:191|unique:quotes,reference,' . $quoteId, 'status' => 'sometimes|in:brouillon,envoye,accepte,refuse,expire,annule', 'quote_date' => 'sometimes|date', 'valid_until' => 'nullable|date|after_or_equal:quote_date', diff --git a/thanasoft-back/app/Http/Resources/ClientGroupResource.php b/thanasoft-back/app/Http/Resources/ClientGroupResource.php new file mode 100644 index 0000000..0eb97cc --- /dev/null +++ b/thanasoft-back/app/Http/Resources/ClientGroupResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/DocumentStatusHistoryResource.php b/thanasoft-back/app/Http/Resources/DocumentStatusHistoryResource.php new file mode 100644 index 0000000..d7a8d35 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/DocumentStatusHistoryResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'old_status' => $this->old_status, + 'new_status' => $this->new_status, + 'changed_at' => $this->changed_at, + 'changed_by' => $this->user ? $this->user->name : 'System', // Simple user display + 'comment' => $this->comment, + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/QuoteResource.php b/thanasoft-back/app/Http/Resources/QuoteResource.php index a31ab24..40202b2 100644 --- a/thanasoft-back/app/Http/Resources/QuoteResource.php +++ b/thanasoft-back/app/Http/Resources/QuoteResource.php @@ -30,6 +30,8 @@ class QuoteResource extends JsonResource 'updated_at' => $this->updated_at, 'client' => $this->whenLoaded('client'), 'group' => $this->whenLoaded('group'), + 'lines' => QuoteLineResource::collection($this->whenLoaded('lines')), + 'history' => DocumentStatusHistoryResource::collection($this->whenLoaded('history')), ]; } } diff --git a/thanasoft-back/app/Models/DocumentStatusHistory.php b/thanasoft-back/app/Models/DocumentStatusHistory.php new file mode 100644 index 0000000..d11eb35 --- /dev/null +++ b/thanasoft-back/app/Models/DocumentStatusHistory.php @@ -0,0 +1,44 @@ + 'datetime', + ]; + + public function user() + { + return $this->belongsTo(User::class, 'changed_by'); + } + + /** + * Get the parent document model (quote or invoice). + */ + public function document() + { + // define a custom polymorphic relationship or helper if needed + // Since it is an enum, we can't use standard morphTo easily without a map. + // But for now, I will just leave it or maybe add a helper. + // Standard Laravel morph expects 'document_type' to be the class name. + // Here it is 'quote' or 'invoice'. + // We can use morphMap in AppServiceProvider to map 'quote' => Quote::class. + return $this->morphTo(__FUNCTION__, 'document_type', 'document_id'); + } +} diff --git a/thanasoft-back/app/Models/Quote.php b/thanasoft-back/app/Models/Quote.php index 6e849c7..0f18fe0 100644 --- a/thanasoft-back/app/Models/Quote.php +++ b/thanasoft-back/app/Models/Quote.php @@ -58,4 +58,16 @@ class Quote extends Model { return $this->belongsTo(ClientGroup::class); } + + public function lines() + { + return $this->hasMany(QuoteLine::class); + } + + public function history() + { + return $this->hasMany(DocumentStatusHistory::class, 'document_id') + ->where('document_type', 'quote') + ->orderBy('changed_at', 'desc'); + } } diff --git a/thanasoft-back/app/Repositories/QuoteRepository.php b/thanasoft-back/app/Repositories/QuoteRepository.php index 1b3eebc..fa23a43 100644 --- a/thanasoft-back/app/Repositories/QuoteRepository.php +++ b/thanasoft-back/app/Repositories/QuoteRepository.php @@ -17,6 +17,7 @@ class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface parent::__construct($model); } + public function create(array $data): Quote { return DB::transaction(function () use ($data) { @@ -32,17 +33,70 @@ class QuoteRepository extends BaseRepository implements QuoteRepositoryInterface } } + // Record initial status history + $this->recordHistory($quote->id, null, $quote->status, 'Quote created'); + return $quote; } catch (\Exception $e) { - // Log the error Log::error('Error creating quote with lines: ' . $e->getMessage(), [ 'exception' => $e, 'data' => $data, ]); - - // Re-throw to trigger rollback throw $e; } }); } + + public function update(int|string $id, array $attributes): bool + { + return DB::transaction(function () use ($id, $attributes) { + try { + $quote = $this->find($id); + if (!$quote) { + return false; + } + + $oldStatus = $quote->status; + + // Update the quote + $updated = parent::update($id, $attributes); + + if ($updated) { + $newStatus = $attributes['status'] ?? $oldStatus; + + // If status changed, record history + if ($oldStatus !== $newStatus) { + $this->recordHistory((int) $id, $oldStatus, $newStatus, 'Quote status updated'); + } + } + + return $updated; + } catch (\Exception $e) { + Log::error('Error updating quote: ' . $e->getMessage(), [ + 'id' => $id, + 'attributes' => $attributes, + 'exception' => $e, + ]); + throw $e; + } + }); + } + + public function find(int|string $id, array $columns = ['*']): ?Quote + { + return $this->model->with(['client', 'lines.product', 'history.user'])->find($id, $columns); + } + + private function recordHistory(int $quoteId, ?string $oldStatus, string $newStatus, ?string $comment = null): void + { + \App\Models\DocumentStatusHistory::create([ + 'document_type' => 'quote', + 'document_id' => $quoteId, + 'old_status' => $oldStatus, + 'new_status' => $newStatus, + 'changed_by' => auth()->id(), // Assuming authenticated user + 'comment' => $comment, + 'changed_at' => now(), + ]); + } } diff --git a/thanasoft-back/database/migrations/2026_01_07_081428_create_document_status_history_table.php b/thanasoft-back/database/migrations/2026_01_07_081428_create_document_status_history_table.php new file mode 100644 index 0000000..c73f7e2 --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_07_081428_create_document_status_history_table.php @@ -0,0 +1,36 @@ +id(); + $table->enum('document_type', ['quote', 'invoice']); + $table->unsignedBigInteger('document_id'); + $table->string('old_status', 32)->nullable(); + $table->string('new_status', 32); + $table->unsignedBigInteger('changed_by')->nullable(); + $table->timestamp('changed_at')->useCurrent(); + $table->text('comment')->nullable(); + + $table->index(['document_type', 'document_id'], 'idx_dsh_doc'); + // Assuming we might want a foreign key for changed_by if users table exists, but user didn't explicitly ask for constraint, just column. I will leave it as column. + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('document_status_history'); + } +}; diff --git a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue index 0acb9bc..0bee068 100644 --- a/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue +++ b/thanasoft-front/src/components/Organism/Agenda/InterventionMultiStepModal.vue @@ -438,10 +438,10 @@ const handleSubmit = async () => { if (interventionForm.value[key] != null) { let value = interventionForm.value[key]; // Fix date format for scheduled_at - if (key === 'scheduled_at' && value) { - value = value.replace('T', ' '); + if (key === "scheduled_at" && value) { + value = value.replace("T", " "); if (value.length === 16) { - value += ':00'; + value += ":00"; } } formData.append(`intervention[${key}]`, value); diff --git a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepClient.vue b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepClient.vue index b29109e..738e6d0 100644 --- a/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepClient.vue +++ b/thanasoft-front/src/components/Organism/Agenda/WizardSteps/StepClient.vue @@ -135,7 +135,10 @@ placeholder="Code postal" maxlength="20" /> -
+
{{ getFieldError("billing_postal_code") }}
@@ -176,8 +179,8 @@ diff --git a/thanasoft-front/src/components/Organism/Interventions/intervention/AddPractitionerModal.vue b/thanasoft-front/src/components/Organism/Interventions/intervention/AddPractitionerModal.vue index 920833c..8a8f9bc 100644 --- a/thanasoft-front/src/components/Organism/Interventions/intervention/AddPractitionerModal.vue +++ b/thanasoft-front/src/components/Organism/Interventions/intervention/AddPractitionerModal.vue @@ -12,15 +12,15 @@