diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientController.php b/thanasoft-back/app/Http/Controllers/Api/ClientController.php new file mode 100644 index 0000000..0e78717 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ClientController.php @@ -0,0 +1,156 @@ +clientRepository->all(); + return ClientResource::collection($clients); + } catch (\Exception $e) { + Log::error('Error fetching clients: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created client. + */ + public function store(StoreClientRequest $request): ClientResource|JsonResponse + { + try { + $client = $this->clientRepository->create($request->validated()); + return new ClientResource($client); + } catch (\Exception $e) { + Log::error('Error creating client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified client. + */ + public function show(string $id): ClientResource|JsonResponse + { + try { + $client = $this->clientRepository->find($id); + + if (!$client) { + return response()->json([ + 'message' => 'Client non trouvé.', + ], 404); + } + + return new ClientResource($client); + } catch (\Exception $e) { + Log::error('Error fetching client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified client. + */ + public function update(UpdateClientRequest $request, string $id): ClientResource|JsonResponse + { + try { + $updated = $this->clientRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Client non trouvé ou échec de la mise à jour.', + ], 404); + } + + $client = $this->clientRepository->find($id); + return new ClientResource($client); + } catch (\Exception $e) { + Log::error('Error updating client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified client. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->clientRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Client non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Client supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting client: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientGroupController.php b/thanasoft-back/app/Http/Controllers/Api/ClientGroupController.php new file mode 100644 index 0000000..8f34a93 --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ClientGroupController.php @@ -0,0 +1,156 @@ +clientGroupRepository->all(); + return ClientGroupResource::collection($clientGroups); + } catch (\Exception $e) { + Log::error('Error fetching client groups: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des groupes de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created client group. + */ + public function store(StoreClientGroupRequest $request): ClientGroupResource|JsonResponse + { + try { + $clientGroup = $this->clientGroupRepository->create($request->validated()); + return new ClientGroupResource($clientGroup); + } catch (\Exception $e) { + Log::error('Error creating client group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du groupe de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified client group. + */ + public function show(string $id): ClientGroupResource|JsonResponse + { + try { + $clientGroup = $this->clientGroupRepository->find($id); + + if (!$clientGroup) { + return response()->json([ + 'message' => 'Groupe de clients non trouvé.', + ], 404); + } + + return new ClientGroupResource($clientGroup); + } catch (\Exception $e) { + Log::error('Error fetching client group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_group_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du groupe de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified client group. + */ + public function update(UpdateClientGroupRequest $request, string $id): ClientGroupResource|JsonResponse + { + try { + $updated = $this->clientGroupRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Groupe de clients non trouvé ou échec de la mise à jour.', + ], 404); + } + + $clientGroup = $this->clientGroupRepository->find($id); + return new ClientGroupResource($clientGroup); + } catch (\Exception $e) { + Log::error('Error updating client group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_group_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du groupe de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified client group. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->clientGroupRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Groupe de clients non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Groupe de clients supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting client group: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_group_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du groupe de clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php b/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php new file mode 100644 index 0000000..f10614f --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ClientLocationController.php @@ -0,0 +1,156 @@ +clientLocationRepository->all(); + return ClientLocationResource::collection($clientLocations); + } catch (\Exception $e) { + Log::error('Error fetching client locations: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des lieux clients.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created client location. + */ + public function store(StoreClientLocationRequest $request): ClientLocationResource|JsonResponse + { + try { + $clientLocation = $this->clientLocationRepository->create($request->validated()); + return new ClientLocationResource($clientLocation); + } catch (\Exception $e) { + Log::error('Error creating client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified client location. + */ + public function show(string $id): ClientLocationResource|JsonResponse + { + try { + $clientLocation = $this->clientLocationRepository->find($id); + + if (!$clientLocation) { + return response()->json([ + 'message' => 'Lieu client non trouvé.', + ], 404); + } + + return new ClientLocationResource($clientLocation); + } catch (\Exception $e) { + Log::error('Error fetching client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_location_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified client location. + */ + public function update(UpdateClientLocationRequest $request, string $id): ClientLocationResource|JsonResponse + { + try { + $updated = $this->clientLocationRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Lieu client non trouvé ou échec de la mise à jour.', + ], 404); + } + + $clientLocation = $this->clientLocationRepository->find($id); + return new ClientLocationResource($clientLocation); + } catch (\Exception $e) { + Log::error('Error updating client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_location_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified client location. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->clientLocationRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Lieu client non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Lieu client supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting client location: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'client_location_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du lieu client.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/ContactController.php b/thanasoft-back/app/Http/Controllers/Api/ContactController.php new file mode 100644 index 0000000..cb9e4ed --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ContactController.php @@ -0,0 +1,156 @@ +contactRepository->all(); + return ContactResource::collection($contacts); + } catch (\Exception $e) { + Log::error('Error fetching contacts: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération des contacts.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Store a newly created contact. + */ + public function store(StoreContactRequest $request): ContactResource|JsonResponse + { + try { + $contact = $this->contactRepository->create($request->validated()); + return new ContactResource($contact); + } catch (\Exception $e) { + Log::error('Error creating contact: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la création du contact.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Display the specified contact. + */ + public function show(string $id): ContactResource|JsonResponse + { + try { + $contact = $this->contactRepository->find($id); + + if (!$contact) { + return response()->json([ + 'message' => 'Contact non trouvé.', + ], 404); + } + + return new ContactResource($contact); + } catch (\Exception $e) { + Log::error('Error fetching contact: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'contact_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la récupération du contact.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Update the specified contact. + */ + public function update(UpdateContactRequest $request, string $id): ContactResource|JsonResponse + { + try { + $updated = $this->contactRepository->update($id, $request->validated()); + + if (!$updated) { + return response()->json([ + 'message' => 'Contact non trouvé ou échec de la mise à jour.', + ], 404); + } + + $contact = $this->contactRepository->find($id); + return new ContactResource($contact); + } catch (\Exception $e) { + Log::error('Error updating contact: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'contact_id' => $id, + 'data' => $request->validated(), + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la mise à jour du contact.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + /** + * Remove the specified contact. + */ + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->contactRepository->delete($id); + + if (!$deleted) { + return response()->json([ + 'message' => 'Contact non trouvé ou échec de la suppression.', + ], 404); + } + + return response()->json([ + 'message' => 'Contact supprimé avec succès.', + ], 200); + } catch (\Exception $e) { + Log::error('Error deleting contact: ' . $e->getMessage(), [ + 'exception' => $e, + 'trace' => $e->getTraceAsString(), + 'contact_id' => $id, + ]); + + return response()->json([ + 'message' => 'Une erreur est survenue lors de la suppression du contact.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreClientGroupRequest.php b/thanasoft-back/app/Http/Requests/StoreClientGroupRequest.php new file mode 100644 index 0000000..01bc264 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreClientGroupRequest.php @@ -0,0 +1,40 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:191|unique:client_groups,name', + 'description' => 'nullable|string', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du groupe est obligatoire.', + 'name.string' => 'Le nom du groupe doit être une chaîne de caractères.', + 'name.max' => 'Le nom du groupe ne peut pas dépasser 191 caractères.', + 'name.unique' => 'Un groupe avec ce nom existe déjà.', + 'description.string' => 'La description doit être une chaîne de caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreClientLocationRequest.php b/thanasoft-back/app/Http/Requests/StoreClientLocationRequest.php new file mode 100644 index 0000000..88cff52 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreClientLocationRequest.php @@ -0,0 +1,68 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => 'required|exists:clients,id', + 'name' => 'nullable|string|max:191', + 'address_line1' => 'nullable|string|max:255', + 'address_line2' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'city' => 'nullable|string|max:191', + 'country_code' => 'nullable|string|size:2', + 'gps_lat' => 'nullable|numeric|between:-90,90', + 'gps_lng' => 'nullable|numeric|between:-180,180', + 'is_default' => 'boolean', + ]; + } + + public function messages(): array + { + return [ + 'client_id.required' => 'Le client est obligatoire.', + 'client_id.exists' => 'Le client sélectionné n\'existe pas.', + 'name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'gps_lat.numeric' => 'La latitude doit être un nombre.', + 'gps_lat.between' => 'La latitude doit être comprise entre -90 et 90.', + 'gps_lng.numeric' => 'La longitude doit être un nombre.', + 'gps_lng.between' => 'La longitude doit être comprise entre -180 et 180.', + 'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.', + ]; + } + + public function withValidator($validator) + { + $validator->after(function ($validator) { + if (empty($this->address_line1) && empty($this->postal_code) && empty($this->city)) { + $validator->errors()->add( + 'general', + 'Au moins un champ d\'adresse (adresse, code postal ou ville) doit être renseigné.' + ); + } + }); + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreClientRequest.php b/thanasoft-back/app/Http/Requests/StoreClientRequest.php new file mode 100644 index 0000000..882e59a --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreClientRequest.php @@ -0,0 +1,70 @@ +|string> + */ + public function rules(): array + { + return [ + + 'type' => 'required|in:pompes_funebres,famille,entreprise,collectivite,autre', + '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', + 'group_id' => 'nullable|exists:client_groups,id', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + 'default_tva_rate_id' => 'nullable|exists:tva_rates,id', + ]; + } + + + public function messages(): array + { + return [ + 'company_id.required' => 'La société est obligatoire.', + 'company_id.exists' => 'La société sélectionnée n\'existe pas.', + 'type.required' => 'Le type de client est obligatoire.', + 'type.in' => 'Le type de client sélectionné est invalide.', + 'name.required' => 'Le nom du client est obligatoire.', + 'name.string' => 'Le nom du client doit être une chaîne de caractères.', + 'name.max' => 'Le nom du client 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.', + 'group_id.exists' => 'Le groupe de clients sélectionné n\'existe pas.', + 'is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + 'default_tva_rate_id.exists' => 'Le taux de TVA sélectionné n\'existe pas.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreContactRequest.php b/thanasoft-back/app/Http/Requests/StoreContactRequest.php new file mode 100644 index 0000000..129f5a9 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreContactRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateClientGroupRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientGroupRequest.php new file mode 100644 index 0000000..18c31b0 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateClientGroupRequest.php @@ -0,0 +1,48 @@ +|string> + */ + public function rules(): array + { + $clientGroupId = $this->route('client_group') ? $this->route('client_group')->id : $this->route('id'); + + return [ + 'name' => [ + 'required', + 'string', + 'max:191', + Rule::unique('client_groups', 'name')->ignore($clientGroupId) + ], + 'description' => 'nullable|string', + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Le nom du groupe est obligatoire.', + 'name.string' => 'Le nom du groupe doit être une chaîne de caractères.', + 'name.max' => 'Le nom du groupe ne peut pas dépasser 191 caractères.', + 'name.unique' => 'Un groupe avec ce nom existe déjà.', + 'description.string' => 'La description doit être une chaîne de caractères.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateClientLocationRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientLocationRequest.php new file mode 100644 index 0000000..66606cf --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateClientLocationRequest.php @@ -0,0 +1,56 @@ +|string> + */ + public function rules(): array + { + return [ + 'client_id' => 'required|exists:clients,id', + 'name' => 'nullable|string|max:191', + 'address_line1' => 'nullable|string|max:255', + 'address_line2' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'city' => 'nullable|string|max:191', + 'country_code' => 'nullable|string|size:2', + 'gps_lat' => 'nullable|numeric|between:-90,90', + 'gps_lng' => 'nullable|numeric|between:-180,180', + 'is_default' => 'boolean', + ]; + } + + public function messages(): array + { + return [ + 'client_id.required' => 'Le client est obligatoire.', + 'client_id.exists' => 'Le client sélectionné n\'existe pas.', + 'name.max' => 'Le nom ne peut pas dépasser 191 caractères.', + 'address_line1.max' => 'L\'adresse ne peut pas dépasser 255 caractères.', + 'address_line2.max' => 'Le complément d\'adresse ne peut pas dépasser 255 caractères.', + 'postal_code.max' => 'Le code postal ne peut pas dépasser 20 caractères.', + 'city.max' => 'La ville ne peut pas dépasser 191 caractères.', + 'country_code.size' => 'Le code pays doit contenir 2 caractères.', + 'gps_lat.numeric' => 'La latitude doit être un nombre.', + 'gps_lat.between' => 'La latitude doit être comprise entre -90 et 90.', + 'gps_lng.numeric' => 'La longitude doit être un nombre.', + 'gps_lng.between' => 'La longitude doit être comprise entre -180 et 180.', + 'is_default.boolean' => 'Le statut par défaut doit être vrai ou faux.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateClientRequest.php b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php new file mode 100644 index 0000000..1c08317 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateClientRequest.php @@ -0,0 +1,68 @@ +|string> + */ + public function rules(): array + { + return [ + 'type' => 'required|in:pompes_funebres,famille,entreprise,collectivite,autre', + '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', + 'group_id' => 'nullable|exists:client_groups,id', + 'notes' => 'nullable|string', + 'is_active' => 'boolean', + 'default_tva_rate_id' => 'nullable|exists:tva_rates,id', + ]; + } + + public function messages(): array + { + return [ + 'company_id.required' => 'La société est obligatoire.', + 'company_id.exists' => 'La société sélectionnée n\'existe pas.', + 'type.required' => 'Le type de client est obligatoire.', + 'type.in' => 'Le type de client sélectionné est invalide.', + 'name.required' => 'Le nom du client est obligatoire.', + 'name.string' => 'Le nom du client doit être une chaîne de caractères.', + 'name.max' => 'Le nom du client 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.', + 'group_id.exists' => 'Le groupe de clients sélectionné n\'existe pas.', + 'is_active.boolean' => 'Le statut actif doit être vrai ou faux.', + 'default_tva_rate_id.exists' => 'Le taux de TVA sélectionné n\'existe pas.', + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateContactRequest.php b/thanasoft-back/app/Http/Requests/UpdateContactRequest.php new file mode 100644 index 0000000..204b7e5 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateContactRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientCollection.php b/thanasoft-back/app/Http/Resources/Client/ClientCollection.php new file mode 100644 index 0000000..7b2ced8 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientCollection.php @@ -0,0 +1,48 @@ + + */ + 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(), + 'by_type' => $this->collection->groupBy('type')->map->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' => 'Clients récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientGroupCollection.php b/thanasoft-back/app/Http/Resources/Client/ClientGroupCollection.php new file mode 100644 index 0000000..b53e721 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientGroupCollection.php @@ -0,0 +1,43 @@ + + */ + 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(), + ], + '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' => 'Groupes de clients récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientGroupResource.php b/thanasoft-back/app/Http/Resources/Client/ClientGroupResource.php new file mode 100644 index 0000000..c4e85cb --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientGroupResource.php @@ -0,0 +1,25 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'description' => $this->description ?? null, + '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/Http/Resources/Client/ClientLocationCollection.php b/thanasoft-back/app/Http/Resources/Client/ClientLocationCollection.php new file mode 100644 index 0000000..aa6fdd3 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientLocationCollection.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' => [ + 'default_locations' => $this->collection->where('is_default', true)->count(), + 'with_gps' => $this->collection->filter(fn($location) => $location->gps_lat && $location->gps_lng)->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' => 'Lieux clients récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientLocationResource.php b/thanasoft-back/app/Http/Resources/Client/ClientLocationResource.php new file mode 100644 index 0000000..f7ba37c --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientLocationResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/thanasoft-back/app/Http/Resources/Client/ClientResource.php b/thanasoft-back/app/Http/Resources/Client/ClientResource.php new file mode 100644 index 0000000..06f2b8d --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Client/ClientResource.php @@ -0,0 +1,62 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + //'company_id' => $this->company_id, + 'type' => $this->type, + 'type_label' => $this->getTypeLabel(), + '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, + ], + 'group_id' => $this->group_id, + 'notes' => $this->notes, + 'is_active' => $this->is_active, + // 'default_tva_rate_id' => $this->default_tva_rate_id, + 'created_at' => $this->created_at?->format('Y-m-d H:i:s'), + 'updated_at' => $this->updated_at?->format('Y-m-d H:i:s'), + + // Counts + 'contacts_count' => $this->whenCounted('contacts'), + 'locations_count' => $this->whenCounted('locations'), + // 'interventions_count' => $this->whenCounted('interventions'), + // 'quotes_count' => $this->whenCounted('quotes'), + // 'invoices_count' => $this->whenCounted('invoices'), + + // Relations + // 'company' => new CompanyResource($this->whenLoaded('company')), + 'group' => new ClientGroupResource($this->whenLoaded('group')), + // 'default_tva_rate' => new TvaRateResource($this->whenLoaded('defaultTvaRate')), + 'contacts' => ContactResource::collection($this->whenLoaded('contacts')), + 'locations' => ClientLocationResource::collection($this->whenLoaded('locations')), + // 'price_lists' => PriceListResource::collection($this->whenLoaded('priceLists')), + // 'interventions' => InterventionResource::collection($this->whenLoaded('interventions')), + // 'quotes' => QuoteResource::collection($this->whenLoaded('quotes')), + // 'invoices' => InvoiceResource::collection($this->whenLoaded('invoices')), + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Contact/ContactCollection.php b/thanasoft-back/app/Http/Resources/Contact/ContactCollection.php new file mode 100644 index 0000000..dd3b441 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Contact/ContactCollection.php @@ -0,0 +1,43 @@ + + */ + 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(), + ], + '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' => 'Contacts récupérés avec succès', + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Contact/ContactResource.php b/thanasoft-back/app/Http/Resources/Contact/ContactResource.php new file mode 100644 index 0000000..60f1ac4 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Contact/ContactResource.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/thanasoft-back/app/Models/Client.php b/thanasoft-back/app/Models/Client.php new file mode 100644 index 0000000..0f66ffd --- /dev/null +++ b/thanasoft-back/app/Models/Client.php @@ -0,0 +1,60 @@ + 'boolean', + ]; + + /** + * Get the human-readable label for the client type. + */ + public function getTypeLabel(): string + { + return match($this->type) { + 'pompes_funebres' => 'Pompes funèbres', + 'famille' => 'Famille', + 'entreprise' => 'Entreprise', + 'collectivite' => 'Collectivité', + 'autre' => 'Autre', + default => $this->type, + }; + } + + /** + * 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/Models/ClientContact.php b/thanasoft-back/app/Models/ClientContact.php new file mode 100644 index 0000000..20b6cc0 --- /dev/null +++ b/thanasoft-back/app/Models/ClientContact.php @@ -0,0 +1,10 @@ + 'boolean', + 'gps_lat' => 'decimal:8', + 'gps_lng' => 'decimal:8', + ]; +} diff --git a/thanasoft-back/app/Models/Contact.php b/thanasoft-back/app/Models/Contact.php new file mode 100644 index 0000000..11a03fa --- /dev/null +++ b/thanasoft-back/app/Models/Contact.php @@ -0,0 +1,23 @@ + 'boolean', + ]; +} diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index 452e6b6..8b54e4a 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -11,7 +11,26 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // + // Repository interface to implementation bindings + $this->app->bind(\App\Repositories\ClientRepositoryInterface::class, function ($app) { + return new \App\Repositories\ClientRepository($app->make(\App\Models\Client::class)); + }); + + $this->app->bind(\App\Repositories\ClientGroupRepositoryInterface::class, function ($app) { + return new \App\Repositories\ClientGroupRepository($app->make(\App\Models\ClientGroup::class)); + }); + + $this->app->bind(\App\Repositories\ClientContactRepositoryInterface::class, function ($app) { + return new \App\Repositories\ClientContactRepository($app->make(\App\Models\ClientContact::class)); + }); + + $this->app->bind(\App\Repositories\ContactRepositoryInterface::class, function ($app) { + return new \App\Repositories\ContactRepository($app->make(\App\Models\Contact::class)); + }); + + $this->app->bind(\App\Repositories\ClientLocationRepositoryInterface::class, function ($app) { + return new \App\Repositories\ClientLocationRepository($app->make(\App\Models\ClientLocation::class)); + }); } /** diff --git a/thanasoft-back/app/Repositories/BaseRepository.php b/thanasoft-back/app/Repositories/BaseRepository.php new file mode 100644 index 0000000..0d22279 --- /dev/null +++ b/thanasoft-back/app/Repositories/BaseRepository.php @@ -0,0 +1,72 @@ + $columns + * @return Collection + */ + public function all(array $columns = ['*']): Collection + { + return $this->model->newQuery()->get($columns); + } + + /** + * @param int|string $id + * @param array $columns + */ + public function find(int|string $id, array $columns = ['*']): ?Model + { + return $this->model->newQuery()->find($id, $columns); + } + + /** + * @param array $attributes + */ + public function create(array $attributes): Model + { + // Uses mass assignment; ensure $fillable is set on the model + return $this->model->newQuery()->create($attributes); + } + + /** + * @param int|string $id + * @param array $attributes + */ + public function update(int|string $id, array $attributes): bool + { + $instance = $this->find($id); + if (! $instance) { + return false; + } + + return $instance->fill($attributes)->save(); + } + + /** + * @param int|string $id + */ + public function delete(int|string $id): bool + { + $instance = $this->find($id); + if (! $instance) { + return false; + } + + return (bool) $instance->delete(); + } +} diff --git a/thanasoft-back/app/Repositories/BaseRepositoryInterface.php b/thanasoft-back/app/Repositories/BaseRepositoryInterface.php new file mode 100644 index 0000000..ea8688d --- /dev/null +++ b/thanasoft-back/app/Repositories/BaseRepositoryInterface.php @@ -0,0 +1,52 @@ + $columns + * @return Collection + */ + public function all(array $columns = ['*']): Collection; + + /** + * Find a record by its primary key. + * + * @param int|string $id + * @param array $columns + */ + public function find(int|string $id, array $columns = ['*']): ?Model; + + /** + * Create a new record with the given attributes. + * + * @param array $attributes + */ + public function create(array $attributes): Model; + + /** + * Update an existing record by id with the given attributes. + * + * @param int|string $id + * @param array $attributes + */ + public function update(int|string $id, array $attributes): bool; + + /** + * Delete a record by its primary key. + * + * @param int|string $id + */ + public function delete(int|string $id): bool; +} diff --git a/thanasoft-back/app/Repositories/ClientContactRepository.php b/thanasoft-back/app/Repositories/ClientContactRepository.php new file mode 100644 index 0000000..afd0b05 --- /dev/null +++ b/thanasoft-back/app/Repositories/ClientContactRepository.php @@ -0,0 +1,15 @@ +withMiddleware(function (Middleware $middleware): void { - // + $middleware->statefulApi(); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/thanasoft-back/database/migrations/2025_10_06_145345_create_personal_access_tokens_table.php b/thanasoft-back/database/migrations/2025_10_06_145345_create_personal_access_tokens_table.php deleted file mode 100644 index 40ff706..0000000 --- a/thanasoft-back/database/migrations/2025_10_06_145345_create_personal_access_tokens_table.php +++ /dev/null @@ -1,33 +0,0 @@ -id(); - $table->morphs('tokenable'); - $table->text('name'); - $table->string('token', 64)->unique(); - $table->text('abilities')->nullable(); - $table->timestamp('last_used_at')->nullable(); - $table->timestamp('expires_at')->nullable()->index(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('personal_access_tokens'); - } -}; diff --git a/thanasoft-back/database/migrations/2025_10_07_113450_create_client_groups_table.php b/thanasoft-back/database/migrations/2025_10_07_113450_create_client_groups_table.php new file mode 100644 index 0000000..4c7b073 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_07_113450_create_client_groups_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name', 191)->unique(); + $table->text('description')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('client_groups'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_07_113522_create_clients_table.php b/thanasoft-back/database/migrations/2025_10_07_113522_create_clients_table.php new file mode 100644 index 0000000..5fd7495 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_07_113522_create_clients_table.php @@ -0,0 +1,50 @@ +id(); + $table->enum('type', [ + 'pompes_funebres', + 'famille', + 'entreprise', + 'collectivite', + 'autre' + ])->default('pompes_funebres'); + $table->string('name', 255); + $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', 255)->nullable(); + $table->string('billing_address_line2', 255)->nullable(); + $table->string('billing_postal_code', 20)->nullable(); + $table->string('billing_city', 191)->nullable(); + $table->char('billing_country_code', 2)->default('FR'); + $table->foreignId('group_id')->nullable()->constrained('client_groups')->onDelete('set null'); + $table->text('notes')->nullable(); + $table->boolean('is_active')->default(true); + //$table->foreignId('default_tva_rate_id')->nullable()->constrained('tva_rates')->onDelete('set null'); + $table->timestamps(); + + $table->index(['group_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('clients'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_07_113551_create_contacts_table.php b/thanasoft-back/database/migrations/2025_10_07_113551_create_contacts_table.php new file mode 100644 index 0000000..e0f6528 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_07_113551_create_contacts_table.php @@ -0,0 +1,27 @@ +id(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contacts'); + } +}; diff --git a/thanasoft-back/database/migrations/2025_10_07_113647_create_client_locations_table.php b/thanasoft-back/database/migrations/2025_10_07_113647_create_client_locations_table.php new file mode 100644 index 0000000..dc54f12 --- /dev/null +++ b/thanasoft-back/database/migrations/2025_10_07_113647_create_client_locations_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('client_id')->constrained()->onDelete('cascade'); + $table->string('name', 191)->nullable(); + $table->string('address_line1', 255)->nullable(); + $table->string('address_line2', 255)->nullable(); + $table->string('postal_code', 20)->nullable(); + $table->string('city', 191)->nullable(); + $table->char('country_code', 2)->default('FR'); + $table->double('gps_lat')->nullable(); + $table->double('gps_lng')->nullable(); + $table->boolean('is_default')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('client_locations'); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index 62d4673..e4500b5 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -2,6 +2,10 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\Api\AuthController; +use App\Http\Controllers\Api\ClientController; +use App\Http\Controllers\Api\ClientGroupController; +use App\Http\Controllers\Api\ClientLocationController; +use App\Http\Controllers\Api\ContactController; /* |-------------------------------------------------------------------------- @@ -25,3 +29,14 @@ Route::prefix('auth')->group(function () { Route::post('/logout-all', [AuthController::class, 'logoutAll']); }); }); + +// Protected API routes +Route::middleware('auth:sanctum')->group(function () { + // Client management + Route::apiResource('clients', ClientController::class); + Route::apiResource('client-groups', ClientGroupController::class); + Route::apiResource('client-locations', ClientLocationController::class); + + // Contact management + Route::apiResource('contacts', ContactController::class); +});