From 284d228dc54d303c02cc4582d882af3dd9acc04f Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 15 Apr 2026 17:27:40 +0300 Subject: [PATCH] feat(vehicle): add vehicle+convoy feature with API, models, repos, UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds model, repo, controller, and request classes for Vehicle and Convoy. Registers routes for vehicles and convoys, updates client store. Adds front‑end files to list, add, edit vehicles. Cleans up console logging from client store. --- Convoi.json | 291 +++++++++ .../Http/Controllers/Api/ConvoyController.php | 144 ++++ .../Controllers/Api/VehicleController.php | 144 ++++ .../app/Http/Requests/StoreConvoyRequest.php | 41 ++ .../app/Http/Requests/StoreVehicleRequest.php | 33 + .../app/Http/Requests/UpdateConvoyRequest.php | 51 ++ .../Http/Requests/UpdateVehicleRequest.php | 35 + .../Http/Resources/Convoy/ConvoyResource.php | 79 +++ .../Resources/Vehicle/VehicleResource.php | 42 ++ thanasoft-back/app/Models/Client.php | 5 + thanasoft-back/app/Models/Convoy.php | 64 ++ thanasoft-back/app/Models/Deceased.php | 5 + thanasoft-back/app/Models/Employee.php | 5 + thanasoft-back/app/Models/Vehicle.php | 44 ++ .../app/Providers/AppServiceProvider.php | 2 + .../app/Repositories/ConvoyRepository.php | 51 ++ .../ConvoyRepositoryInterface.php | 12 + .../app/Repositories/VehicleRepository.php | 42 ++ .../VehicleRepositoryInterface.php | 12 + ...21_create_convois_and_vehicules_tables.php | 77 +++ thanasoft-back/routes/api.php | 4 + .../CRM/lieux/ListeLieuxPresentation.vue | 11 +- .../Employee/AddVehiclePresentation.vue | 40 ++ .../Employee/VehicleDetailPresentation.vue | 538 +++++++++++++++ .../Organism/Employee/VehiclePresentation.vue | 70 ++ .../Tables/Employees/VehicleTable.vue | 273 ++++++++ .../molecules/form/NewVehicleForm.vue | 383 +++++++++++ .../templates/Employee/NewVehicleTemplate.vue | 20 + .../templates/Employee/VehicleTemplate.vue | 22 + thanasoft-front/src/router/index.js | 20 + thanasoft-front/src/services/vehicle.ts | 122 ++++ thanasoft-front/src/stores/clientStore.ts | 9 +- thanasoft-front/src/stores/vehicleStore.ts | 203 ++++++ .../src/views/pages/CRM/AddLocation.vue | 287 ++++++++ .../src/views/pages/Employes/AddVehicle.vue | 54 ++ .../views/pages/Employes/VehicleDetails.vue | 47 ++ .../src/views/pages/Employes/Vehicules.vue | 32 +- thanasoft-front/tsconfig.tsbuildinfo | 613 +++++++++--------- 38 files changed, 3624 insertions(+), 303 deletions(-) create mode 100644 Convoi.json create mode 100644 thanasoft-back/app/Http/Controllers/Api/ConvoyController.php create mode 100644 thanasoft-back/app/Http/Controllers/Api/VehicleController.php create mode 100644 thanasoft-back/app/Http/Requests/StoreConvoyRequest.php create mode 100644 thanasoft-back/app/Http/Requests/StoreVehicleRequest.php create mode 100644 thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php create mode 100644 thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php create mode 100644 thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php create mode 100644 thanasoft-back/app/Http/Resources/Vehicle/VehicleResource.php create mode 100644 thanasoft-back/app/Models/Convoy.php create mode 100644 thanasoft-back/app/Models/Vehicle.php create mode 100644 thanasoft-back/app/Repositories/ConvoyRepository.php create mode 100644 thanasoft-back/app/Repositories/ConvoyRepositoryInterface.php create mode 100644 thanasoft-back/app/Repositories/VehicleRepository.php create mode 100644 thanasoft-back/app/Repositories/VehicleRepositoryInterface.php create mode 100644 thanasoft-back/database/migrations/2026_04_15_111321_create_convois_and_vehicules_tables.php create mode 100644 thanasoft-front/src/components/Organism/Employee/AddVehiclePresentation.vue create mode 100644 thanasoft-front/src/components/Organism/Employee/VehicleDetailPresentation.vue create mode 100644 thanasoft-front/src/components/Organism/Employee/VehiclePresentation.vue create mode 100644 thanasoft-front/src/components/molecules/Tables/Employees/VehicleTable.vue create mode 100644 thanasoft-front/src/components/molecules/form/NewVehicleForm.vue create mode 100644 thanasoft-front/src/components/templates/Employee/NewVehicleTemplate.vue create mode 100644 thanasoft-front/src/components/templates/Employee/VehicleTemplate.vue create mode 100644 thanasoft-front/src/services/vehicle.ts create mode 100644 thanasoft-front/src/stores/vehicleStore.ts create mode 100644 thanasoft-front/src/views/pages/CRM/AddLocation.vue create mode 100644 thanasoft-front/src/views/pages/Employes/AddVehicle.vue create mode 100644 thanasoft-front/src/views/pages/Employes/VehicleDetails.vue diff --git a/Convoi.json b/Convoi.json new file mode 100644 index 0000000..703821f --- /dev/null +++ b/Convoi.json @@ -0,0 +1,291 @@ +{ + "defuntId": null, + "titreMission": "", + "clientId": null, + "typeConvoi": "local", + "lieuDepart": { + "modeSelection": "lieu", + "lieuId": null, + "nom": "", + "adresse": "", + "ville": "", + "codePostal": "", + "pays": "", + "latitude": null, + "longitude": null, + "detailsComplementaires": "" + }, + "modeTransport": "route", + "debutPrevu": "2026-04-15T10:57", + "finEstimee": null, + "statut": "planifie", + "emailFamille": "", + "notificationsAutomatiques": true, + "ongletsMission": { + "itineraire": { + "enabled": false + }, + "documentsLegaux": { + "enabled": false + }, + "equipeMoyens": { + "enabled": true + }, + "demarches": { + "enabled": true + }, + "suiviCouts": { + "enabled": true + }, + "carburant": { + "enabled": true + }, + "ceremonie": { + "enabled": true + }, + "thanatopraxie": { + "enabled": true + }, + "suiviGpsEtapes": { + "enabled": false + }, + "communication": { + "enabled": true + } + } +} + + +// VEHICULE +{ + "attributMissionConvoi": { + "defuntId": null, + "titreMission": "", + "clientId": null, + "typeConvoi": "local", + "lieuDepart": { + "modeSelection": "lieu", + "lieuId": null, + "nom": "", + "adresse": "", + "ville": "", + "codePostal": "", + "pays": "", + "latitude": null, + "longitude": null, + "detailsComplementaires": "" + }, + "modeTransport": "route", + "debutPrevu": "2026-04-15T10:57", + "finEstimee": null, + "statut": "planifie", + "emailFamille": "", + "notificationsAutomatiques": true, + "ongletsMission": { + "itineraire": false, + "documentsLegaux": false, + "equipeMoyens": true, + "demarches": true, + "suiviCouts": true, + "carburant": true, + "ceremonie": true, + "thanatopraxie": true, + "suiviGpsEtapes": false, + "communication": true + } + }, + "attributVehicule": { + "photoVehicule": { + "fileName": "", + "fileUrl": "", + "mimeType": "", + "size": null + }, + "marque": "", + "modele": "", + "immatriculation": "", + "typeVehicule": "utilitaire", + "carburant": "diesel", + "annee": 2026, + "utilisateurPrincipalId": null, + "statut": "actif", + "notes": "", + "ongletsVehicule": { + "informationsGenerales": true, + "entretienMaintenance": true, + "coutsAcquisition": true + } + } +} + +{ + "attributMissionConvoi": { + "defunt": { + "label": "Défunt", + "required": true, + "type": "select", + "value": null + }, + "titreMission": { + "label": "Titre de la mission", + "required": false, + "type": "string", + "value": "" + }, + "client": { + "label": "Client (Donneur d'ordre)", + "required": false, + "type": "select", + "value": null + }, + "typeConvoi": { + "label": "Type de convoi", + "required": false, + "type": "select", + "value": "Local" + }, + "lieuDepartConvoi": { + "label": "Lieu de Départ du Convoi", + "required": false, + "type": "object", + "value": { + "mode": "selection_lieu", + "recherche": "", + "lieuId": null, + "adresseManuelle": null + } + }, + "modeTransport": { + "label": "Mode de Transport", + "required": false, + "type": "select", + "value": "Route" + }, + "debutPrevu": { + "label": "Début Prévu", + "required": true, + "type": "datetime-local", + "value": "2026-04-15T10:57" + }, + "finEstimee": { + "label": "Fin Estimée", + "required": false, + "type": "datetime-local", + "value": null + }, + "statut": { + "label": "Statut", + "required": false, + "type": "select", + "value": "Planifié" + }, + "emailFamille": { + "label": "Email famille (pour notifications auto)", + "required": false, + "type": "email", + "value": "" + }, + "notificationsAutomatiques": { + "label": "Notifications automatiques (départ, arrivée, frontière)", + "required": false, + "type": "boolean", + "value": true + } + }, + "attributVehicule": { + "photoVehicule": { + "label": "Photo du véhicule", + "required": false, + "type": "file:image", + "value": null + }, + "marque": { + "label": "Marque", + "required": true, + "type": "select", + "value": null, + "options": [ + "Mercedes-Benz", + "Peugeot", + "Renault", + "Citroën", + "Volkswagen", + "Ford", + "Fiat", + "Opel", + "Toyota", + "Nissan", + "Volvo", + "BMW", + "Audi", + "Iveco", + "Autre" + ] + }, + "modele": { + "label": "Modèle", + "required": true, + "type": "string", + "value": "" + }, + "immatriculation": { + "label": "Immatriculation", + "required": true, + "type": "string", + "value": "" + }, + "typeVehicule": { + "label": "Type véhicule", + "required": false, + "type": "select", + "value": "utilitaire", + "options": [ + "corbillard", + "vehicule_transport", + "utilitaire", + "berline" + ] + }, + "carburant": { + "label": "Carburant", + "required": false, + "type": "select", + "value": "diesel", + "options": [ + "diesel", + "essence", + "electrique", + "hybride" + ] + }, + "annee": { + "label": "Année", + "required": false, + "type": "number", + "value": 2026 + }, + "utilisateurPrincipal": { + "label": "Utilisateur principal", + "required": false, + "type": "select", + "value": null + }, + "statutVehicule": { + "label": "Statut", + "required": false, + "type": "select", + "value": "actif", + "options": [ + "actif", + "en_maintenance", + "hors_service" + ] + }, + "notes": { + "label": "Notes", + "required": false, + "type": "textarea", + "value": "" + } + } +} \ No newline at end of file diff --git a/thanasoft-back/app/Http/Controllers/Api/ConvoyController.php b/thanasoft-back/app/Http/Controllers/Api/ConvoyController.php new file mode 100644 index 0000000..20eb42a --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/ConvoyController.php @@ -0,0 +1,144 @@ +convoyRepository->paginate( + (int) $request->integer('per_page', 15), + $request->only(['search', 'status', 'convoy_type', 'vehicle_id', 'deceased_id', 'sort_by', 'sort_direction']) + ); + + return response()->json([ + 'data' => ConvoyResource::collection($convoys->items()), + 'meta' => [ + 'current_page' => $convoys->currentPage(), + 'last_page' => $convoys->lastPage(), + 'per_page' => $convoys->perPage(), + 'total' => $convoys->total(), + ], + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error fetching convoys: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while fetching convoys.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function store(StoreConvoyRequest $request): JsonResponse + { + try { + $convoy = $this->convoyRepository->create($request->validated()); + + return response()->json([ + 'data' => new ConvoyResource($convoy->load(['deceased', 'client', 'vehicle', 'departureLocation'])), + 'message' => 'Convoy created successfully.', + 'status' => 'success', + ], 201); + } catch (\Exception $e) { + Log::error('Error creating convoy: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while creating the convoy.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function show(string $id): JsonResponse + { + try { + $convoy = $this->convoyRepository->find((int) $id); + + if (! $convoy) { + return response()->json(['message' => 'Convoy not found.'], 404); + } + + $convoy->load(['deceased', 'client', 'vehicle', 'departureLocation']); + + return response()->json([ + 'data' => new ConvoyResource($convoy), + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error fetching convoy: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while fetching the convoy.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function update(UpdateConvoyRequest $request, string $id): JsonResponse + { + try { + $updated = $this->convoyRepository->update((int) $id, $request->validated()); + + if (! $updated) { + return response()->json(['message' => 'Convoy not found or update failed.'], 404); + } + + $convoy = $this->convoyRepository->find((int) $id); + + return response()->json([ + 'data' => new ConvoyResource($convoy->load(['deceased', 'client', 'vehicle', 'departureLocation'])), + 'message' => 'Convoy updated successfully.', + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error updating convoy: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while updating the convoy.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->convoyRepository->delete((int) $id); + + if (! $deleted) { + return response()->json(['message' => 'Convoy not found or delete failed.'], 404); + } + + return response()->json([ + 'message' => 'Convoy deleted successfully.', + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error deleting convoy: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while deleting the convoy.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Controllers/Api/VehicleController.php b/thanasoft-back/app/Http/Controllers/Api/VehicleController.php new file mode 100644 index 0000000..ac24ddb --- /dev/null +++ b/thanasoft-back/app/Http/Controllers/Api/VehicleController.php @@ -0,0 +1,144 @@ +vehicleRepository->paginate( + (int) $request->integer('per_page', 15), + $request->only(['search', 'status', 'vehicle_type', 'sort_by', 'sort_direction']) + ); + + return response()->json([ + 'data' => VehicleResource::collection($vehicles->items()), + 'meta' => [ + 'current_page' => $vehicles->currentPage(), + 'last_page' => $vehicles->lastPage(), + 'per_page' => $vehicles->perPage(), + 'total' => $vehicles->total(), + ], + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error fetching vehicles: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while fetching vehicles.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function store(StoreVehicleRequest $request): JsonResponse + { + try { + $vehicle = $this->vehicleRepository->create($request->validated()); + + return response()->json([ + 'data' => new VehicleResource($vehicle->load('primaryUser')), + 'message' => 'Vehicle created successfully.', + 'status' => 'success', + ], 201); + } catch (\Exception $e) { + Log::error('Error creating vehicle: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while creating the vehicle.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function show(string $id): JsonResponse + { + try { + $vehicle = $this->vehicleRepository->find((int) $id); + + if (! $vehicle) { + return response()->json(['message' => 'Vehicle not found.'], 404); + } + + $vehicle->load(['primaryUser', 'convoys']); + + return response()->json([ + 'data' => new VehicleResource($vehicle), + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error fetching vehicle: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while fetching the vehicle.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function update(UpdateVehicleRequest $request, string $id): JsonResponse + { + try { + $updated = $this->vehicleRepository->update((int) $id, $request->validated()); + + if (! $updated) { + return response()->json(['message' => 'Vehicle not found or update failed.'], 404); + } + + $vehicle = $this->vehicleRepository->find((int) $id); + + return response()->json([ + 'data' => new VehicleResource($vehicle->load('primaryUser')), + 'message' => 'Vehicle updated successfully.', + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error updating vehicle: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while updating the vehicle.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } + + public function destroy(string $id): JsonResponse + { + try { + $deleted = $this->vehicleRepository->delete((int) $id); + + if (! $deleted) { + return response()->json(['message' => 'Vehicle not found or delete failed.'], 404); + } + + return response()->json([ + 'message' => 'Vehicle deleted successfully.', + 'status' => 'success', + ]); + } catch (\Exception $e) { + Log::error('Error deleting vehicle: ' . $e->getMessage()); + + return response()->json([ + 'message' => 'An error occurred while deleting the vehicle.', + 'error' => config('app.debug') ? $e->getMessage() : null, + ], 500); + } + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreConvoyRequest.php b/thanasoft-back/app/Http/Requests/StoreConvoyRequest.php new file mode 100644 index 0000000..6faa002 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreConvoyRequest.php @@ -0,0 +1,41 @@ + ['required', 'exists:deceased,id'], + 'client_id' => ['nullable', 'exists:clients,id'], + 'vehicle_id' => ['nullable', 'exists:vehicles,id'], + 'mission_title' => ['nullable', 'string', 'max:255'], + 'convoy_type' => ['nullable', Rule::in(['local', 'national', 'international'])], + 'transport_mode' => ['nullable', Rule::in(['road', 'air', 'sea', 'rail'])], + 'status' => ['nullable', Rule::in(['planned', 'in_progress', 'completed', 'cancelled'])], + 'planned_start_at' => ['required', 'date'], + 'estimated_end_at' => ['nullable', 'date', 'after_or_equal:planned_start_at'], + 'family_email' => ['nullable', 'email', 'max:255'], + 'automatic_notifications' => ['nullable', 'boolean'], + 'departure_location_selection_mode' => ['nullable', Rule::in(['place', 'manual'])], + 'departure_location_id' => ['nullable', 'exists:client_locations,id'], + 'departure_name' => ['nullable', 'string', 'max:255'], + 'departure_address' => ['nullable', 'string', 'max:255'], + 'departure_city' => ['nullable', 'string', 'max:255'], + 'departure_postal_code' => ['nullable', 'string', 'max:20'], + 'departure_country_code' => ['nullable', 'string', 'size:2'], + 'departure_latitude' => ['nullable', 'numeric', 'between:-90,90'], + 'departure_longitude' => ['nullable', 'numeric', 'between:-180,180'], + 'departure_additional_details' => ['nullable', 'string'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/StoreVehicleRequest.php b/thanasoft-back/app/Http/Requests/StoreVehicleRequest.php new file mode 100644 index 0000000..a94f251 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/StoreVehicleRequest.php @@ -0,0 +1,33 @@ + ['nullable', 'string', 'max:255'], + 'photo_file_url' => ['nullable', 'string', 'max:2048'], + 'photo_mime_type' => ['nullable', 'string', 'max:100'], + 'photo_size' => ['nullable', 'integer', 'min:0'], + 'brand' => ['required', 'string', 'max:255'], + 'model' => ['required', 'string', 'max:255'], + 'registration_number' => ['required', 'string', 'max:255', 'unique:vehicles,registration_number'], + 'vehicle_type' => ['nullable', Rule::in(['hearse', 'transport_vehicle', 'utility', 'sedan'])], + 'fuel_type' => ['nullable', Rule::in(['diesel', 'petrol', 'electric', 'hybrid'])], + 'year' => ['nullable', 'integer', 'min:1900', 'max:2100'], + 'primary_user_id' => ['nullable', 'exists:employees,id'], + 'status' => ['nullable', Rule::in(['active', 'maintenance', 'out_of_service'])], + 'notes' => ['nullable', 'string'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php b/thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php new file mode 100644 index 0000000..240064e --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php @@ -0,0 +1,51 @@ + ['sometimes', 'required', 'exists:deceased,id'], + 'client_id' => ['nullable', 'exists:clients,id'], + 'vehicle_id' => ['nullable', 'exists:vehicles,id'], + 'mission_title' => ['nullable', 'string', 'max:255'], + 'convoy_type' => ['nullable', Rule::in(['local', 'national', 'international'])], + 'transport_mode' => ['nullable', Rule::in(['road', 'air', 'sea', 'rail'])], + 'status' => ['nullable', Rule::in(['planned', 'in_progress', 'completed', 'cancelled'])], + 'planned_start_at' => ['sometimes', 'required', 'date'], + 'estimated_end_at' => ['nullable', 'date'], + 'family_email' => ['nullable', 'email', 'max:255'], + 'automatic_notifications' => ['nullable', 'boolean'], + 'departure_location_selection_mode' => ['nullable', Rule::in(['place', 'manual'])], + 'departure_location_id' => ['nullable', 'exists:client_locations,id'], + 'departure_name' => ['nullable', 'string', 'max:255'], + 'departure_address' => ['nullable', 'string', 'max:255'], + 'departure_city' => ['nullable', 'string', 'max:255'], + 'departure_postal_code' => ['nullable', 'string', 'max:20'], + 'departure_country_code' => ['nullable', 'string', 'size:2'], + 'departure_latitude' => ['nullable', 'numeric', 'between:-90,90'], + 'departure_longitude' => ['nullable', 'numeric', 'between:-180,180'], + 'departure_additional_details' => ['nullable', 'string'], + 'tab_itinerary' => ['nullable', 'boolean'], + 'tab_legal_documents' => ['nullable', 'boolean'], + 'tab_team_resources' => ['nullable', 'boolean'], + 'tab_procedures' => ['nullable', 'boolean'], + 'tab_cost_tracking' => ['nullable', 'boolean'], + 'tab_fuel' => ['nullable', 'boolean'], + 'tab_ceremony' => ['nullable', 'boolean'], + 'tab_thanatopraxy' => ['nullable', 'boolean'], + 'tab_gps_tracking_steps' => ['nullable', 'boolean'], + 'tab_communication' => ['nullable', 'boolean'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php b/thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php new file mode 100644 index 0000000..44a1fd3 --- /dev/null +++ b/thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php @@ -0,0 +1,35 @@ +route('vehicle') ?? $this->route('id'); + + return [ + 'photo_file_name' => ['nullable', 'string', 'max:255'], + 'photo_file_url' => ['nullable', 'string', 'max:2048'], + 'photo_mime_type' => ['nullable', 'string', 'max:100'], + 'photo_size' => ['nullable', 'integer', 'min:0'], + 'brand' => ['sometimes', 'required', 'string', 'max:255'], + 'model' => ['sometimes', 'required', 'string', 'max:255'], + 'registration_number' => ['sometimes', 'required', 'string', 'max:255', Rule::unique('vehicles', 'registration_number')->ignore($vehicleId)], + 'vehicle_type' => ['nullable', Rule::in(['hearse', 'transport_vehicle', 'utility', 'sedan'])], + 'fuel_type' => ['nullable', Rule::in(['diesel', 'petrol', 'electric', 'hybrid'])], + 'year' => ['nullable', 'integer', 'min:1900', 'max:2100'], + 'primary_user_id' => ['nullable', 'exists:employees,id'], + 'status' => ['nullable', Rule::in(['active', 'maintenance', 'out_of_service'])], + 'notes' => ['nullable', 'string'], + ]; + } +} diff --git a/thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php b/thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php new file mode 100644 index 0000000..3f8bae5 --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php @@ -0,0 +1,79 @@ + $this->id, + 'deceased_id' => $this->deceased_id, + 'client_id' => $this->client_id, + 'vehicle_id' => $this->vehicle_id, + 'mission_title' => $this->mission_title, + 'convoy_type' => $this->convoy_type, + 'transport_mode' => $this->transport_mode, + 'status' => $this->status, + 'planned_start_at' => $this->planned_start_at?->format('Y-m-d H:i:s'), + 'estimated_end_at' => $this->estimated_end_at?->format('Y-m-d H:i:s'), + 'family_email' => $this->family_email, + 'automatic_notifications' => $this->automatic_notifications, + 'departure' => [ + 'location_selection_mode' => $this->departure_location_selection_mode, + 'location_id' => $this->departure_location_id, + 'location' => $this->whenLoaded('departureLocation', function () { + return $this->departureLocation ? [ + 'id' => $this->departureLocation->id, + 'client_id' => $this->departureLocation->client_id, + 'name' => $this->departureLocation->name, + 'address_line1' => $this->departureLocation->address_line1, + 'address_line2' => $this->departureLocation->address_line2, + 'postal_code' => $this->departureLocation->postal_code, + 'city' => $this->departureLocation->city, + 'country_code' => $this->departureLocation->country_code, + 'gps_lat' => $this->departureLocation->gps_lat, + 'gps_lng' => $this->departureLocation->gps_lng, + ] : null; + }), + 'name' => $this->departure_name, + 'address' => $this->departure_address, + 'city' => $this->departure_city, + 'postal_code' => $this->departure_postal_code, + 'country_code' => $this->departure_country_code, + 'latitude' => $this->departure_latitude, + 'longitude' => $this->departure_longitude, + 'additional_details' => $this->departure_additional_details, + ], + 'tabs' => [ + 'itinerary' => $this->tab_itinerary, + 'legal_documents' => $this->tab_legal_documents, + 'team_resources' => $this->tab_team_resources, + 'procedures' => $this->tab_procedures, + 'cost_tracking' => $this->tab_cost_tracking, + 'fuel' => $this->tab_fuel, + 'ceremony' => $this->tab_ceremony, + 'thanatopraxy' => $this->tab_thanatopraxy, + 'gps_tracking_steps' => $this->tab_gps_tracking_steps, + 'communication' => $this->tab_communication, + ], + 'deceased' => $this->whenLoaded('deceased', function () { + return new DeceasedResource($this->deceased); + }), + 'client' => $this->whenLoaded('client', function () { + return $this->client ? new ClientResource($this->client) : null; + }), + 'vehicle' => $this->whenLoaded('vehicle', function () { + return $this->vehicle ? new VehicleResource($this->vehicle) : 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/Vehicle/VehicleResource.php b/thanasoft-back/app/Http/Resources/Vehicle/VehicleResource.php new file mode 100644 index 0000000..002074a --- /dev/null +++ b/thanasoft-back/app/Http/Resources/Vehicle/VehicleResource.php @@ -0,0 +1,42 @@ + $this->id, + 'photo' => [ + 'file_name' => $this->photo_file_name, + 'file_url' => $this->photo_file_url, + 'mime_type' => $this->photo_mime_type, + 'size' => $this->photo_size, + ], + 'brand' => $this->brand, + 'model' => $this->model, + 'registration_number' => $this->registration_number, + 'vehicle_type' => $this->vehicle_type, + 'fuel_type' => $this->fuel_type, + 'year' => $this->year, + 'status' => $this->status, + 'notes' => $this->notes, + 'primary_user_id' => $this->primary_user_id, + 'primary_user' => $this->whenLoaded('primaryUser', function () { + return $this->primaryUser ? [ + 'id' => $this->primaryUser->id, + 'first_name' => $this->primaryUser->first_name, + 'last_name' => $this->primaryUser->last_name, + 'full_name' => $this->primaryUser->full_name, + 'email' => $this->primaryUser->email, + ] : 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/Models/Client.php b/thanasoft-back/app/Models/Client.php index 16e8b2e..2e7ad2f 100644 --- a/thanasoft-back/app/Models/Client.php +++ b/thanasoft-back/app/Models/Client.php @@ -66,6 +66,11 @@ class Client extends Model return $this->belongsTo(ClientGroup::class, 'group_id'); } + public function convoys() + { + return $this->hasMany(Convoy::class); + } + /** * Get the human-readable label for the client type. */ diff --git a/thanasoft-back/app/Models/Convoy.php b/thanasoft-back/app/Models/Convoy.php new file mode 100644 index 0000000..4683ced --- /dev/null +++ b/thanasoft-back/app/Models/Convoy.php @@ -0,0 +1,64 @@ + 'datetime', + 'estimated_end_at' => 'datetime', + 'automatic_notifications' => 'boolean', + 'departure_latitude' => 'decimal:7', + 'departure_longitude' => 'decimal:7', + ]; + + public function deceased(): BelongsTo + { + return $this->belongsTo(Deceased::class); + } + + public function client(): BelongsTo + { + return $this->belongsTo(Client::class); + } + + public function vehicle(): BelongsTo + { + return $this->belongsTo(Vehicle::class); + } + + public function departureLocation(): BelongsTo + { + return $this->belongsTo(ClientLocation::class, 'departure_location_id'); + } +} diff --git a/thanasoft-back/app/Models/Deceased.php b/thanasoft-back/app/Models/Deceased.php index 98f8515..51ce87e 100644 --- a/thanasoft-back/app/Models/Deceased.php +++ b/thanasoft-back/app/Models/Deceased.php @@ -57,6 +57,11 @@ class Deceased extends Model return $this->hasMany(Intervention::class); } + public function convoys(): HasMany + { + return $this->hasMany(Convoy::class); + } + /** * Get the file attachments for the deceased (polymorphic). */ diff --git a/thanasoft-back/app/Models/Employee.php b/thanasoft-back/app/Models/Employee.php index 8550797..5adb6f5 100644 --- a/thanasoft-back/app/Models/Employee.php +++ b/thanasoft-back/app/Models/Employee.php @@ -53,6 +53,11 @@ class Employee extends Model return $this->belongsTo(User::class); } + public function vehicles(): HasMany + { + return $this->hasMany(Vehicle::class, 'primary_user_id'); + } + /** * Get the full name of the employee. */ diff --git a/thanasoft-back/app/Models/Vehicle.php b/thanasoft-back/app/Models/Vehicle.php new file mode 100644 index 0000000..bbe7c0e --- /dev/null +++ b/thanasoft-back/app/Models/Vehicle.php @@ -0,0 +1,44 @@ + 'integer', + 'year' => 'integer', + ]; + + public function primaryUser(): BelongsTo + { + return $this->belongsTo(Employee::class, 'primary_user_id'); + } + + public function convoys(): HasMany + { + return $this->hasMany(Convoy::class); + } +} diff --git a/thanasoft-back/app/Providers/AppServiceProvider.php b/thanasoft-back/app/Providers/AppServiceProvider.php index 97ea76e..53eb5c6 100644 --- a/thanasoft-back/app/Providers/AppServiceProvider.php +++ b/thanasoft-back/app/Providers/AppServiceProvider.php @@ -116,6 +116,8 @@ class AppServiceProvider extends ServiceProvider $this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::class); $this->app->bind(\App\Repositories\DeceasedDocumentRepositoryInterface::class, \App\Repositories\DeceasedDocumentRepository::class); + $this->app->bind(\App\Repositories\VehicleRepositoryInterface::class, \App\Repositories\VehicleRepository::class); + $this->app->bind(\App\Repositories\ConvoyRepositoryInterface::class, \App\Repositories\ConvoyRepository::class); } diff --git a/thanasoft-back/app/Repositories/ConvoyRepository.php b/thanasoft-back/app/Repositories/ConvoyRepository.php new file mode 100644 index 0000000..8e648ad --- /dev/null +++ b/thanasoft-back/app/Repositories/ConvoyRepository.php @@ -0,0 +1,51 @@ +model->newQuery()->with(['deceased', 'client', 'vehicle', 'departureLocation']); + + if (! empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('mission_title', 'like', '%' . $filters['search'] . '%') + ->orWhere('family_email', 'like', '%' . $filters['search'] . '%') + ->orWhere('departure_name', 'like', '%' . $filters['search'] . '%') + ->orWhere('departure_city', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (! empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (! empty($filters['convoy_type'])) { + $query->where('convoy_type', $filters['convoy_type']); + } + + if (! empty($filters['vehicle_id'])) { + $query->where('vehicle_id', $filters['vehicle_id']); + } + + if (! empty($filters['deceased_id'])) { + $query->where('deceased_id', $filters['deceased_id']); + } + + $sortField = $filters['sort_by'] ?? 'planned_start_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + + return $query->orderBy($sortField, $sortDirection)->paginate($perPage); + } +} diff --git a/thanasoft-back/app/Repositories/ConvoyRepositoryInterface.php b/thanasoft-back/app/Repositories/ConvoyRepositoryInterface.php new file mode 100644 index 0000000..7bd753c --- /dev/null +++ b/thanasoft-back/app/Repositories/ConvoyRepositoryInterface.php @@ -0,0 +1,12 @@ +model->newQuery()->with(['primaryUser', 'convoys']); + + if (! empty($filters['search'])) { + $query->where(function ($q) use ($filters) { + $q->where('brand', 'like', '%' . $filters['search'] . '%') + ->orWhere('model', 'like', '%' . $filters['search'] . '%') + ->orWhere('registration_number', 'like', '%' . $filters['search'] . '%'); + }); + } + + if (! empty($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (! empty($filters['vehicle_type'])) { + $query->where('vehicle_type', $filters['vehicle_type']); + } + + $sortField = $filters['sort_by'] ?? 'created_at'; + $sortDirection = $filters['sort_direction'] ?? 'desc'; + + return $query->orderBy($sortField, $sortDirection)->paginate($perPage); + } +} diff --git a/thanasoft-back/app/Repositories/VehicleRepositoryInterface.php b/thanasoft-back/app/Repositories/VehicleRepositoryInterface.php new file mode 100644 index 0000000..5008812 --- /dev/null +++ b/thanasoft-back/app/Repositories/VehicleRepositoryInterface.php @@ -0,0 +1,12 @@ +id(); + $table->string('photo_file_name')->nullable(); + $table->string('photo_file_url')->nullable(); + $table->string('photo_mime_type')->nullable(); + $table->unsignedBigInteger('photo_size')->nullable(); + $table->string('brand'); + $table->string('model'); + $table->string('registration_number')->unique(); + $table->enum('vehicle_type', ['hearse', 'transport_vehicle', 'utility', 'sedan'])->default('utility'); + $table->enum('fuel_type', ['diesel', 'petrol', 'electric', 'hybrid'])->default('diesel'); + $table->unsignedSmallInteger('year')->nullable(); + $table->foreignId('primary_user_id')->nullable()->constrained('employees')->nullOnDelete(); + $table->enum('status', ['active', 'maintenance', 'out_of_service'])->default('active'); + $table->text('notes')->nullable(); + $table->timestamps(); + + $table->index(['brand']); + $table->index(['status']); + $table->index(['primary_user_id']); + }); + + Schema::create('convoys', function (Blueprint $table) { + $table->id(); + $table->foreignId('deceased_id')->constrained('deceased')->cascadeOnDelete(); + $table->foreignId('client_id')->nullable()->constrained('clients')->nullOnDelete(); + $table->foreignId('vehicle_id')->nullable()->constrained('vehicles')->nullOnDelete(); + $table->string('mission_title')->nullable(); + $table->enum('convoy_type', ['local', 'national', 'international'])->default('local'); + $table->enum('transport_mode', ['road', 'air', 'sea', 'rail'])->default('road'); + $table->enum('status', ['planned', 'in_progress', 'completed', 'cancelled'])->default('planned'); + $table->dateTime('planned_start_at'); + $table->dateTime('estimated_end_at')->nullable(); + $table->string('family_email')->nullable(); + $table->boolean('automatic_notifications')->default(true); + $table->enum('departure_location_selection_mode', ['place', 'manual'])->default('place'); + $table->unsignedBigInteger('departure_location_id')->nullable(); + $table->string('departure_name')->nullable(); + $table->string('departure_address')->nullable(); + $table->string('departure_city')->nullable(); + $table->string('departure_postal_code', 20)->nullable(); + $table->string('departure_country_code', 2)->nullable(); + $table->decimal('departure_latitude', 10, 7)->nullable(); + $table->decimal('departure_longitude', 10, 7)->nullable(); + $table->text('departure_additional_details')->nullable(); + $table->timestamps(); + + $table->index(['deceased_id']); + $table->index(['client_id']); + $table->index(['vehicle_id']); + $table->index(['status']); + $table->index(['planned_start_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('convoys'); + Schema::dropIfExists('vehicles'); + } +}; diff --git a/thanasoft-back/routes/api.php b/thanasoft-back/routes/api.php index 3795d90..33d4099 100644 --- a/thanasoft-back/routes/api.php +++ b/thanasoft-back/routes/api.php @@ -25,6 +25,8 @@ use App\Http\Controllers\Api\PriceListController; use App\Http\Controllers\Api\TvaRateController; use App\Http\Controllers\Api\GoodsReceiptController; use App\Http\Controllers\Api\UserController; +use App\Http\Controllers\Api\VehicleController; +use App\Http\Controllers\Api\ConvoyController; /* @@ -121,6 +123,8 @@ Route::middleware('auth:sanctum')->group(function () { // Goods Receipts management Route::apiResource('goods-receipts', GoodsReceiptController::class); + Route::apiResource('vehicles', VehicleController::class); + Route::apiResource('convoys', ConvoyController::class); // Product Category management Route::get('/product-categories/search', [ProductCategoryController::class, 'search']); diff --git a/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue b/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue index efa1d9d..d6cb9a2 100644 --- a/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue +++ b/thanasoft-front/src/components/Organism/CRM/lieux/ListeLieuxPresentation.vue @@ -1,7 +1,7 @@