From 19b592720e5c52552e252f24c8049fde5107591a Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 6 Jan 2026 13:59:35 +0300 Subject: [PATCH] feat: Implement full CRUD API for quotes with dedicated model, repository, requests, resource, and database migration. --- .../Http/Controllers/Api/QuoteController.php | 158 ++++++++++++++++++ .../app/Http/Requests/StoreQuoteRequest.php | 67 ++++++++ .../app/Http/Requests/UpdateQuoteRequest.php | 59 +++++++ .../app/Http/Resources/QuoteResource.php | 35 ++++ thanasoft-back/app/Models/Quote.php | 42 +++++ .../Providers/RepositoryServiceProvider.php | 1 + .../app/Repositories/QuoteRepository.php | 15 ++ .../Repositories/QuoteRepositoryInterface.php | 9 + .../2026_01_06_105000_create_quotes_table.php | 40 +++++ thanasoft-back/routes/api.php | 4 + 10 files changed, 430 insertions(+) create mode 100644 thanasoft-back/app/Http/Controllers/Api/QuoteController.php create mode 100644 thanasoft-back/app/Http/Requests/StoreQuoteRequest.php create mode 100644 thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php create mode 100644 thanasoft-back/app/Http/Resources/QuoteResource.php create mode 100644 thanasoft-back/app/Models/Quote.php create mode 100644 thanasoft-back/app/Repositories/QuoteRepository.php create mode 100644 thanasoft-back/app/Repositories/QuoteRepositoryInterface.php create mode 100644 thanasoft-back/database/migrations/2026_01_06_105000_create_quotes_table.php diff --git a/thanasoft-back/app/Http/Controllers/Api/QuoteController.php b/thanasoft-back/app/Http/Controllers/Api/QuoteController.php new file mode 100644 index 0000000..1713408 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/QuoteController.php @@ -0,0 +1,158 @@ +quoteRepository->all(); + return QuoteResource::collection($quotes); + } catch (\Exception $e) { + Log::error('Error fetching quotes: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created quote. + */ + public function store(StoreQuoteRequest $request): QuoteResource|JsonResponse + { + try { + $quote = $this->quoteRepository->create($request->validated()); + return new QuoteResource($quote); + } catch (\Exception $e) { + Log::error('Error creating quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified quote. + */ + public function show(string $id): QuoteResource|JsonResponse + { + try { + $quote = $this->quoteRepository->find($id); + + if (! $quote) { + return response()->json([ + 'message' => 'Devis non trouvé.', + ], 404); + } + + return new QuoteResource($quote); + } catch (\Exception $e) { + Log::error('Error fetching quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'quote_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified quote. + */ + public function update(UpdateQuoteRequest $request, string $id): QuoteResource|JsonResponse + { + try { + $updated = $this->quoteRepository->update($id, $request->validated()); + + if (! $updated) { + return response()->json([ + 'message' => 'Devis non trouvé ou échec de la mise à jour.', + ], 404); + } + + $quote = $this->quoteRepository->find($id); + return new QuoteResource($quote); + } catch (\Exception $e) { + Log::error('Error updating quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'quote_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified quote. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->quoteRepository->delete($id); + + if (! $deleted) { + return response()->json([ + 'message' => 'Devis non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Devis supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting quote: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'quote_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du devis.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php b/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php new file mode 100644 index 0000000..6ec31a8 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreQuoteRequest.php @@ -0,0 +1,67 @@ +|string> + */ + public function rules(): array + { + 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', + 'currency' => 'required|string|size:3', + 'total_ht' => 'required|numeric|min:0', + 'total_tva' => 'required|numeric|min:0', + 'total_ttc' => 'required|numeric|min:0', + ]; + } + + public function messages(): array + { + return [ + '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.', + 'quote_date.date' => 'La date du devis n\'est pas valide.', + 'valid_until.date' => 'La date de validité n\'est pas valide.', + 'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.', + 'currency.required' => 'La devise est obligatoire.', + 'currency.size' => 'La devise doit comporter 3 caractères.', + 'total_ht.required' => 'Le total HT est obligatoire.', + 'total_ht.numeric' => 'Le total HT doit être un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas être négatif.', + 'total_tva.required' => 'Le total TVA est obligatoire.', + 'total_tva.numeric' => 'Le total TVA doit être un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', + 'total_ttc.required' => 'Le total TTC est obligatoire.', + 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php new file mode 100644 index 0000000..e56ce43 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateQuoteRequest.php @@ -0,0 +1,59 @@ +|string> + */ + public function rules(): array + { + 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, + 'status' => 'sometimes|in:brouillon,envoye,accepte,refuse,expire,annule', + 'quote_date' => 'sometimes|date', + 'valid_until' => 'nullable|date|after_or_equal:quote_date', + 'currency' => 'sometimes|string|size:3', + 'total_ht' => 'sometimes|numeric|min:0', + 'total_tva' => 'sometimes|numeric|min:0', + 'total_ttc' => 'sometimes|numeric|min:0', + ]; + } + + public function messages(): array + { + 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à.', + '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.', + 'valid_until.after_or_equal' => 'La date de validité doit être postérieure ou égale à la date du devis.', + 'currency.size' => 'La devise doit comporter 3 caractères.', + 'total_ht.numeric' => 'Le total HT doit être un nombre.', + 'total_ht.min' => 'Le total HT ne peut pas être négatif.', + 'total_tva.numeric' => 'Le total TVA doit être un nombre.', + 'total_tva.min' => 'Le total TVA ne peut pas être négatif.', + 'total_ttc.numeric' => 'Le total TTC doit être un nombre.', + 'total_ttc.min' => 'Le total TTC ne peut pas être négatif.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/QuoteResource.php b/thanasoft-back/app/Http/Resources/QuoteResource.php new file mode 100644 index 0000000..54f6fd7 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/QuoteResource.php @@ -0,0 +1,35 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'client_id' => $this->client_id, + 'group_id' => $this->group_id, + 'quote_number' => $this->quote_number, + 'status' => $this->status, + 'quote_date' => $this->quote_date, + 'valid_until' => $this->valid_until, + 'currency' => $this->currency, + 'total_ht' => $this->total_ht, + 'total_tva' => $this->total_tva, + 'total_ttc' => $this->total_ttc, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'client' => $this->whenLoaded('client'), + 'group' => $this->whenLoaded('group'), + ]; + } +} diff --git a/thanasoft-back/app/Models/Quote.php b/thanasoft-back/app/Models/Quote.php new file mode 100644 index 0000000..ad5a275 --- /dev/null +++ b/thanasoft-back/app/Models/Quote.php @@ -0,0 +1,42 @@ + 'date', + 'valid_until' => 'date', + 'total_ht' => 'decimal:2', + 'total_tva' => 'decimal:2', + 'total_ttc' => 'decimal:2', + ]; + + public function client() + { + return $this->belongsTo(Client::class); + } + + public function group() + { + return $this->belongsTo(ClientGroup::class); + } +} diff --git a/thanasoft-back/app/Providers/RepositoryServiceProvider.php b/thanasoft-back/app/Providers/RepositoryServiceProvider.php index a71fb2a..018d3ed 100644 --- a/thanasoft-back/app/Providers/RepositoryServiceProvider.php +++ b/thanasoft-back/app/Providers/RepositoryServiceProvider.php @@ -23,6 +23,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(DeceasedDocumentRepositoryInterface::class, DeceasedDocumentRepository::class); $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); } /** diff --git a/thanasoft-back/app/Repositories/QuoteRepository.php b/thanasoft-back/app/Repositories/QuoteRepository.php new file mode 100644 index 0000000..816e179 --- /dev/null +++ b/thanasoft-back/app/Repositories/QuoteRepository.php @@ -0,0 +1,15 @@ +id(); + $table->unsignedBigInteger('client_id'); + $table->unsignedBigInteger('group_id')->nullable(); + $table->string('quote_number', 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(); + $table->char('currency', 3)->default('EUR'); + $table->decimal('total_ht', 14, 2)->default(0); + $table->decimal('total_tva', 14, 2)->default(0); + $table->decimal('total_ttc', 14, 2)->default(0); + $table->timestamps(); + + $table->foreign('client_id', 'fk_quotes_client')->references('id')->on('clients'); + $table->foreign('group_id', 'fk_quotes_group')->references('id')->on('client_groups')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('quotes'); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index bcfa8e4..09c7428 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -18,6 +18,7 @@ use App\Http\Controllers\Api\DeceasedDocumentController; use App\Http\Controllers\Api\InterventionController; use App\Http\Controllers\Api\FileController; use App\Http\Controllers\Api\FileAttachmentController; +use App\Http\Controllers\Api\QuoteController; /* @@ -61,6 +62,9 @@ Route::middleware('auth:sanctum')->group(function () { Route::apiResource('client-categories', ClientCategoryController::class); + // Quote management + Route::apiResource('quotes', QuoteController::class); + // Fournisseur management Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']); Route::apiResource('fournisseurs', FournisseurController::class);