From d911435b5c2e611eb2b836454be4bbcf6c538eae Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 6 Jan 2026 16:50:03 +0300 Subject: [PATCH] feat: Implement comprehensive quote and quote line management with new backend API and frontend UI. --- .../Http/Requests/StoreQuoteLineRequest.php | 39 ++ .../app/Http/Requests/StoreQuoteRequest.php | 17 +- .../Http/Requests/UpdateQuoteLineRequest.php | 39 ++ .../app/Http/Requests/UpdateQuoteRequest.php | 8 +- .../app/Http/Resources/QuoteLineResource.php | 37 ++ .../app/Http/Resources/QuoteResource.php | 2 +- thanasoft-back/app/Models/Quote.php | 21 +- thanasoft-back/app/Models/QuoteLine.php | 48 +++ .../app/Providers/AppServiceProvider.php | 4 + .../Providers/RepositoryServiceProvider.php | 1 + .../app/Repositories/QuoteLineRepository.php | 41 ++ .../QuoteLineRepositoryInterface.php | 14 + .../app/Repositories/QuoteRepository.php | 37 +- .../2026_01_06_105000_create_quotes_table.php | 2 +- ..._01_06_124703_create_quote_lines_table.php | 44 +++ .../Quote/QuoteCreationPresentation.vue | 183 +++++++++ .../Quote/QuoteDetailPresentation.vue | 107 +++++ .../Organism/Quote/QuoteListPresentation.vue | 56 +++ .../molecules/Quote/ProductLineItem.vue | 214 ++++++++++ .../molecules/Quote/QuoteInfoCard.vue | 68 ++++ .../molecules/Quote/QuoteLinesTable.vue | 55 +++ .../molecules/Quote/QuoteTotalsCard.vue | 35 ++ .../molecules/Tables/Ventes/QuoteTable.vue | 368 ++++++++++++++++++ .../templates/Quote/CreateQuoteTemplate.vue | 57 +++ .../templates/Quote/ListQuoteTemplate.vue | 24 ++ .../templates/Quote/QuoteDetailTemplate.vue | 38 ++ thanasoft-front/src/router/index.js | 10 + thanasoft-front/src/services/quote.ts | 144 +++++++ thanasoft-front/src/stores/quoteStore.ts | 223 +++++++++++ .../src/views/pages/Ventes/Devis.vue | 10 +- .../src/views/pages/Ventes/NewQuote.vue | 7 + .../src/views/pages/Ventes/QuoteDetail.vue | 12 + 32 files changed, 1944 insertions(+), 21 deletions(-) create mode 100644 thanasoft-back/app/Http/Requests/StoreQuoteLineRequest.php create mode 100644 thanasoft-back/app/Http/Requests/UpdateQuoteLineRequest.php create mode 100644 thanasoft-back/app/Http/Resources/QuoteLineResource.php create mode 100644 thanasoft-back/app/Models/QuoteLine.php create mode 100644 thanasoft-back/app/Repositories/QuoteLineRepository.php create mode 100644 thanasoft-back/app/Repositories/QuoteLineRepositoryInterface.php create mode 100644 thanasoft-back/database/migrations/2026_01_06_124703_create_quote_lines_table.php create mode 100644 thanasoft-front/src/components/Organism/Quote/QuoteCreationPresentation.vue create mode 100644 thanasoft-front/src/components/Organism/Quote/QuoteDetailPresentation.vue create mode 100644 thanasoft-front/src/components/Organism/Quote/QuoteListPresentation.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/ProductLineItem.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/QuoteInfoCard.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/QuoteLinesTable.vue create mode 100644 thanasoft-front/src/components/molecules/Quote/QuoteTotalsCard.vue create mode 100644 thanasoft-front/src/components/molecules/Tables/Ventes/QuoteTable.vue create mode 100644 thanasoft-front/src/components/templates/Quote/CreateQuoteTemplate.vue create mode 100644 thanasoft-front/src/components/templates/Quote/ListQuoteTemplate.vue create mode 100644 thanasoft-front/src/components/templates/Quote/QuoteDetailTemplate.vue create mode 100644 thanasoft-front/src/services/quote.ts create mode 100644 thanasoft-front/src/stores/quoteStore.ts create mode 100644 thanasoft-front/src/views/pages/Ventes/NewQuote.vue create mode 100644 thanasoft-front/src/views/pages/Ventes/QuoteDetail.vue diff --git a/thanasoft-back/app/Http/Requests/StoreQuoteLineRequest.php b/thanasoft-back/app/Http/Requests/StoreQuoteLineRequest.php new file mode 100644 index 0000000..51a8ee7 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreQuoteLineRequest.php @@ -0,0 +1,39 @@ +|string> + */ + public function rules(): array + { + return [ + 'quote_id' => 'required|exists:quotes,id', + 'product_id' => 'nullable|exists:products,id', + 'packaging_id' => 'nullable|exists:product_packagings,id', + 'packages_qty' => 'nullable|numeric|min:0', + 'units_qty' => 'nullable|numeric|min:0', + 'description' => 'required|string', + 'qty_base' => 'nullable|numeric|min:0', + 'unit_price' => 'required|numeric|min:0', + 'unit_price_per_package' => 'nullable|numeric|min:0', + 'tva_rate_id' => 'nullable|exists:tva_rates,id', + 'discount_pct' => 'required|numeric|min:0|max:100', + 'total_ht' => 'required|numeric|min:0', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php b/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php index 6ec31a8..26e6c32 100644 --- a/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php +++ b/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php @@ -24,7 +24,6 @@ class StoreQuoteRequest extends FormRequest return [ 'client_id' => 'required|exists:clients,id', 'group_id' => 'nullable|exists:client_groups,id', - 'quote_number' => 'required|string|max:191|unique:quotes,quote_number', 'status' => 'required|in:brouillon,envoye,accepte,refuse,expire,annule', 'quote_date' => 'required|date', 'valid_until' => 'nullable|date|after_or_equal:quote_date', @@ -32,6 +31,18 @@ class StoreQuoteRequest extends FormRequest 'total_ht' => 'required|numeric|min:0', 'total_tva' => 'required|numeric|min:0', 'total_ttc' => 'required|numeric|min:0', + 'lines' => 'required|array|min:1', + 'lines.*.product_id' => 'nullable|exists:products,id', + 'lines.*.packaging_id' => 'nullable|exists:product_packagings,id', + 'lines.*.packages_qty' => 'nullable|numeric|min:0', + 'lines.*.units_qty' => 'nullable|numeric|min:0', + 'lines.*.description' => 'required|string', + 'lines.*.qty_base' => 'nullable|numeric|min:0', + 'lines.*.unit_price' => 'required|numeric|min:0', + 'lines.*.unit_price_per_package' => 'nullable|numeric|min:0', + 'lines.*.tva_rate_id' => 'nullable|exists:tva_rates,id', + 'lines.*.discount_pct' => 'required|numeric|min:0|max:100', + 'lines.*.total_ht' => 'required|numeric|min:0', ]; } @@ -41,10 +52,6 @@ class StoreQuoteRequest extends FormRequest 'client_id.required' => 'Le client est obligatoire.', 'client_id.exists' => 'Le client sélectionné est invalide.', 'group_id.exists' => 'Le groupe sélectionné est invalide.', - 'quote_number.required' => 'Le numéro de devis est obligatoire.', - 'quote_number.string' => 'Le numéro de devis doit être une chaîne de caractères.', - 'quote_number.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.', - 'quote_number.unique' => 'Ce numéro de devis existe déjà.', 'status.required' => 'Le statut est obligatoire.', 'status.in' => 'Le statut sélectionné est invalide.', 'quote_date.required' => 'La date du devis est obligatoire.', diff --git a/thanasoft-back/app/Http/Requests/UpdateQuoteLineRequest.php b/thanasoft-back/app/Http/Requests/UpdateQuoteLineRequest.php new file mode 100644 index 0000000..7916e87 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateQuoteLineRequest.php @@ -0,0 +1,39 @@ +|string> + */ + public function rules(): array + { + return [ + 'quote_id' => 'sometimes|exists:quotes,id', + 'product_id' => 'nullable|exists:products,id', + 'packaging_id' => 'nullable|exists:product_packagings,id', + 'packages_qty' => 'nullable|numeric|min:0', + 'units_qty' => 'nullable|numeric|min:0', + 'description' => 'sometimes|string', + 'qty_base' => 'nullable|numeric|min:0', + 'unit_price' => 'sometimes|numeric|min:0', + 'unit_price_per_package' => 'nullable|numeric|min:0', + 'tva_rate_id' => 'nullable|exists:tva_rates,id', + 'discount_pct' => 'sometimes|numeric|min:0|max:100', + 'total_ht' => 'sometimes|numeric|min:0', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php index e56ce43..a99361b 100644 --- a/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php +++ b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php @@ -24,7 +24,7 @@ class UpdateQuoteRequest extends FormRequest return [ 'client_id' => 'sometimes|exists:clients,id', 'group_id' => 'nullable|exists:client_groups,id', - 'quote_number' => 'sometimes|string|max:191|unique:quotes,quote_number,' . $this->quote->id, + 'reference' => 'sometimes|string|max:191|unique:quotes,reference,' . $this->quote->id, 'status' => 'sometimes|in:brouillon,envoye,accepte,refuse,expire,annule', 'quote_date' => 'sometimes|date', 'valid_until' => 'nullable|date|after_or_equal:quote_date', @@ -40,9 +40,9 @@ class UpdateQuoteRequest extends FormRequest return [ 'client_id.exists' => 'Le client sélectionné est invalide.', 'group_id.exists' => 'Le groupe sélectionné est invalide.', - 'quote_number.string' => 'Le numéro de devis doit être une chaîne de caractères.', - 'quote_number.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.', - 'quote_number.unique' => 'Ce numéro de devis existe déjà.', + 'reference.string' => 'Le numéro de devis doit être une chaîne de caractères.', + 'reference.max' => 'Le numéro de devis ne doit pas dépasser 191 caractères.', + 'reference.unique' => 'Ce numéro de devis existe déjà.', 'status.in' => 'Le statut sélectionné est invalide.', 'quote_date.date' => 'La date du devis n\'est pas valide.', 'valid_until.date' => 'La date de validité n\'est pas valide.', diff --git a/thanasoft-back/app/Http/Resources/QuoteLineResource.php b/thanasoft-back/app/Http/Resources/QuoteLineResource.php new file mode 100644 index 0000000..a39ddbb --- /dev/null +++ b/thanasoft-back/app/Http/Resources/QuoteLineResource.php @@ -0,0 +1,37 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'quote_id' => $this->quote_id, + 'product_id' => $this->product_id, + 'product_name' => $this->product ? $this->product->nom : null, // Assuming 'nom' is the name field + 'packaging_id' => $this->packaging_id, + 'packages_qty' => $this->packages_qty, + 'units_qty' => $this->units_qty, + 'description' => $this->description, + 'qty_base' => $this->qty_base, + 'unit_price' => $this->unit_price, + 'unit_price_per_package' => $this->unit_price_per_package, + 'tva_rate_id' => $this->tva_rate_id, + 'discount_pct' => $this->discount_pct, + 'total_ht' => $this->total_ht, + 'product' => $this->whenLoaded('product'), + 'packaging' => $this->whenLoaded('packaging'), + 'tva_rate' => $this->whenLoaded('tvaRate'), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/QuoteResource.php b/thanasoft-back/app/Http/Resources/QuoteResource.php index 54f6fd7..a31ab24 100644 --- a/thanasoft-back/app/Http/Resources/QuoteResource.php +++ b/thanasoft-back/app/Http/Resources/QuoteResource.php @@ -18,7 +18,7 @@ class QuoteResource extends JsonResource 'id' => $this->id, 'client_id' => $this->client_id, 'group_id' => $this->group_id, - 'quote_number' => $this->quote_number, + 'reference' => $this->reference, 'status' => $this->status, 'quote_date' => $this->quote_date, 'valid_until' => $this->valid_until, diff --git a/thanasoft-back/app/Models/Quote.php b/thanasoft-back/app/Models/Quote.php index ad5a275..6e849c7 100644 --- a/thanasoft-back/app/Models/Quote.php +++ b/thanasoft-back/app/Models/Quote.php @@ -12,7 +12,7 @@ class Quote extends Model protected $fillable = [ 'client_id', 'group_id', - 'quote_number', + 'reference', 'status', 'quote_date', 'valid_until', @@ -22,6 +22,25 @@ class Quote extends Model 'total_ttc', ]; + protected static function booted() + { + static::creating(function ($quote) { + $prefix = 'DEV-' . now()->format('Ym') . '-'; + $lastQuote = self::where('reference', 'like', $prefix . '%') + ->orderBy('reference', 'desc') + ->first(); + + if ($lastQuote) { + $lastNumber = intval(substr($lastQuote->reference, -4)); + $newNumber = $lastNumber + 1; + } else { + $newNumber = 1; + } + + $quote->reference = $prefix . str_pad((string)$newNumber, 4, '0', STR_PAD_LEFT); + }); + } + protected $casts = [ 'quote_date' => 'date', 'valid_until' => 'date', diff --git a/thanasoft-back/app/Models/QuoteLine.php b/thanasoft-back/app/Models/QuoteLine.php new file mode 100644 index 0000000..22ecd20 --- /dev/null +++ b/thanasoft-back/app/Models/QuoteLine.php @@ -0,0 +1,48 @@ +belongsTo(Quote::class); + } + + public function product() + { + return $this->belongsTo(Product::class); + } + + public function packaging() + { + // Assuming ProductPackaging model exists + return $this->belongsTo(\App\Models\Stock\ProductPackaging::class, 'packaging_id'); + } + + public function tvaRate() + { + // Assuming TvaRate model exists + return $this->belongsTo(\App\Models\TvaRate::class, 'tva_rate_id'); + } +} diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index f79d6fd..5f859b2 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -66,6 +66,10 @@ class AppServiceProvider extends ServiceProvider $this->app->bind(\App\Repositories\FileRepositoryInterface::class, \App\Repositories\FileRepository::class); $this->app->bind(\App\Repositories\ProductCategoryRepositoryInterface::class, \App\Repositories\ProductCategoryRepository::class); + + $this->app->bind(\App\Repositories\QuoteRepositoryInterface::class, \App\Repositories\QuoteRepository::class); + + $this->app->bind(\App\Repositories\QuoteLineRepositoryInterface::class, \App\Repositories\QuoteLineRepository::class); } /** diff --git a/thanasoft-back/app/Providers/RepositoryServiceProvider.php b/thanasoft-back/app/Providers/RepositoryServiceProvider.php index 018d3ed..15e4862 100644 --- a/thanasoft-back/app/Providers/RepositoryServiceProvider.php +++ b/thanasoft-back/app/Providers/RepositoryServiceProvider.php @@ -24,6 +24,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(InterventionRepositoryInterface::class, InterventionRepository::class); $this->app->bind(FileRepositoryInterface::class, FileRepository::class); $this->app->bind(\App\Repositories\QuoteRepositoryInterface::class, \App\Repositories\QuoteRepository::class); + $this->app->bind(\App\Repositories\QuoteLineRepositoryInterface::class, \App\Repositories\QuoteLineRepository::class); } /** diff --git a/thanasoft-back/app/Repositories/QuoteLineRepository.php b/thanasoft-back/app/Repositories/QuoteLineRepository.php new file mode 100644 index 0000000..c9f0bfb --- /dev/null +++ b/thanasoft-back/app/Repositories/QuoteLineRepository.php @@ -0,0 +1,41 @@ +find($id); + if ($line) { + return $line->update($data); + } + return false; + } + + public function delete(string $id): bool + { + $line = $this->find($id); + if ($line) { + return $line->delete(); + } + return false; + } + + public function find(string $id): ?QuoteLine + { + return QuoteLine::find($id); + } + + public function getByQuoteId(string $quoteId) + { + return QuoteLine::where('quote_id', $quoteId)->get(); + } +} diff --git a/thanasoft-back/app/Repositories/QuoteLineRepositoryInterface.php b/thanasoft-back/app/Repositories/QuoteLineRepositoryInterface.php new file mode 100644 index 0000000..88e4b04 --- /dev/null +++ b/thanasoft-back/app/Repositories/QuoteLineRepositoryInterface.php @@ -0,0 +1,14 @@ +id; + $this->quoteLineRepository->create($lineData); + } + } + + 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; + } + }); + } } diff --git a/thanasoft-back/database/migrations/2026_01_06_105000_create_quotes_table.php b/thanasoft-back/database/migrations/2026_01_06_105000_create_quotes_table.php index 2c01ab5..357da47 100644 --- a/thanasoft-back/database/migrations/2026_01_06_105000_create_quotes_table.php +++ b/thanasoft-back/database/migrations/2026_01_06_105000_create_quotes_table.php @@ -15,7 +15,7 @@ return new class extends Migration $table->id(); $table->unsignedBigInteger('client_id'); $table->unsignedBigInteger('group_id')->nullable(); - $table->string('quote_number', 191); + $table->string('reference', 191); $table->enum('status', ['brouillon', 'envoye', 'accepte', 'refuse', 'expire', 'annule'])->default('brouillon')->index('idx_quotes_status'); $table->date('quote_date')->default(now()); $table->date('valid_until')->nullable(); diff --git a/thanasoft-back/database/migrations/2026_01_06_124703_create_quote_lines_table.php b/thanasoft-back/database/migrations/2026_01_06_124703_create_quote_lines_table.php new file mode 100644 index 0000000..7ffe22a --- /dev/null +++ b/thanasoft-back/database/migrations/2026_01_06_124703_create_quote_lines_table.php @@ -0,0 +1,44 @@ +id(); + $table->foreignId('quote_id')->constrained('quotes')->onDelete('cascade'); + $table->foreignId('product_id')->nullable()->constrained('products')->onDelete('set null'); + $table->decimal('packages_qty', 14, 3)->nullable(); + $table->decimal('units_qty', 14, 3)->nullable(); + $table->text('description'); + $table->decimal('qty_base', 14, 3)->nullable(); + $table->decimal('unit_price', 12, 2); + $table->decimal('unit_price_per_package', 12, 2)->nullable(); + + // Tables do not exist yet, removing constraints + $table->foreignId('packaging_id')->nullable(); + $table->foreignId('tva_rate_id')->nullable(); + + $table->decimal('discount_pct', 5, 2)->default(0); + $table->decimal('total_ht', 14, 2); + + // If timestamps are needed (default model usually has them) + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('quote_lines'); + } +}; diff --git a/thanasoft-front/src/components/Organism/Quote/QuoteCreationPresentation.vue b/thanasoft-front/src/components/Organism/Quote/QuoteCreationPresentation.vue new file mode 100644 index 0000000..28e5ea4 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Quote/QuoteCreationPresentation.vue @@ -0,0 +1,183 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Quote/QuoteDetailPresentation.vue b/thanasoft-front/src/components/Organism/Quote/QuoteDetailPresentation.vue new file mode 100644 index 0000000..b109176 --- /dev/null +++ b/thanasoft-front/src/components/Organism/Quote/QuoteDetailPresentation.vue @@ -0,0 +1,107 @@ + + + diff --git a/thanasoft-front/src/components/Organism/Quote/QuoteListPresentation.vue b/thanasoft-front/src/components/Organism/Quote/QuoteListPresentation.vue new file mode 100644 index 0000000..978083d --- /dev/null +++ b/thanasoft-front/src/components/Organism/Quote/QuoteListPresentation.vue @@ -0,0 +1,56 @@ + + + \ No newline at end of file diff --git a/thanasoft-front/src/components/molecules/Quote/ProductLineItem.vue b/thanasoft-front/src/components/molecules/Quote/ProductLineItem.vue new file mode 100644 index 0000000..fdde91c --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/ProductLineItem.vue @@ -0,0 +1,214 @@ + + + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteInfoCard.vue b/thanasoft-front/src/components/molecules/Quote/QuoteInfoCard.vue new file mode 100644 index 0000000..0acf0cf --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteInfoCard.vue @@ -0,0 +1,68 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteLinesTable.vue b/thanasoft-front/src/components/molecules/Quote/QuoteLinesTable.vue new file mode 100644 index 0000000..c421318 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteLinesTable.vue @@ -0,0 +1,55 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Quote/QuoteTotalsCard.vue b/thanasoft-front/src/components/molecules/Quote/QuoteTotalsCard.vue new file mode 100644 index 0000000..c18c232 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Quote/QuoteTotalsCard.vue @@ -0,0 +1,35 @@ + + + diff --git a/thanasoft-front/src/components/molecules/Tables/Ventes/QuoteTable.vue b/thanasoft-front/src/components/molecules/Tables/Ventes/QuoteTable.vue new file mode 100644 index 0000000..2043e23 --- /dev/null +++ b/thanasoft-front/src/components/molecules/Tables/Ventes/QuoteTable.vue @@ -0,0 +1,368 @@ + + + + + diff --git a/thanasoft-front/src/components/templates/Quote/CreateQuoteTemplate.vue b/thanasoft-front/src/components/templates/Quote/CreateQuoteTemplate.vue new file mode 100644 index 0000000..bdfe923 --- /dev/null +++ b/thanasoft-front/src/components/templates/Quote/CreateQuoteTemplate.vue @@ -0,0 +1,57 @@ + + + diff --git a/thanasoft-front/src/components/templates/Quote/ListQuoteTemplate.vue b/thanasoft-front/src/components/templates/Quote/ListQuoteTemplate.vue new file mode 100644 index 0000000..d670806 --- /dev/null +++ b/thanasoft-front/src/components/templates/Quote/ListQuoteTemplate.vue @@ -0,0 +1,24 @@ + + + diff --git a/thanasoft-front/src/components/templates/Quote/QuoteDetailTemplate.vue b/thanasoft-front/src/components/templates/Quote/QuoteDetailTemplate.vue new file mode 100644 index 0000000..5f07559 --- /dev/null +++ b/thanasoft-front/src/components/templates/Quote/QuoteDetailTemplate.vue @@ -0,0 +1,38 @@ + + + diff --git a/thanasoft-front/src/router/index.js b/thanasoft-front/src/router/index.js index 95b7b97..54611bf 100644 --- a/thanasoft-front/src/router/index.js +++ b/thanasoft-front/src/router/index.js @@ -482,11 +482,21 @@ const routes = [ name: "Devis", component: () => import("@/views/pages/Ventes/Devis.vue"), }, + { + path: "/ventes/devis/:id", + name: "Quote Details", + component: () => import("@/views/pages/Ventes/QuoteDetail.vue"), + }, { path: "/ventes/factures", name: "Factures ventes", component: () => import("@/views/pages/Ventes/Factures.vue"), }, + { + path: "/ventes/devis/new", + name: "Nouveau Devis", + component: () => import("@/views/pages/Ventes/NewQuote.vue"), + }, { path: "/ventes/statistiques", name: "Statistiques ventes", diff --git a/thanasoft-front/src/services/quote.ts b/thanasoft-front/src/services/quote.ts new file mode 100644 index 0000000..01bf389 --- /dev/null +++ b/thanasoft-front/src/services/quote.ts @@ -0,0 +1,144 @@ +import { request } from "./http"; +import { Client } from "./client"; + +export interface Quote { + id: number; + client_id: number; + group_id: number | null; + reference: string; + status: 'brouillon' | 'envoye' | 'accepte' | 'refuse' | 'expire' | 'annule'; + quote_date: string; + valid_until: string | null; + currency: string; + total_ht: number; + total_tva: number; + total_ttc: number; + created_at: string; + updated_at: string; + client?: Client; +} + +export interface QuoteListResponse { + data: Quote[]; + meta?: { + current_page: number; + last_page: number; + per_page: number; + total: number; + }; +} + +export interface QuoteResponse { + data: Quote; +} + +export interface QuoteLine { + product_id: number | null; + product_name: string; + packaging_id?: number | null; + packages_qty?: number | null; + units_qty?: number | null; + description?: string; + qty_base?: number | null; + unit_price: number; + unit_price_per_package?: number | null; + tva_rate_id?: number | null; + discount_pct?: number; + quantity: number; // Frontend helper, maps to units_qty usually or distinct + tva?: number; // Frontend helper + total_ht?: number; +} + +export interface CreateQuotePayload { + client_id: number; + group_id?: number | null; + status: 'brouillon' | 'envoye' | 'accepte' | 'refuse' | 'expire' | 'annule'; + quote_date: string; + valid_until?: string | null; + currency: string; + total_ht: number; + total_tva: number; + total_ttc: number; + lines: QuoteLine[]; +} + +export interface UpdateQuotePayload extends Partial { + id: number; +} + +export const QuoteService = { + /** + * Get all quotes with pagination + */ + async getAllQuotes(params?: { + page?: number; + per_page?: number; + search?: string; + status?: string; + client_id?: number; + }): Promise { + const response = await request({ + url: "/api/quotes", + method: "get", + params, + }); + + return response; + }, + + /** + * Get a specific quote by ID + */ + async getQuote(id: number): Promise { + const response = await request({ + url: `/api/quotes/${id}`, + method: "get", + }); + + return response; + }, + + /** + * Create a new quote + */ + async createQuote(payload: CreateQuotePayload): Promise { + const response = await request({ + url: "/api/quotes", + method: "post", + data: payload, + }); + + return response; + }, + + /** + * Update an existing quote + */ + async updateQuote(payload: UpdateQuotePayload): Promise { + const { id, ...updateData } = payload; + + const response = await request({ + url: `/api/quotes/${id}`, + method: "put", + data: updateData, + }); + + return response; + }, + + /** + * Delete a quote + */ + async deleteQuote( + id: number + ): Promise<{ success: boolean; message: string }> { + const response = await request<{ success: boolean; message: string }>({ + url: `/api/quotes/${id}`, + method: "delete", + }); + + return response; + }, +}; + +export default QuoteService; diff --git a/thanasoft-front/src/stores/quoteStore.ts b/thanasoft-front/src/stores/quoteStore.ts new file mode 100644 index 0000000..4fd5869 --- /dev/null +++ b/thanasoft-front/src/stores/quoteStore.ts @@ -0,0 +1,223 @@ +import { defineStore } from "pinia"; +import { ref, computed } from "vue"; +import QuoteService, { + Quote, + CreateQuotePayload, + UpdateQuotePayload, +} from "@/services/quote"; + +export const useQuoteStore = defineStore("quote", () => { + // State + const quotes = ref([]); + const currentQuote = ref(null); + const loading = ref(false); + const error = ref(null); + + // Pagination state + const pagination = ref({ + current_page: 1, + last_page: 1, + per_page: 10, + total: 0, + }); + + // Getters + const allQuotes = computed(() => quotes.value); + const isLoading = computed(() => loading.value); + const hasError = computed(() => error.value !== null); + const getError = computed(() => error.value); + const getQuoteById = computed(() => (id: number) => + quotes.value.find((quote) => quote.id === id) + ); + const getPagination = computed(() => pagination.value); + + // Actions + const setLoading = (isLoading: boolean) => { + loading.value = isLoading; + }; + + const setError = (err: string | null) => { + error.value = err; + }; + + const setQuotes = (newQuotes: Quote[]) => { + quotes.value = newQuotes; + }; + + const setCurrentQuote = (quote: Quote | null) => { + currentQuote.value = quote; + }; + + const setPagination = (meta: any) => { + if (meta) { + pagination.value = { + current_page: meta.current_page || 1, + last_page: meta.last_page || 1, + per_page: meta.per_page || 10, + total: meta.total || 0, + }; + } + }; + + /** + * Fetch all quotes with optional pagination and filters + */ + const fetchQuotes = async (params?: { + page?: number; + per_page?: number; + search?: string; + status?: string; + client_id?: number; + }) => { + setLoading(true); + setError(null); + + try { + const response = await QuoteService.getAllQuotes(params); + setQuotes(response.data); + if (response.meta) { + setPagination(response.meta); + } + return response; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Failed to fetch quotes"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Fetch a single quote by ID + */ + const fetchQuote = async (id: number) => { + setLoading(true); + setError(null); + + try { + const response = await QuoteService.getQuote(id); + setCurrentQuote(response.data); + return response.data; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Failed to fetch quote"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Create a new quote + */ + const createQuote = async (payload: CreateQuotePayload) => { + setLoading(true); + setError(null); + + try { + const response = await QuoteService.createQuote(payload); + // Add the new quote to the list + quotes.value.push(response.data); + setCurrentQuote(response.data); + return response.data; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Failed to create quote"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Update an existing quote + */ + const updateQuote = async (payload: UpdateQuotePayload) => { + setLoading(true); + setError(null); + + try { + const response = await QuoteService.updateQuote(payload); + const updatedQuote = response.data; + + // Update in the quotes list + const index = quotes.value.findIndex( + (quote) => quote.id === updatedQuote.id + ); + if (index !== -1) { + quotes.value[index] = updatedQuote; + } + + // Update current quote if it's the one being edited + if (currentQuote.value && currentQuote.value.id === updatedQuote.id) { + setCurrentQuote(updatedQuote); + } + + return updatedQuote; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Failed to update quote"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + /** + * Delete a quote + */ + const deleteQuote = async (id: number) => { + setLoading(true); + setError(null); + + try { + const response = await QuoteService.deleteQuote(id); + + // Remove from the quotes list + quotes.value = quotes.value.filter((quote) => quote.id !== id); + + // Clear current quote if it's the one being deleted + if (currentQuote.value && currentQuote.value.id === id) { + setCurrentQuote(null); + } + + return response; + } catch (err: any) { + const errorMessage = + err.response?.data?.message || err.message || "Failed to delete quote"; + setError(errorMessage); + throw err; + } finally { + setLoading(false); + } + }; + + return { + // State + quotes, + currentQuote, + loading, + error, + pagination, + + // Getters + allQuotes, + isLoading, + hasError, + getError, + getQuoteById, + getPagination, + + // Actions + fetchQuotes, + fetchQuote, + createQuote, + updateQuote, + deleteQuote, + }; +}); diff --git a/thanasoft-front/src/views/pages/Ventes/Devis.vue b/thanasoft-front/src/views/pages/Ventes/Devis.vue index 38df022..018a4d9 100644 --- a/thanasoft-front/src/views/pages/Ventes/Devis.vue +++ b/thanasoft-front/src/views/pages/Ventes/Devis.vue @@ -1,11 +1,7 @@ - diff --git a/thanasoft-front/src/views/pages/Ventes/NewQuote.vue b/thanasoft-front/src/views/pages/Ventes/NewQuote.vue new file mode 100644 index 0000000..a029b4c --- /dev/null +++ b/thanasoft-front/src/views/pages/Ventes/NewQuote.vue @@ -0,0 +1,7 @@ + + + diff --git a/thanasoft-front/src/views/pages/Ventes/QuoteDetail.vue b/thanasoft-front/src/views/pages/Ventes/QuoteDetail.vue new file mode 100644 index 0000000..6752907 --- /dev/null +++ b/thanasoft-front/src/views/pages/Ventes/QuoteDetail.vue @@ -0,0 +1,12 @@ + + +