From ca09f6da2fa40936bd39ad3c878c13fd6241aaf9 Mon Sep 17 00:00:00 2001 From: Nyavokevin <42602932+nyavokevin@users.noreply.github.com> Date: Tue, 28 Oct 2025 18:03:44 +0300 Subject: [PATCH] Ajout et liste fournisseur --- .../Controllers/Api/FournisseurController.php | 208 ++++++++++++++++++ .../Http/Requests/StoreFournisseurRequest.php | 59 +++++ .../Requests/UpdateFournisseurRequest.php | 59 +++++ .../Fournisseur/FournisseurCollection.php | 47 ++++ .../Fournisseur/FournisseurResource.php | 39 ++++ thanasoft-back/app/Models/Fournisseur.php | 54 +++++ .../app/Providers/AppServiceProvider.php | 4 + .../Repositories/FournisseurRepository.php | 58 +++++ .../FournisseurRepositoryInterface.php | 12 + ...10_28_131100_create_fournisseurs_table.php | 40 ++++ thanasoft-back/routes/api.php | 4 + 11 files changed, 584 insertions(+) create mode 100644 thanasoft-back/app/Http/Controllers/Api/FournisseurController.php create mode 100644 thanasoft-back/app/Http/Requests/StoreFournisseurRequest.php create mode 100644 thanasoft-back/app/Http/Requests/UpdateFournisseurRequest.php create mode 100644 thanasoft-back/app/Http/Resources/Fournisseur/FournisseurCollection.php create mode 100644 thanasoft-back/app/Http/Resources/Fournisseur/FournisseurResource.php create mode 100644 thanasoft-back/app/Models/Fournisseur.php create mode 100644 thanasoft-back/app/Repositories/FournisseurRepository.php create mode 100644 thanasoft-back/app/Repositories/FournisseurRepositoryInterface.php create mode 100644 thanasoft-back/database/migrations/2025_10_28_131100_create_fournisseurs_table.php diff --git a/thanasoft-back/app/Http/Controllers/Api/FournisseurController.php b/thanasoft-back/app/Http/Controllers/Api/FournisseurController.php new file mode 100644 index 0000000..e916eb7 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/FournisseurController.php @@ -0,0 +1,208 @@ +get('per_page', 15); + $filters = [ + 'search' => $request->get('search'), + 'is_active' => $request->get('is_active'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + // Remove null filters + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $fournisseurs = $this->fournisseurRepository->paginate($perPage, $filters); + + return new FournisseurCollection($fournisseurs); + + } catch (\Exception $e) { + Log::error('Error fetching fournisseurs: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des fournisseurs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created fournisseur. + */ + public function store(StoreFournisseurRequest $request): FournisseurResource|JsonResponse + { + try { + $fournisseur = $this->fournisseurRepository->create($request->validated()); + return new FournisseurResource($fournisseur); + } catch (\Exception $e) { + Log::error('Error creating fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified fournisseur. + */ + public function show(string $id): FournisseurResource|JsonResponse + { + try { + $fournisseur = $this->fournisseurRepository->find($id); + + if (!$fournisseur) { + return response()->json([ + 'message' => 'Fournisseur non trouvé.', + ], 404); + } + + return new FournisseurResource($fournisseur); + } catch (\Exception $e) { + Log::error('Error fetching fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'fournisseur_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function searchBy(Request $request): JsonResponse + { + try { + $name = $request->get('name', ''); + + if (empty($name)) { + return response()->json([ + 'message' => 'Le paramètre "name" est requis.', + ], 400); + } + + $fournisseurs = $this->fournisseurRepository->searchByName($name); + + return response()->json([ + 'data' => $fournisseurs, + 'count' => $fournisseurs->count(), + 'message' => $fournisseurs->count() > 0 + ? 'Fournisseurs trouvés avec succès.' + : 'Aucun fournisseur trouvé.', + ], 200); + + } catch (\Exception $e) { + Log::error('Error searching fournisseurs by name: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'search_term' => $name, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche des fournisseurs.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified fournisseur. + */ + public function update(UpdateFournisseurRequest $request, string $id): FournisseurResource|JsonResponse + { + try { + $updated = $this->fournisseurRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Fournisseur non trouvé ou échec de la mise à jour.', + ], 404); + } + + $fournisseur = $this->fournisseurRepository->find($id); + return new FournisseurResource($fournisseur); + } catch (\Exception $e) { + Log::error('Error updating fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'fournisseur_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified fournisseur. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->fournisseurRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Fournisseur non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Fournisseur supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting fournisseur: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'fournisseur_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du fournisseur.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreFournisseurRequest.php b/thanasoft-back/app/Http/Requests/StoreFournisseurRequest.php new file mode 100644 index 0000000..2e6364b --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreFournisseurRequest.php @@ -0,0 +1,59 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'vat_number' => 'nullable|string|max:32', + 'siret' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'billing_address_line1' => 'nullable|string|max:255', + 'billing_address_line2' => 'nullable|string|max:255', + 'billing_postal_code' => 'nullable|string|max:20', + 'billing_city' => 'nullable|string|max:191', + 'billing_country_code' => 'nullable|string|size:2', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du fournisseur est obligatoire.', + 'name.string' => 'Le nom du fournisseur doit être une chaîne de caractères.', + 'name.max' => 'Le nom du fournisseur ne peut pas dépasser 255 caractères.', + 'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.', + 'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateFournisseurRequest.php b/thanasoft-back/app/Http/Requests/UpdateFournisseurRequest.php new file mode 100644 index 0000000..32923d9 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateFournisseurRequest.php @@ -0,0 +1,59 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'sometimes|required|string|max:255', + 'vat_number' => 'nullable|string|max:32', + 'siret' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:191', + 'phone' => 'nullable|string|max:50', + 'billing_address_line1' => 'nullable|string|max:255', + 'billing_address_line2' => 'nullable|string|max:255', + 'billing_postal_code' => 'nullable|string|max:20', + 'billing_city' => 'nullable|string|max:191', + 'billing_country_code' => 'nullable|string|size:2', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du fournisseur est obligatoire.', + 'name.string' => 'Le nom du fournisseur doit être une chaîne de caractères.', + 'name.max' => 'Le nom du fournisseur ne peut pas dépasser 255 caractères.', + 'vat_number.max' => 'Le numéro de TVA ne peut pas dépasser 32 caractères.', + 'siret.max' => 'Le SIRET ne peut pas dépasser 20 caractères.', + 'email.email' => 'L\'adresse email doit être valide.', + 'email.max' => 'L\'adresse email ne peut pas dépasser 191 caractères.', + 'phone.max' => 'Le téléphone ne peut pas dépasser 50 caractères.', + 'billing_address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'billing_address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'billing_postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'billing_city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'billing_country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurCollection.php b/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurCollection.php new file mode 100644 index 0000000..fd5a894 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurCollection.php @@ -0,0 +1,47 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'data' => $this->collection, + 'meta' => [ + 'total' => $this->total(), + 'per_page' => $this->perPage(), + 'current_page' => $this->currentPage(), + 'last_page' => $this->lastPage(), + 'from' => $this->firstItem(), + 'to' => $this->lastItem(), + 'stats' => [ + 'active' => $this->collection->where('is_active', true)->count(), + 'inactive' => $this->collection->where('is_active', false)->count(), + ], + ], + 'links' => [ + 'first' => $this->url(1), + 'last' => $this->url($this->lastPage()), + 'prev' => $this->previousPageUrl(), + 'next' => $this->nextPageUrl(), + ], + ]; + } + + public function with(Request $request): array + { + return [ + 'status' => 'success', + 'message' => 'Fournisseurs récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurResource.php b/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurResource.php new file mode 100644 index 0000000..b96145a --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Fournisseur/FournisseurResource.php @@ -0,0 +1,39 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'commercial' => $this->commercial(), + 'name' => $this->name, + 'vat_number' => $this->vat_number, + 'siret' => $this->siret, + 'email' => $this->email, + 'phone' => $this->phone, + 'billing_address' => [ + 'line1' => $this->billing_address_line1, + 'line2' => $this->billing_address_line2, + 'postal_code' => $this->billing_postal_code, + 'city' => $this->billing_city, + 'country_code' => $this->billing_country_code, + 'full_address' => $this->billing_address, + ], + 'notes' => $this->notes, + 'is_active' => $this->is_active, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + ]; + } +} diff --git a/thanasoft-back/app/Models/Fournisseur.php b/thanasoft-back/app/Models/Fournisseur.php new file mode 100644 index 0000000..78b1cda --- /dev/null +++ b/thanasoft-back/app/Models/Fournisseur.php @@ -0,0 +1,54 @@ + 'boolean', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function commercial(): ?string + { + return $this->user ? $this->user->name : 'Système'; + } + + /** + * Get the full billing address as a string. + */ + public function getBillingAddressAttribute(): ?string + { + $parts = array_filter([ + $this->billing_address_line1, + $this->billing_address_line2, + $this->billing_postal_code ? $this->billing_postal_code . ' ' . $this->billing_city : $this->billing_city, + $this->billing_country_code, + ]); + + return !empty($parts) ? implode(', ', $parts) : null; + } +} diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index 8b54e4a..e7f280a 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -31,6 +31,10 @@ class AppServiceProvider extends ServiceProvider $this->app->bind(\App\Repositories\ClientLocationRepositoryInterface::class, function ($app) { return new \App\Repositories\ClientLocationRepository($app->make(\App\Models\ClientLocation::class)); }); + + $this->app->bind(\App\Repositories\FournisseurRepositoryInterface::class, function ($app) { + return new \App\Repositories\FournisseurRepository($app->make(\App\Models\Fournisseur::class)); + }); } /** diff --git a/thanasoft-back/app/Repositories/FournisseurRepository.php b/thanasoft-back/app/Repositories/FournisseurRepository.php new file mode 100644 index 0000000..4f45f91 --- /dev/null +++ b/thanasoft-back/app/Repositories/FournisseurRepository.php @@ -0,0 +1,58 @@ +model->newQuery(); + + // Apply filters + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('name', 'like', '%' . $filters['search'] . '%') + ->orWhere('email', 'like', '%' . $filters['search'] . '%') + ->orWhere('vat_number', 'like', '%' . $filters['search'] . '%') + ->orWhere('siret', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (isset($filters['is_active'])) { + $query->where('is_active', $filters['is_active']); + } + + // Apply sorting + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->paginate($perPage); + } + + public function searchByName(string $name, int $perPage = 15, bool $exactMatch = false) + { + $query = $this->model->newQuery(); + + if ($exactMatch) { + $query->where('name', $name); + } else { + $query->where('name', 'like', '%' . $name . '%'); + } + + return $query->get(); + } +} diff --git a/thanasoft-back/app/Repositories/FournisseurRepositoryInterface.php b/thanasoft-back/app/Repositories/FournisseurRepositoryInterface.php new file mode 100644 index 0000000..39875e1 --- /dev/null +++ b/thanasoft-back/app/Repositories/FournisseurRepositoryInterface.php @@ -0,0 +1,12 @@ +id(); + $table->string('name'); + $table->string('vat_number', 32)->nullable(); + $table->string('siret', 20)->nullable(); + $table->string('email', 191)->nullable(); + $table->string('phone', 50)->nullable(); + $table->string('billing_address_line1')->nullable(); + $table->string('billing_address_line2')->nullable(); + $table->string('billing_postal_code', 20)->nullable(); + $table->string('billing_city', 191)->nullable(); + $table->string('billing_country_code', 2)->nullable(); + $table->text('notes')->nullable(); + $table->boolean('is_active')->default(true); + $table->foreignId('user_id')->nullable()->constrained()->onDelete('set null'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('fournisseurs'); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index da3c407..50ae280 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Api\ClientGroupController; use App\Http\Controllers\Api\ClientLocationController; use App\Http\Controllers\Api\ContactController; use App\Http\Controllers\Api\ClientCategoryController; +use App\Http\Controllers\Api\FournisseurController; /* |-------------------------------------------------------------------------- @@ -49,4 +50,7 @@ Route::middleware('auth:sanctum')->group(function () { Route::apiResource('client-categories', ClientCategoryController::class); + // Fournisseur management + Route::get('/fournisseurs/searchBy', [FournisseurController::class, 'searchBy']); + Route::apiResource('fournisseurs', FournisseurController::class); });