diff --git a/thanasoft-back/app/Http/Controllers/Api/SousTraitantController.php b/thanasoft-back/app/Http/Controllers/Api/SousTraitantController.php new file mode 100644 index 0000000..4d12213 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/SousTraitantController.php @@ -0,0 +1,190 @@ +get('per_page', 15); + $filters = [ + 'search' => $request->get('search'), + 'statut' => $request->get('statut'), + 'sort_by' => $request->get('sort_by', 'created_at'), + 'sort_direction' => $request->get('sort_direction', 'desc'), + ]; + + $filters = array_filter($filters, function ($value) { + return $value !== null && $value !== ''; + }); + + $sousTraitants = $this->sousTraitantRepository->paginate($perPage, $filters); + + return new SousTraitantCollection($sousTraitants); + } catch (\Exception $e) { + Log::error('Error fetching sous-traitants: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des sous-traitants.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function store(StoreSousTraitantRequest $request): SousTraitantResource|JsonResponse + { + try { + $sousTraitant = $this->sousTraitantRepository->create($request->validated()); + + return new SousTraitantResource($sousTraitant); + } catch (\Exception $e) { + Log::error('Error creating sous-traitant: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du sous-traitant.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function show(string $id): SousTraitantResource|JsonResponse + { + try { + $sousTraitant = $this->sousTraitantRepository->find($id); + + if (!$sousTraitant) { + return response()->json([ + 'message' => 'Sous-traitant non trouvé.', + ], 404); + } + + return new SousTraitantResource($sousTraitant); + } catch (\Exception $e) { + Log::error('Error fetching sous-traitant: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'sous_traitant_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du sous-traitant.', + '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); + } + + $sousTraitants = $this->sousTraitantRepository->searchByName($name); + + return response()->json([ + 'data' => $sousTraitants, + 'count' => $sousTraitants->count(), + 'message' => $sousTraitants->count() > 0 + ? 'Sous-traitants trouvés avec succès.' + : 'Aucun sous-traitant trouvé.', + ], 200); + } catch (\Exception $e) { + Log::error('Error searching sous-traitants by name: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la recherche des sous-traitants.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function update(UpdateSousTraitantRequest $request, string $id): SousTraitantResource|JsonResponse + { + try { + $updated = $this->sousTraitantRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Sous-traitant non trouvé ou échec de la mise à jour.', + ], 404); + } + + $sousTraitant = $this->sousTraitantRepository->find($id); + + return new SousTraitantResource($sousTraitant); + } catch (\Exception $e) { + Log::error('Error updating sous-traitant: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'sous_traitant_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du sous-traitant.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->sousTraitantRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Sous-traitant non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Sous-traitant supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting sous-traitant: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'sous_traitant_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du sous-traitant.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreSousTraitantRequest.php b/thanasoft-back/app/Http/Requests/StoreSousTraitantRequest.php new file mode 100644 index 0000000..78b9a6a --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreSousTraitantRequest.php @@ -0,0 +1,58 @@ + 'required|string|max:255', + 'siret' => 'nullable|string|max:20', + 'forme_juridique' => 'nullable|string|max:100', + 'code_ape' => 'nullable|string|max:20', + 'adresse' => 'nullable|string', + 'contact_principal' => 'required|string|max:255', + 'telephone' => 'nullable|string|max:50', + 'email' => 'nullable|email|max:191', + 'site_web' => 'nullable|url|max:191', + 'numero_contrat' => 'nullable|string|max:100', + 'montant_contrat' => 'nullable|numeric|min:0', + 'date_debut_contrat' => 'nullable|date', + 'date_fin_contrat' => 'nullable|date|after_or_equal:date_debut_contrat', + 'type_prestation' => 'nullable|string|max:255', + 'conditions_paiement' => 'nullable|string', + 'statut' => ['nullable', Rule::in(['actif', 'inactif', 'en_evaluation'])], + 'note_qualite' => 'nullable|numeric|min:0|max:5', + 'certifications_labels' => 'nullable|array', + 'certifications_labels.*' => 'string|max:255', + ]; + } + + public function messages(): array + { + return [ + 'nom_entreprise.required' => 'Le nom de l\'entreprise est obligatoire.', + 'contact_principal.required' => 'Le contact principal est obligatoire.', + 'email.email' => 'L\'adresse email doit être valide.', + 'site_web.url' => 'Le site web doit être une URL valide.', + 'montant_contrat.numeric' => 'Le montant du contrat doit être numérique.', + 'montant_contrat.min' => 'Le montant du contrat doit être supérieur ou égal à 0.', + 'date_fin_contrat.after_or_equal' => 'La date de fin doit être postérieure ou égale à la date de début.', + 'statut.in' => 'Le statut sélectionné est invalide.', + 'note_qualite.numeric' => 'La note de qualité doit être numérique.', + 'note_qualite.min' => 'La note de qualité doit être au minimum de 0.', + 'note_qualite.max' => 'La note de qualité doit être au maximum de 5.', + 'certifications_labels.array' => 'Les certifications doivent être envoyées sous forme de liste.', + 'certifications_labels.*.string' => 'Chaque certification doit être une chaîne de caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateSousTraitantRequest.php b/thanasoft-back/app/Http/Requests/UpdateSousTraitantRequest.php new file mode 100644 index 0000000..913c2a5 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateSousTraitantRequest.php @@ -0,0 +1,58 @@ + 'sometimes|required|string|max:255', + 'siret' => 'nullable|string|max:20', + 'forme_juridique' => 'nullable|string|max:100', + 'code_ape' => 'nullable|string|max:20', + 'adresse' => 'nullable|string', + 'contact_principal' => 'sometimes|required|string|max:255', + 'telephone' => 'nullable|string|max:50', + 'email' => 'nullable|email|max:191', + 'site_web' => 'nullable|url|max:191', + 'numero_contrat' => 'nullable|string|max:100', + 'montant_contrat' => 'nullable|numeric|min:0', + 'date_debut_contrat' => 'nullable|date', + 'date_fin_contrat' => 'nullable|date|after_or_equal:date_debut_contrat', + 'type_prestation' => 'nullable|string|max:255', + 'conditions_paiement' => 'nullable|string', + 'statut' => ['nullable', Rule::in(['actif', 'inactif', 'en_evaluation'])], + 'note_qualite' => 'nullable|numeric|min:0|max:5', + 'certifications_labels' => 'nullable|array', + 'certifications_labels.*' => 'string|max:255', + ]; + } + + public function messages(): array + { + return [ + 'nom_entreprise.required' => 'Le nom de l\'entreprise est obligatoire.', + 'contact_principal.required' => 'Le contact principal est obligatoire.', + 'email.email' => 'L\'adresse email doit être valide.', + 'site_web.url' => 'Le site web doit être une URL valide.', + 'montant_contrat.numeric' => 'Le montant du contrat doit être numérique.', + 'montant_contrat.min' => 'Le montant du contrat doit être supérieur ou égal à 0.', + 'date_fin_contrat.after_or_equal' => 'La date de fin doit être postérieure ou égale à la date de début.', + 'statut.in' => 'Le statut sélectionné est invalide.', + 'note_qualite.numeric' => 'La note de qualité doit être numérique.', + 'note_qualite.min' => 'La note de qualité doit être au minimum de 0.', + 'note_qualite.max' => 'La note de qualité doit être au maximum de 5.', + 'certifications_labels.array' => 'Les certifications doivent être envoyées sous forme de liste.', + 'certifications_labels.*.string' => 'Chaque certification doit être une chaîne de caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/SousTraitant/SousTraitantCollection.php b/thanasoft-back/app/Http/Resources/SousTraitant/SousTraitantCollection.php new file mode 100644 index 0000000..7f7fd91 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/SousTraitant/SousTraitantCollection.php @@ -0,0 +1,43 @@ + $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' => [ + 'actif' => $this->collection->where('statut', 'actif')->count(), + 'inactif' => $this->collection->where('statut', 'inactif')->count(), + 'en_evaluation' => $this->collection->where('statut', 'en_evaluation')->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' => 'Sous-traitants récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/SousTraitant/SousTraitantResource.php b/thanasoft-back/app/Http/Resources/SousTraitant/SousTraitantResource.php new file mode 100644 index 0000000..6639ed6 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/SousTraitant/SousTraitantResource.php @@ -0,0 +1,36 @@ + $this->id, + 'nom_entreprise' => $this->nom_entreprise, + 'siret' => $this->siret, + 'forme_juridique' => $this->forme_juridique, + 'code_ape' => $this->code_ape, + 'adresse' => $this->adresse, + 'contact_principal' => $this->contact_principal, + 'telephone' => $this->telephone, + 'email' => $this->email, + 'site_web' => $this->site_web, + 'numero_contrat' => $this->numero_contrat, + 'montant_contrat' => $this->montant_contrat, + 'date_debut_contrat' => $this->date_debut_contrat?->format('Y-m-d'), + 'date_fin_contrat' => $this->date_fin_contrat?->format('Y-m-d'), + 'type_prestation' => $this->type_prestation, + 'conditions_paiement' => $this->conditions_paiement, + 'statut' => $this->statut, + 'note_qualite' => $this->note_qualite, + 'certifications_labels' => $this->certifications_labels ?? [], + '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/SousTraitant.php b/thanasoft-back/app/Models/SousTraitant.php new file mode 100644 index 0000000..c658d8d --- /dev/null +++ b/thanasoft-back/app/Models/SousTraitant.php @@ -0,0 +1,42 @@ + 'decimal:2', + 'date_debut_contrat' => 'date', + 'date_fin_contrat' => 'date', + 'note_qualite' => 'decimal:1', + 'certifications_labels' => 'array', + ]; +} diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index 38d0e9c..5a7b445 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -48,6 +48,10 @@ class AppServiceProvider extends ServiceProvider return new \App\Repositories\FournisseurRepository($app->make(\App\Models\Fournisseur::class)); }); + $this->app->bind(\App\Repositories\SousTraitantRepositoryInterface::class, function ($app) { + return new \App\Repositories\SousTraitantRepository($app->make(\App\Models\SousTraitant::class)); + }); + $this->app->bind(\App\Repositories\ProductRepositoryInterface::class, function ($app) { return new \App\Repositories\ProductRepository($app->make(\App\Models\Product::class)); }); diff --git a/thanasoft-back/app/Repositories/SousTraitantRepository.php b/thanasoft-back/app/Repositories/SousTraitantRepository.php new file mode 100644 index 0000000..45e4a8d --- /dev/null +++ b/thanasoft-back/app/Repositories/SousTraitantRepository.php @@ -0,0 +1,53 @@ +model->newQuery(); + + if (!empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('nom_entreprise', 'like', '%' . $filters['search'] . '%') + ->orWhere('contact_principal', 'like', '%' . $filters['search'] . '%') + ->orWhere('email', 'like', '%' . $filters['search'] . '%') + ->orWhere('siret', 'like', '%' . $filters['search'] . '%') + ->orWhere('numero_contrat', 'like', '%' . $filters['search'] . '%') + ->orWhere('type_prestation', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (!empty($filters['statut'])) { + $query->where('statut', $filters['statut']); + } + + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + $query->orderBy($sortField, $sortDirection); + + return $query->paginate($perPage); + } + + public function searchByName(string $name) + { + return $this->model + ->newQuery() + ->where('nom_entreprise', 'like', '%' . $name . '%') + ->get(); + } +} diff --git a/thanasoft-back/app/Repositories/SousTraitantRepositoryInterface.php b/thanasoft-back/app/Repositories/SousTraitantRepositoryInterface.php new file mode 100644 index 0000000..83d8645 --- /dev/null +++ b/thanasoft-back/app/Repositories/SousTraitantRepositoryInterface.php @@ -0,0 +1,12 @@ +id(); + $table->string('nom_entreprise'); + $table->string('siret', 20)->nullable(); + $table->string('forme_juridique', 100)->nullable(); + $table->string('code_ape', 20)->nullable(); + $table->text('adresse')->nullable(); + $table->string('contact_principal'); + $table->string('telephone', 50)->nullable(); + $table->string('email', 191)->nullable(); + $table->string('site_web', 191)->nullable(); + $table->string('numero_contrat', 100)->nullable(); + $table->decimal('montant_contrat', 12, 2)->nullable(); + $table->date('date_debut_contrat')->nullable(); + $table->date('date_fin_contrat')->nullable(); + $table->string('type_prestation')->nullable(); + $table->text('conditions_paiement')->nullable(); + $table->enum('statut', ['actif', 'inactif', 'en_evaluation'])->default('actif'); + $table->decimal('note_qualite', 3, 1)->nullable(); + $table->json('certifications_labels')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sous_traitants'); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index 57a3429..7b536d7 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -8,6 +8,7 @@ use App\Http\Controllers\Api\ClientLocationController; use App\Http\Controllers\Api\ContactController; use App\Http\Controllers\Api\ClientCategoryController; use App\Http\Controllers\Api\FournisseurController; +use App\Http\Controllers\Api\SousTraitantController; use App\Http\Controllers\Api\ProductController; use App\Http\Controllers\Api\ProductCategoryController; use App\Http\Controllers\Api\EmployeeController; @@ -131,6 +132,8 @@ Route::middleware('auth:sanctum')->group(function () { Route::apiResource('purchase-orders', PurchaseOrderController::class); Route::apiResource('fournisseurs', FournisseurController::class); Route::get('fournisseurs/{fournisseurId}/contacts', [ContactController::class, 'getContactsByFournisseur']); + Route::get('/sous-traitants/searchBy', [SousTraitantController::class, 'searchBy']); + Route::apiResource('sous-traitants', SousTraitantController::class); // Product management Route::get('/products/searchBy', [ProductController::class, 'searchBy']);