feat(vehicle): add vehicle+convoy feature with API, models, repos, UI
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.
This commit is contained in:
parent
56b0c50111
commit
284d228dc5
291
Convoi.json
Normal file
291
Convoi.json
Normal file
@ -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": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
thanasoft-back/app/Http/Controllers/Api/ConvoyController.php
Normal file
144
thanasoft-back/app/Http/Controllers/Api/ConvoyController.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreConvoyRequest;
|
||||||
|
use App\Http\Requests\UpdateConvoyRequest;
|
||||||
|
use App\Http\Resources\Convoy\ConvoyResource;
|
||||||
|
use App\Repositories\ConvoyRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class ConvoyController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ConvoyRepositoryInterface $convoyRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$convoys = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
144
thanasoft-back/app/Http/Controllers/Api/VehicleController.php
Normal file
144
thanasoft-back/app/Http/Controllers/Api/VehicleController.php
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StoreVehicleRequest;
|
||||||
|
use App\Http\Requests\UpdateVehicleRequest;
|
||||||
|
use App\Http\Resources\Vehicle\VehicleResource;
|
||||||
|
use App\Repositories\VehicleRepositoryInterface;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class VehicleController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly VehicleRepositoryInterface $vehicleRepository
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$vehicles = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
thanasoft-back/app/Http/Requests/StoreConvoyRequest.php
Normal file
41
thanasoft-back/app/Http/Requests/StoreConvoyRequest.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreConvoyRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'deceased_id' => ['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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
33
thanasoft-back/app/Http/Requests/StoreVehicleRequest.php
Normal file
33
thanasoft-back/app/Http/Requests/StoreVehicleRequest.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreVehicleRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
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' => ['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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
51
thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php
Normal file
51
thanasoft-back/app/Http/Requests/UpdateConvoyRequest.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class UpdateConvoyRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'deceased_id' => ['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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php
Normal file
35
thanasoft-back/app/Http/Requests/UpdateVehicleRequest.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class UpdateVehicleRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$vehicleId = $this->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'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
79
thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php
Normal file
79
thanasoft-back/app/Http/Resources/Convoy/ConvoyResource.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Convoy;
|
||||||
|
|
||||||
|
use App\Http\Resources\Client\ClientResource;
|
||||||
|
use App\Http\Resources\Deceased\DeceasedResource;
|
||||||
|
use App\Http\Resources\Vehicle\VehicleResource;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class ConvoyResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\Vehicle;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class VehicleResource extends JsonResource
|
||||||
|
{
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -66,6 +66,11 @@ class Client extends Model
|
|||||||
return $this->belongsTo(ClientGroup::class, 'group_id');
|
return $this->belongsTo(ClientGroup::class, 'group_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function convoys()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Convoy::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the human-readable label for the client type.
|
* Get the human-readable label for the client type.
|
||||||
*/
|
*/
|
||||||
|
|||||||
64
thanasoft-back/app/Models/Convoy.php
Normal file
64
thanasoft-back/app/Models/Convoy.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Convoy extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'deceased_id',
|
||||||
|
'client_id',
|
||||||
|
'vehicle_id',
|
||||||
|
'mission_title',
|
||||||
|
'convoy_type',
|
||||||
|
'transport_mode',
|
||||||
|
'status',
|
||||||
|
'planned_start_at',
|
||||||
|
'estimated_end_at',
|
||||||
|
'family_email',
|
||||||
|
'automatic_notifications',
|
||||||
|
'departure_location_selection_mode',
|
||||||
|
'departure_location_id',
|
||||||
|
'departure_name',
|
||||||
|
'departure_address',
|
||||||
|
'departure_city',
|
||||||
|
'departure_postal_code',
|
||||||
|
'departure_country_code',
|
||||||
|
'departure_latitude',
|
||||||
|
'departure_longitude',
|
||||||
|
'departure_additional_details',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'planned_start_at' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -57,6 +57,11 @@ class Deceased extends Model
|
|||||||
return $this->hasMany(Intervention::class);
|
return $this->hasMany(Intervention::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function convoys(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Convoy::class);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the file attachments for the deceased (polymorphic).
|
* Get the file attachments for the deceased (polymorphic).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -53,6 +53,11 @@ class Employee extends Model
|
|||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function vehicles(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Vehicle::class, 'primary_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full name of the employee.
|
* Get the full name of the employee.
|
||||||
*/
|
*/
|
||||||
|
|||||||
44
thanasoft-back/app/Models/Vehicle.php
Normal file
44
thanasoft-back/app/Models/Vehicle.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Vehicle extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'photo_file_name',
|
||||||
|
'photo_file_url',
|
||||||
|
'photo_mime_type',
|
||||||
|
'photo_size',
|
||||||
|
'brand',
|
||||||
|
'model',
|
||||||
|
'registration_number',
|
||||||
|
'vehicle_type',
|
||||||
|
'fuel_type',
|
||||||
|
'year',
|
||||||
|
'primary_user_id',
|
||||||
|
'status',
|
||||||
|
'notes',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'photo_size' => 'integer',
|
||||||
|
'year' => 'integer',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function primaryUser(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Employee::class, 'primary_user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function convoys(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Convoy::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -116,6 +116,8 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
|
|
||||||
$this->app->bind(\App\Repositories\PurchaseOrderRepositoryInterface::class, \App\Repositories\PurchaseOrderRepository::class);
|
$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\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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
51
thanasoft-back/app/Repositories/ConvoyRepository.php
Normal file
51
thanasoft-back/app/Repositories/ConvoyRepository.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\Convoy;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class ConvoyRepository extends BaseRepository implements ConvoyRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(Convoy $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$query = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
interface ConvoyRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator;
|
||||||
|
}
|
||||||
42
thanasoft-back/app/Repositories/VehicleRepository.php
Normal file
42
thanasoft-back/app/Repositories/VehicleRepository.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use App\Models\Vehicle;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
class VehicleRepository extends BaseRepository implements VehicleRepositoryInterface
|
||||||
|
{
|
||||||
|
public function __construct(Vehicle $model)
|
||||||
|
{
|
||||||
|
parent::__construct($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$query = $this->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
|
interface VehicleRepositoryInterface extends BaseRepositoryInterface
|
||||||
|
{
|
||||||
|
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator;
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('vehicles', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -25,6 +25,8 @@ use App\Http\Controllers\Api\PriceListController;
|
|||||||
use App\Http\Controllers\Api\TvaRateController;
|
use App\Http\Controllers\Api\TvaRateController;
|
||||||
use App\Http\Controllers\Api\GoodsReceiptController;
|
use App\Http\Controllers\Api\GoodsReceiptController;
|
||||||
use App\Http\Controllers\Api\UserController;
|
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
|
// Goods Receipts management
|
||||||
Route::apiResource('goods-receipts', GoodsReceiptController::class);
|
Route::apiResource('goods-receipts', GoodsReceiptController::class);
|
||||||
|
Route::apiResource('vehicles', VehicleController::class);
|
||||||
|
Route::apiResource('convoys', ConvoyController::class);
|
||||||
|
|
||||||
// Product Category management
|
// Product Category management
|
||||||
Route::get('/product-categories/search', [ProductCategoryController::class, 'search']);
|
Route::get('/product-categories/search', [ProductCategoryController::class, 'search']);
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<liste-lieux-template>
|
<liste-lieux-template>
|
||||||
<template #lieux-new-action>
|
<template #lieux-new-action>
|
||||||
<add-button text="Ajouter" />
|
<add-button text="Ajouter" @click="goToAddLocation" />
|
||||||
</template>
|
</template>
|
||||||
<template #select-filter>
|
<template #select-filter>
|
||||||
<filter-table />
|
<filter-table />
|
||||||
@ -29,6 +29,9 @@ import addButton from "@/components/molecules/new-button/addButton.vue";
|
|||||||
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
defineProps({
|
defineProps({
|
||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -61,4 +64,10 @@ const handleDeleteLocation = (locationId) => {
|
|||||||
const handlePageChange = (newPage) => {
|
const handlePageChange = (newPage) => {
|
||||||
emit("page-change", newPage);
|
emit("page-change", newPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const goToAddLocation = () => {
|
||||||
|
router.push({
|
||||||
|
name: "Add Location",
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<new-vehicle-template>
|
||||||
|
<template #multi-step></template>
|
||||||
|
<template #vehicle-form>
|
||||||
|
<new-vehicle-form
|
||||||
|
:loading="loading"
|
||||||
|
:validation-errors="validationErrors"
|
||||||
|
:success="success"
|
||||||
|
@create-vehicle="handleCreateVehicle"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</new-vehicle-template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
import NewVehicleForm from "@/components/molecules/form/NewVehicleForm.vue";
|
||||||
|
import NewVehicleTemplate from "@/components/templates/Employee/NewVehicleTemplate.vue";
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
validationErrors: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["createVehicle"]);
|
||||||
|
|
||||||
|
const handleCreateVehicle = (data) => {
|
||||||
|
emit("createVehicle", data);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,538 @@
|
|||||||
|
<template>
|
||||||
|
<div class="vdp">
|
||||||
|
<div v-if="isLoading" class="vdp__state">
|
||||||
|
<div class="vdp__spinner"></div>
|
||||||
|
<p>Chargement du véhicule...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="error" class="vdp__state">
|
||||||
|
<h5>Erreur de chargement</h5>
|
||||||
|
<p>{{ error }}</p>
|
||||||
|
<SoftButton color="primary" variant="outline" size="sm" @click="emit('reload')">
|
||||||
|
Réessayer
|
||||||
|
</SoftButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="!vehicle" class="vdp__state">
|
||||||
|
<h5>Véhicule introuvable</h5>
|
||||||
|
<p>Ce véhicule n'existe pas ou a été supprimé.</p>
|
||||||
|
<RouterLink to="/employes/vehicules">
|
||||||
|
<SoftButton color="primary" variant="outline" size="sm">Retour à la liste</SoftButton>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="vdp__topbar">
|
||||||
|
<div class="vdp__topbar-left">
|
||||||
|
<RouterLink to="/employes/vehicules">
|
||||||
|
<SoftButton color="secondary" variant="outline" size="sm">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M10 3L5 8l5 5" />
|
||||||
|
</svg>
|
||||||
|
Retour
|
||||||
|
</SoftButton>
|
||||||
|
</RouterLink>
|
||||||
|
<div class="vdp__breadcrumb">
|
||||||
|
<span>Employés</span>
|
||||||
|
<span class="vdp__breadcrumb-sep">/</span>
|
||||||
|
<span>Véhicules</span>
|
||||||
|
<span class="vdp__breadcrumb-sep">/</span>
|
||||||
|
<span class="vdp__breadcrumb-current">{{ vehicle.brand }} {{ vehicle.model }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vdp__topbar-actions">
|
||||||
|
<template v-if="!isEditMode">
|
||||||
|
<SoftButton color="primary" variant="outline" size="sm" @click="startEdit">
|
||||||
|
Modifier
|
||||||
|
</SoftButton>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<SoftButton color="secondary" variant="outline" size="sm" :disabled="saving" @click="cancelEdit">
|
||||||
|
Annuler
|
||||||
|
</SoftButton>
|
||||||
|
<SoftButton color="primary" variant="gradient" size="sm" :disabled="saving" @click="saveVehicle">
|
||||||
|
{{ saving ? "Sauvegarde..." : "Sauvegarder" }}
|
||||||
|
</SoftButton>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vdp__body">
|
||||||
|
<aside class="vdp__sidebar">
|
||||||
|
<div class="vdp-card vdp-card--sidebar">
|
||||||
|
<div class="vdp-sidebar__hero">
|
||||||
|
<div class="vdp-sidebar__icon">
|
||||||
|
<i class="fas fa-truck"></i>
|
||||||
|
</div>
|
||||||
|
<h4>{{ vehicle.brand }} {{ vehicle.model }}</h4>
|
||||||
|
<p>{{ vehicle.registration_number }}</p>
|
||||||
|
<span class="badge" :class="statusClass(vehicle.status)">
|
||||||
|
{{ formatStatus(vehicle.status) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vdp-sidebar__nav">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.id"
|
||||||
|
type="button"
|
||||||
|
class="vdp-sidebar__nav-item"
|
||||||
|
:class="{ 'vdp-sidebar__nav-item--active': activeTab === tab.id }"
|
||||||
|
@click="activeTab = tab.id"
|
||||||
|
>
|
||||||
|
{{ tab.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="vdp__panel">
|
||||||
|
<div v-if="activeTab === 'details'" class="vdp-card">
|
||||||
|
<div class="vdp-card__header">
|
||||||
|
<span class="vdp-card__title">Détails modifiables</span>
|
||||||
|
</div>
|
||||||
|
<div class="vdp-card__body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Marque</label>
|
||||||
|
<soft-input v-model="form.brand" :disabled="!isEditMode" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Modèle</label>
|
||||||
|
<soft-input v-model="form.model" :disabled="!isEditMode" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Immatriculation</label>
|
||||||
|
<soft-input v-model="form.registration_number" :disabled="!isEditMode" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Année</label>
|
||||||
|
<soft-input v-model="form.year" type="number" :disabled="!isEditMode" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Type</label>
|
||||||
|
<select v-model="form.vehicle_type" class="form-control" :disabled="!isEditMode">
|
||||||
|
<option value="utility">Utilitaire</option>
|
||||||
|
<option value="hearse">Corbillard</option>
|
||||||
|
<option value="transport_vehicle">Transport</option>
|
||||||
|
<option value="sedan">Berline</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Carburant</label>
|
||||||
|
<select v-model="form.fuel_type" class="form-control" :disabled="!isEditMode">
|
||||||
|
<option value="diesel">Diesel</option>
|
||||||
|
<option value="petrol">Essence</option>
|
||||||
|
<option value="electric">Électrique</option>
|
||||||
|
<option value="hybrid">Hybride</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Statut</label>
|
||||||
|
<select v-model="form.status" class="form-control" :disabled="!isEditMode">
|
||||||
|
<option value="active">Actif</option>
|
||||||
|
<option value="maintenance">Maintenance</option>
|
||||||
|
<option value="out_of_service">Hors service</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label">Utilisateur principal</label>
|
||||||
|
<div v-if="!isEditMode" class="vdp__read-value">
|
||||||
|
{{ vehicle.primary_user?.full_name || "Non attribué" }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="position-relative">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||||
|
<input
|
||||||
|
v-model="employeeSearch"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Rechercher un employé"
|
||||||
|
@input="handleEmployeeSearch"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="selectedEmployee || form.primary_user_id"
|
||||||
|
class="btn btn-outline-secondary mb-0"
|
||||||
|
type="button"
|
||||||
|
@click="clearSelectedEmployee"
|
||||||
|
>
|
||||||
|
Désassigner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="employeeLoading"
|
||||||
|
class="text-center py-2 position-absolute w-100 bg-white border rounded shadow-sm"
|
||||||
|
style="z-index: 1000; top: 100%"
|
||||||
|
>
|
||||||
|
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="employeeResults.length > 0 && showEmployeeResults"
|
||||||
|
class="list-group position-absolute w-100 mt-1 shadow-lg"
|
||||||
|
style="z-index: 1000; max-height: 280px; overflow-y: auto"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="employee in employeeResults"
|
||||||
|
:key="employee.id"
|
||||||
|
type="button"
|
||||||
|
class="list-group-item list-group-item-action"
|
||||||
|
@click="selectEmployee(employee)"
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="font-weight-bold text-sm">{{ employee.full_name }}</span>
|
||||||
|
<span class="text-xs text-muted">{{ employee.email || employee.job_title || 'Aucune information' }}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Notes</label>
|
||||||
|
<textarea v-model="form.notes" class="form-control" rows="4" :disabled="!isEditMode"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="activeTab === 'history'" class="vdp-card">
|
||||||
|
<div class="vdp-card__header">
|
||||||
|
<span class="vdp-card__title">Historique</span>
|
||||||
|
</div>
|
||||||
|
<div class="vdp-card__body">
|
||||||
|
<div class="vdp-timeline">
|
||||||
|
<div class="vdp-timeline__item">
|
||||||
|
<div class="vdp-timeline__dot"></div>
|
||||||
|
<div>
|
||||||
|
<strong>Création du véhicule</strong>
|
||||||
|
<p class="mb-0 text-sm text-secondary">{{ formatDate(vehicle.created_at) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vdp-timeline__item">
|
||||||
|
<div class="vdp-timeline__dot"></div>
|
||||||
|
<div>
|
||||||
|
<strong>Dernière mise à jour</strong>
|
||||||
|
<p class="mb-0 text-sm text-secondary">{{ formatDate(vehicle.updated_at) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="vdp-timeline__item" v-if="vehicle.primary_user">
|
||||||
|
<div class="vdp-timeline__dot"></div>
|
||||||
|
<div>
|
||||||
|
<strong>Utilisateur actuellement assigné</strong>
|
||||||
|
<p class="mb-0 text-sm text-secondary">{{ vehicle.primary_user.full_name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, defineEmits, defineProps, ref, watch } from "vue";
|
||||||
|
import { RouterLink } from "vue-router";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import { useEmployeeStore } from "@/stores/employeeStore";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
vehicle: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
saving: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["reload", "save"]);
|
||||||
|
const employeeStore = useEmployeeStore();
|
||||||
|
|
||||||
|
const activeTab = ref("details");
|
||||||
|
const isEditMode = ref(false);
|
||||||
|
const employeeSearch = ref("");
|
||||||
|
const employeeResults = ref([]);
|
||||||
|
const employeeLoading = ref(false);
|
||||||
|
const showEmployeeResults = ref(false);
|
||||||
|
const selectedEmployee = ref(null);
|
||||||
|
let debounceTimeout = null;
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
brand: "",
|
||||||
|
model: "",
|
||||||
|
registration_number: "",
|
||||||
|
vehicle_type: "utility",
|
||||||
|
fuel_type: "diesel",
|
||||||
|
year: "",
|
||||||
|
primary_user_id: null,
|
||||||
|
status: "active",
|
||||||
|
notes: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{ id: "details", label: "Détails" },
|
||||||
|
{ id: "history", label: "Historique" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.vehicle,
|
||||||
|
(vehicle) => {
|
||||||
|
if (!vehicle) return;
|
||||||
|
form.value = {
|
||||||
|
brand: vehicle.brand || "",
|
||||||
|
model: vehicle.model || "",
|
||||||
|
registration_number: vehicle.registration_number || "",
|
||||||
|
vehicle_type: vehicle.vehicle_type || "utility",
|
||||||
|
fuel_type: vehicle.fuel_type || "diesel",
|
||||||
|
year: vehicle.year || "",
|
||||||
|
primary_user_id: vehicle.primary_user_id || null,
|
||||||
|
status: vehicle.status || "active",
|
||||||
|
notes: vehicle.notes || "",
|
||||||
|
};
|
||||||
|
selectedEmployee.value = vehicle.primary_user || null;
|
||||||
|
employeeSearch.value = vehicle.primary_user?.full_name || "";
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const startEdit = () => {
|
||||||
|
isEditMode.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
isEditMode.value = false;
|
||||||
|
if (props.vehicle) {
|
||||||
|
selectedEmployee.value = props.vehicle.primary_user || null;
|
||||||
|
employeeSearch.value = props.vehicle.primary_user?.full_name || "";
|
||||||
|
form.value.primary_user_id = props.vehicle.primary_user_id || null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmployeeSearch = () => {
|
||||||
|
showEmployeeResults.value = true;
|
||||||
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
||||||
|
|
||||||
|
if (!employeeSearch.value.trim()) {
|
||||||
|
employeeResults.value = [];
|
||||||
|
showEmployeeResults.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
employeeLoading.value = true;
|
||||||
|
debounceTimeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
employeeResults.value = await employeeStore.searchEmployees(employeeSearch.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching employees:", error);
|
||||||
|
employeeResults.value = [];
|
||||||
|
} finally {
|
||||||
|
employeeLoading.value = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectEmployee = (employee) => {
|
||||||
|
selectedEmployee.value = employee;
|
||||||
|
employeeSearch.value = employee.full_name;
|
||||||
|
form.value.primary_user_id = employee.id;
|
||||||
|
employeeResults.value = [];
|
||||||
|
showEmployeeResults.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSelectedEmployee = () => {
|
||||||
|
selectedEmployee.value = null;
|
||||||
|
employeeSearch.value = "";
|
||||||
|
form.value.primary_user_id = null;
|
||||||
|
employeeResults.value = [];
|
||||||
|
showEmployeeResults.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveVehicle = () => {
|
||||||
|
emit("save", {
|
||||||
|
...form.value,
|
||||||
|
year: form.value.year ? Number(form.value.year) : null,
|
||||||
|
primary_user_id: form.value.primary_user_id || null,
|
||||||
|
notes: form.value.notes || null,
|
||||||
|
});
|
||||||
|
isEditMode.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatStatus = (status) => {
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
active: "Actif",
|
||||||
|
maintenance: "Maintenance",
|
||||||
|
out_of_service: "Hors service",
|
||||||
|
}[status] || status
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusClass = (status) => {
|
||||||
|
return {
|
||||||
|
active: "bg-gradient-success",
|
||||||
|
maintenance: "bg-gradient-warning",
|
||||||
|
out_of_service: "bg-gradient-danger",
|
||||||
|
}[status] || "bg-gradient-secondary";
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (value) => {
|
||||||
|
if (!value) return "-";
|
||||||
|
return new Date(value).toLocaleDateString("fr-FR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vdp {
|
||||||
|
padding: 1.5rem 1.75rem;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
.vdp__topbar,
|
||||||
|
.vdp__topbar-left,
|
||||||
|
.vdp__topbar-actions,
|
||||||
|
.vdp__breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.vdp__topbar {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.vdp__topbar-left { gap: 0.75rem; }
|
||||||
|
.vdp__topbar-actions { gap: 0.5rem; }
|
||||||
|
.vdp__breadcrumb { gap: 5px; font-size: 13px; color: #9ca3af; }
|
||||||
|
.vdp__breadcrumb-sep { color: #d1d5db; }
|
||||||
|
.vdp__breadcrumb-current { color: #374151; font-weight: 500; }
|
||||||
|
.vdp__body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 260px 1fr;
|
||||||
|
gap: 1.25rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.vdp__sidebar,
|
||||||
|
.vdp__panel { min-width: 0; }
|
||||||
|
.vdp__state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 420px;
|
||||||
|
gap: 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
.vdp__spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid #e5e7eb;
|
||||||
|
border-top-color: #111827;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.75s linear infinite;
|
||||||
|
}
|
||||||
|
.vdp-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid rgba(131, 146, 171, 0.25);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
||||||
|
}
|
||||||
|
.vdp-card__header {
|
||||||
|
padding: 0.875rem 1.25rem;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
.vdp-card__title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #8392ab;
|
||||||
|
}
|
||||||
|
.vdp-card__body { padding: 1.25rem; }
|
||||||
|
.vdp-card--sidebar { position: sticky; top: 1rem; }
|
||||||
|
.vdp-sidebar__hero {
|
||||||
|
padding: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
.vdp-sidebar__icon {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(310deg, #5e72e4 0%, #825ee4 100%);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.vdp-sidebar__hero h4 { margin-bottom: 0.25rem; }
|
||||||
|
.vdp-sidebar__hero p { color: #8392ab; margin-bottom: 0.75rem; }
|
||||||
|
.vdp-sidebar__nav { padding: 0.75rem; display: grid; gap: 0.5rem; }
|
||||||
|
.vdp-sidebar__nav-item {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.75rem 0.9rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #67748e;
|
||||||
|
}
|
||||||
|
.vdp-sidebar__nav-item--active {
|
||||||
|
background: rgba(94, 114, 228, 0.1);
|
||||||
|
color: #344767;
|
||||||
|
}
|
||||||
|
.vdp__read-value {
|
||||||
|
min-height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
.vdp-timeline { display: grid; gap: 1rem; }
|
||||||
|
.vdp-timeline__item { display: flex; gap: 0.9rem; align-items: flex-start; }
|
||||||
|
.vdp-timeline__dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #5e72e4;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
textarea.form-control,
|
||||||
|
select.form-control,
|
||||||
|
input.form-control,
|
||||||
|
.input-group-text {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.vdp { padding: 1rem; }
|
||||||
|
.vdp__body { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<vehicle-template>
|
||||||
|
<template #vehicle-new-action>
|
||||||
|
<add-button text="Ajouter véhicule" @click="goToVehicle" />
|
||||||
|
</template>
|
||||||
|
<template #select-filter>
|
||||||
|
<filter-table />
|
||||||
|
</template>
|
||||||
|
<template #vehicle-other-action>
|
||||||
|
<table-action />
|
||||||
|
</template>
|
||||||
|
<template #vehicle-table>
|
||||||
|
<vehicle-table
|
||||||
|
:data="vehicleData"
|
||||||
|
:loading="loadingData"
|
||||||
|
@view="goToDetails"
|
||||||
|
@edit="goToEdit"
|
||||||
|
@delete="deleteVehicle"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</vehicle-template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import VehicleTemplate from "@/components/templates/Employee/VehicleTemplate.vue";
|
||||||
|
import VehicleTable from "@/components/molecules/Tables/Employees/VehicleTable.vue";
|
||||||
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const emit = defineEmits(["pushDetails", "deleteVehicle"]);
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
vehicleData: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
loadingData: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const goToVehicle = () => {
|
||||||
|
router.push({
|
||||||
|
name: "Ajouter véhicule",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToDetails = (vehicleId) => {
|
||||||
|
router.push({
|
||||||
|
name: "Détail véhicule",
|
||||||
|
params: { id: vehicleId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToEdit = (vehicleId) => {
|
||||||
|
router.push({
|
||||||
|
path: `/employes/vehicules/${vehicleId}/edit`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteVehicle = (vehicleId) => {
|
||||||
|
emit("deleteVehicle", vehicleId);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<div class="table-container">
|
||||||
|
<div v-if="loading" class="loading-container">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<div class="spinner-border text-success loading-spinner-circle" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-content">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Véhicule</th>
|
||||||
|
<th>Immatriculation</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Carburant</th>
|
||||||
|
<th>Utilisateur principal</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
|
||||||
|
<td><div class="skeleton-text long"></div></td>
|
||||||
|
<td><div class="skeleton-text medium"></div></td>
|
||||||
|
<td><div class="skeleton-text medium"></div></td>
|
||||||
|
<td><div class="skeleton-text short"></div></td>
|
||||||
|
<td><div class="skeleton-text long"></div></td>
|
||||||
|
<td><div class="skeleton-text short"></div></td>
|
||||||
|
<td><div class="skeleton-text short"></div></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="table-responsive">
|
||||||
|
<table id="vehicle-list" class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Véhicule</th>
|
||||||
|
<th>Immatriculation</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Carburant</th>
|
||||||
|
<th>Utilisateur principal</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="vehicle in data" :key="vehicle.id">
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-checkbox />
|
||||||
|
<div class="ms-2">
|
||||||
|
<span>{{ vehicle.brand }} {{ vehicle.model }}</span>
|
||||||
|
<div class="text-xs text-muted">{{ vehicle.year || "N/A" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-xs font-weight-bold">{{ vehicle.registration_number }}</td>
|
||||||
|
<td class="text-xs font-weight-bold">{{ formatVehicleType(vehicle.vehicle_type) }}</td>
|
||||||
|
<td class="text-xs font-weight-bold">{{ formatFuelType(vehicle.fuel_type) }}</td>
|
||||||
|
<td class="text-xs font-weight-bold">{{ vehicle.primary_user?.full_name || "Non attribué" }}</td>
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<soft-button
|
||||||
|
:color="getStatusColor(vehicle.status)"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-sm"
|
||||||
|
>
|
||||||
|
{{ formatStatus(vehicle.status) }}
|
||||||
|
</soft-button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
title="Voir le véhicule"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('view', vehicle.id)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
<soft-button
|
||||||
|
color="warning"
|
||||||
|
variant="outline"
|
||||||
|
title="Modifier le véhicule"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('edit', vehicle.id)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
<soft-button
|
||||||
|
color="danger"
|
||||||
|
variant="outline"
|
||||||
|
title="Supprimer le véhicule"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
@click="emit('delete', vehicle.id)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && data.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-truck fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="empty-title">Aucun véhicule trouvé</h5>
|
||||||
|
<p class="empty-text text-muted">Aucun véhicule à afficher pour le moment.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import { defineEmits, defineProps } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["view", "edit", "delete"]);
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
skeletonRows: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatVehicleType = (type) => {
|
||||||
|
const labels = {
|
||||||
|
hearse: "Corbillard",
|
||||||
|
transport_vehicle: "Transport",
|
||||||
|
utility: "Utilitaire",
|
||||||
|
sedan: "Berline",
|
||||||
|
};
|
||||||
|
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFuelType = (type) => {
|
||||||
|
const labels = {
|
||||||
|
diesel: "Diesel",
|
||||||
|
petrol: "Essence",
|
||||||
|
electric: "Électrique",
|
||||||
|
hybrid: "Hybride",
|
||||||
|
};
|
||||||
|
|
||||||
|
return labels[type] || type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatStatus = (status) => {
|
||||||
|
const labels = {
|
||||||
|
active: "Actif",
|
||||||
|
maintenance: "Maintenance",
|
||||||
|
out_of_service: "Hors service",
|
||||||
|
};
|
||||||
|
|
||||||
|
return labels[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
active: "success",
|
||||||
|
maintenance: "warning",
|
||||||
|
out_of_service: "danger",
|
||||||
|
};
|
||||||
|
|
||||||
|
return colors[status] || "secondary";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-container {
|
||||||
|
position: relative;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
position: relative;
|
||||||
|
min-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner-circle {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
border-width: 0.28em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-content {
|
||||||
|
opacity: 0.55;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-row {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text {
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.short {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
383
thanasoft-front/src/components/molecules/form/NewVehicleForm.vue
Normal file
383
thanasoft-front/src/components/molecules/form/NewVehicleForm.vue
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
<template>
|
||||||
|
<div class="card p-3 border-radius-xl bg-white" data-animation="FadeIn">
|
||||||
|
<h5 class="font-weight-bolder mb-0">Nouveau Véhicule</h5>
|
||||||
|
<p class="mb-0 text-sm">Informations générales du véhicule</p>
|
||||||
|
|
||||||
|
<div class="multisteps-form__content">
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Marque <span class="text-danger">*</span></label>
|
||||||
|
<soft-input
|
||||||
|
v-model="form.brand"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:error="!!fieldErrors.brand"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. Mercedes-Benz"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.brand" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.brand }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Modèle <span class="text-danger">*</span></label>
|
||||||
|
<soft-input
|
||||||
|
v-model="form.model"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:error="!!fieldErrors.model"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. Sprinter"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.model" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.model }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Immatriculation <span class="text-danger">*</span></label>
|
||||||
|
<soft-input
|
||||||
|
v-model="form.registration_number"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:error="!!fieldErrors.registration_number"
|
||||||
|
type="text"
|
||||||
|
placeholder="ex. AB-123-CD"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.registration_number" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.registration_number }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Année</label>
|
||||||
|
<soft-input
|
||||||
|
v-model="form.year"
|
||||||
|
class="multisteps-form__input"
|
||||||
|
:error="!!fieldErrors.year"
|
||||||
|
type="number"
|
||||||
|
min="1900"
|
||||||
|
max="2100"
|
||||||
|
placeholder="ex. 2024"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.year" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.year }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Type de véhicule</label>
|
||||||
|
<select v-model="form.vehicle_type" class="form-control">
|
||||||
|
<option value="utility">Utilitaire</option>
|
||||||
|
<option value="hearse">Corbillard</option>
|
||||||
|
<option value="transport_vehicle">Transport</option>
|
||||||
|
<option value="sedan">Berline</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Carburant</label>
|
||||||
|
<select v-model="form.fuel_type" class="form-control">
|
||||||
|
<option value="diesel">Diesel</option>
|
||||||
|
<option value="petrol">Essence</option>
|
||||||
|
<option value="electric">Électrique</option>
|
||||||
|
<option value="hybrid">Hybride</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<label class="form-label">Statut</label>
|
||||||
|
<select v-model="form.status" class="form-control">
|
||||||
|
<option value="active">Actif</option>
|
||||||
|
<option value="maintenance">Maintenance</option>
|
||||||
|
<option value="out_of_service">Hors service</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||||
|
<label class="form-label">Utilisateur principal</label>
|
||||||
|
<div class="position-relative">
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||||
|
<input
|
||||||
|
v-model="employeeSearch"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Rechercher un employé par nom"
|
||||||
|
@input="handleEmployeeSearch"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="selectedEmployee"
|
||||||
|
class="btn btn-outline-secondary mb-0"
|
||||||
|
type="button"
|
||||||
|
@click="clearSelectedEmployee"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="employeeLoading"
|
||||||
|
class="text-center py-2 position-absolute w-100 bg-white border rounded shadow-sm"
|
||||||
|
style="z-index: 1000; top: 100%"
|
||||||
|
>
|
||||||
|
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="employeeResults.length > 0 && showEmployeeResults"
|
||||||
|
class="list-group position-absolute w-100 mt-1 shadow-lg"
|
||||||
|
style="z-index: 1000; max-height: 300px; overflow-y: auto"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="employee in employeeResults"
|
||||||
|
:key="employee.id"
|
||||||
|
type="button"
|
||||||
|
class="list-group-item list-group-item-action"
|
||||||
|
@click="selectEmployee(employee)"
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="font-weight-bold text-sm">{{ employee.full_name }}</span>
|
||||||
|
<span class="text-xs text-muted">
|
||||||
|
{{ employee.email || employee.job_title || "Aucune information" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="employeeSearch && !employeeLoading && showEmployeeResults"
|
||||||
|
class="position-absolute w-100 mt-1 p-3 bg-white border rounded shadow-sm text-center text-sm text-muted"
|
||||||
|
style="z-index: 1000"
|
||||||
|
>
|
||||||
|
Aucun employé trouvé.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedEmployee" class="mt-2 small text-success">
|
||||||
|
Sélectionné: {{ selectedEmployee.full_name }}
|
||||||
|
</div>
|
||||||
|
<div v-if="fieldErrors.primary_user_id" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.primary_user_id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label">Notes</label>
|
||||||
|
<textarea
|
||||||
|
v-model="form.notes"
|
||||||
|
class="form-control"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Informations complémentaires sur le véhicule"
|
||||||
|
></textarea>
|
||||||
|
<div v-if="fieldErrors.notes" class="invalid-feedback d-block">
|
||||||
|
{{ fieldErrors.notes }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-row d-flex mt-4">
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="secondary"
|
||||||
|
variant="outline"
|
||||||
|
class="me-2 mb-0"
|
||||||
|
@click="resetForm"
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</soft-button>
|
||||||
|
<soft-button
|
||||||
|
type="button"
|
||||||
|
color="dark"
|
||||||
|
variant="gradient"
|
||||||
|
class="ms-auto mb-0"
|
||||||
|
:disabled="props.loading"
|
||||||
|
@click="submitForm"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="props.loading"
|
||||||
|
class="spinner-border spinner-border-sm me-2"
|
||||||
|
role="status"
|
||||||
|
></span>
|
||||||
|
{{ props.loading ? "Création..." : "Créer le véhicule" }}
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, defineProps, ref, watch } from "vue";
|
||||||
|
import SoftInput from "@/components/SoftInput.vue";
|
||||||
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import { useEmployeeStore } from "@/stores/employeeStore";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
validationErrors: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["createVehicle"]);
|
||||||
|
const employeeStore = useEmployeeStore();
|
||||||
|
|
||||||
|
const fieldErrors = ref({});
|
||||||
|
const employeeSearch = ref("");
|
||||||
|
const employeeResults = ref([]);
|
||||||
|
const employeeLoading = ref(false);
|
||||||
|
const showEmployeeResults = ref(false);
|
||||||
|
const selectedEmployee = ref(null);
|
||||||
|
let debounceTimeout = null;
|
||||||
|
|
||||||
|
const defaultForm = () => ({
|
||||||
|
brand: "",
|
||||||
|
model: "",
|
||||||
|
registration_number: "",
|
||||||
|
vehicle_type: "utility",
|
||||||
|
fuel_type: "diesel",
|
||||||
|
year: "",
|
||||||
|
primary_user_id: "",
|
||||||
|
status: "active",
|
||||||
|
notes: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = ref(defaultForm());
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.validationErrors,
|
||||||
|
(newErrors) => {
|
||||||
|
const normalized = {};
|
||||||
|
Object.entries(newErrors || {}).forEach(([key, value]) => {
|
||||||
|
normalized[key] = Array.isArray(value) ? value[0] : value;
|
||||||
|
});
|
||||||
|
fieldErrors.value = normalized;
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.success,
|
||||||
|
(newSuccess) => {
|
||||||
|
if (newSuccess) {
|
||||||
|
resetForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEmployeeSearch = () => {
|
||||||
|
showEmployeeResults.value = true;
|
||||||
|
|
||||||
|
if (debounceTimeout) clearTimeout(debounceTimeout);
|
||||||
|
|
||||||
|
if (!employeeSearch.value.trim()) {
|
||||||
|
employeeResults.value = [];
|
||||||
|
showEmployeeResults.value = false;
|
||||||
|
form.value.primary_user_id = "";
|
||||||
|
selectedEmployee.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
employeeLoading.value = true;
|
||||||
|
|
||||||
|
debounceTimeout = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const results = await employeeStore.searchEmployees(employeeSearch.value);
|
||||||
|
employeeResults.value = results;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching employees:", error);
|
||||||
|
employeeResults.value = [];
|
||||||
|
} finally {
|
||||||
|
employeeLoading.value = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectEmployee = (employee) => {
|
||||||
|
selectedEmployee.value = employee;
|
||||||
|
employeeSearch.value = employee.full_name;
|
||||||
|
form.value.primary_user_id = employee.id;
|
||||||
|
employeeResults.value = [];
|
||||||
|
showEmployeeResults.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSelectedEmployee = () => {
|
||||||
|
selectedEmployee.value = null;
|
||||||
|
employeeSearch.value = "";
|
||||||
|
form.value.primary_user_id = "";
|
||||||
|
employeeResults.value = [];
|
||||||
|
showEmployeeResults.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
fieldErrors.value = {};
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
brand: form.value.brand,
|
||||||
|
model: form.value.model,
|
||||||
|
registration_number: form.value.registration_number,
|
||||||
|
vehicle_type: form.value.vehicle_type,
|
||||||
|
fuel_type: form.value.fuel_type,
|
||||||
|
year: form.value.year ? Number(form.value.year) : null,
|
||||||
|
primary_user_id: form.value.primary_user_id
|
||||||
|
? Number(form.value.primary_user_id)
|
||||||
|
: null,
|
||||||
|
status: form.value.status,
|
||||||
|
notes: form.value.notes || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
emit("createVehicle", payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
form.value = defaultForm();
|
||||||
|
fieldErrors.value = {};
|
||||||
|
employeeSearch.value = "";
|
||||||
|
employeeResults.value = [];
|
||||||
|
employeeLoading.value = false;
|
||||||
|
showEmployeeResults.value = false;
|
||||||
|
selectedEmployee.value = null;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #f5365c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid-feedback {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-border-sm {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control,
|
||||||
|
select.form-control {
|
||||||
|
border: 1px solid #d2d6da;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="multisteps-form mb-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-8 mx-auto my-5">
|
||||||
|
<slot name="multi-step" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-lg-8 m-auto">
|
||||||
|
<slot name="vehicle-form" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="d-sm-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<slot name="vehicle-new-action"></slot>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="dropdown d-inline">
|
||||||
|
<slot name="select-filter"></slot>
|
||||||
|
</div>
|
||||||
|
<slot name="vehicle-other-action"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card mt-4">
|
||||||
|
<slot name="vehicle-table"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -375,6 +375,11 @@ const routes = [
|
|||||||
name: "Localisation clients",
|
name: "Localisation clients",
|
||||||
component: () => import("@/views/pages/CRM/LocalisationClients.vue"),
|
component: () => import("@/views/pages/CRM/LocalisationClients.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/crm/new-location",
|
||||||
|
name: "Add Location",
|
||||||
|
component: () => import("@/views/pages/CRM/AddLocation.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/crm/clients",
|
path: "/crm/clients",
|
||||||
name: "Gestion clients",
|
name: "Gestion clients",
|
||||||
@ -695,6 +700,21 @@ const routes = [
|
|||||||
name: "Véhicules",
|
name: "Véhicules",
|
||||||
component: () => import("@/views/pages/Employes/Vehicules.vue"),
|
component: () => import("@/views/pages/Employes/Vehicules.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/employes/vehicules/:id",
|
||||||
|
name: "Détail véhicule",
|
||||||
|
component: () => import("@/views/pages/Employes/VehicleDetails.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/employes/vehicules/new",
|
||||||
|
name: "Ajouter véhicule",
|
||||||
|
component: () => import("@/views/pages/Employes/AddVehicle.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/employes/vehicules/:id/edit",
|
||||||
|
name: "Modifier véhicule",
|
||||||
|
component: () => import("@/views/pages/Employes/Vehicules.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/employes/absences",
|
path: "/employes/absences",
|
||||||
name: "Absences",
|
name: "Absences",
|
||||||
|
|||||||
122
thanasoft-front/src/services/vehicle.ts
Normal file
122
thanasoft-front/src/services/vehicle.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { request } from "./http";
|
||||||
|
|
||||||
|
export interface VehiclePhoto {
|
||||||
|
file_name: string | null;
|
||||||
|
file_url: string | null;
|
||||||
|
mime_type: string | null;
|
||||||
|
size: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VehiclePrimaryUser {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
full_name: string;
|
||||||
|
email: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Vehicle {
|
||||||
|
id: number;
|
||||||
|
photo: VehiclePhoto;
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
registration_number: string;
|
||||||
|
vehicle_type: "hearse" | "transport_vehicle" | "utility" | "sedan";
|
||||||
|
fuel_type: "diesel" | "petrol" | "electric" | "hybrid";
|
||||||
|
year: number | null;
|
||||||
|
status: "active" | "maintenance" | "out_of_service";
|
||||||
|
notes: string | null;
|
||||||
|
primary_user_id: number | null;
|
||||||
|
primary_user?: VehiclePrimaryUser | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VehicleListResponse {
|
||||||
|
data: Vehicle[];
|
||||||
|
meta: {
|
||||||
|
current_page: number;
|
||||||
|
last_page: number;
|
||||||
|
per_page: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VehicleResponse {
|
||||||
|
data: Vehicle;
|
||||||
|
message?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateVehiclePayload {
|
||||||
|
photo_file_name?: string | null;
|
||||||
|
photo_file_url?: string | null;
|
||||||
|
photo_mime_type?: string | null;
|
||||||
|
photo_size?: number | null;
|
||||||
|
brand: string;
|
||||||
|
model: string;
|
||||||
|
registration_number: string;
|
||||||
|
vehicle_type?: "hearse" | "transport_vehicle" | "utility" | "sedan";
|
||||||
|
fuel_type?: "diesel" | "petrol" | "electric" | "hybrid";
|
||||||
|
year?: number | null;
|
||||||
|
primary_user_id?: number | null;
|
||||||
|
status?: "active" | "maintenance" | "out_of_service";
|
||||||
|
notes?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateVehiclePayload extends Partial<CreateVehiclePayload> {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VehicleService = {
|
||||||
|
async getAllVehicles(params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
status?: string;
|
||||||
|
vehicle_type?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_direction?: string;
|
||||||
|
}): Promise<VehicleListResponse> {
|
||||||
|
return await request<VehicleListResponse>({
|
||||||
|
url: "/api/vehicles",
|
||||||
|
method: "get",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async getVehicle(id: number): Promise<VehicleResponse> {
|
||||||
|
return await request<VehicleResponse>({
|
||||||
|
url: `/api/vehicles/${id}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async createVehicle(payload: CreateVehiclePayload): Promise<VehicleResponse> {
|
||||||
|
return await request<VehicleResponse>({
|
||||||
|
url: "/api/vehicles",
|
||||||
|
method: "post",
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateVehicle(payload: UpdateVehiclePayload): Promise<VehicleResponse> {
|
||||||
|
const { id, ...updateData } = payload;
|
||||||
|
|
||||||
|
return await request<VehicleResponse>({
|
||||||
|
url: `/api/vehicles/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: updateData,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteVehicle(id: number): Promise<{ message: string; status: string }> {
|
||||||
|
return await request<{ message: string; status: string }>({
|
||||||
|
url: `/api/vehicles/${id}`,
|
||||||
|
method: "delete",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VehicleService;
|
||||||
@ -99,7 +99,6 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await ClientService.getAllClients(params);
|
const response = await ClientService.getAllClients(params);
|
||||||
console.log("API Response:", response);
|
|
||||||
setClients(response.data);
|
setClients(response.data);
|
||||||
if (response.meta) {
|
if (response.meta) {
|
||||||
setPagination(response.meta);
|
setPagination(response.meta);
|
||||||
@ -167,7 +166,7 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(payload);
|
|
||||||
const response = await ClientService.updateClient(payload);
|
const response = await ClientService.updateClient(payload);
|
||||||
const updatedClient = response.data;
|
const updatedClient = response.data;
|
||||||
|
|
||||||
@ -228,7 +227,7 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
* Search clients
|
* Search clients
|
||||||
*/
|
*/
|
||||||
const searchClients = async (query: string, exactMatch: boolean = false) => {
|
const searchClients = async (query: string, exactMatch: boolean = false) => {
|
||||||
console.log("ClientStore: searchClients called with query:", query);
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
@ -236,11 +235,7 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
const results = await ClientService.searchClients(query, {
|
const results = await ClientService.searchClients(query, {
|
||||||
exact_match: exactMatch,
|
exact_match: exactMatch,
|
||||||
});
|
});
|
||||||
console.log("ClientStore: Raw results from ClientService:", results);
|
|
||||||
console.log("ClientStore: Results type:", typeof results);
|
|
||||||
console.log("ClientStore: Results length:", results?.length);
|
|
||||||
setSearchClient(results);
|
setSearchClient(results);
|
||||||
console.log("ClientStore: Set searchResults to:", searchResults.value);
|
|
||||||
return results;
|
return results;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = "Erreur lors de la recherche des clients";
|
error.value = "Erreur lors de la recherche des clients";
|
||||||
|
|||||||
203
thanasoft-front/src/stores/vehicleStore.ts
Normal file
203
thanasoft-front/src/stores/vehicleStore.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import VehicleService from "@/services/vehicle";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CreateVehiclePayload,
|
||||||
|
UpdateVehiclePayload,
|
||||||
|
Vehicle,
|
||||||
|
} from "@/services/vehicle";
|
||||||
|
|
||||||
|
export const useVehicleStore = defineStore("vehicle", () => {
|
||||||
|
const vehicles = ref<Vehicle[]>([]);
|
||||||
|
const currentVehicle = ref<Vehicle | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
const pagination = ref({
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allVehicles = computed(() => vehicles.value);
|
||||||
|
const activeVehicles = computed(() =>
|
||||||
|
vehicles.value.filter((vehicle) => vehicle.status === "active")
|
||||||
|
);
|
||||||
|
const isLoading = computed(() => loading.value);
|
||||||
|
const hasError = computed(() => error.value !== null);
|
||||||
|
const getError = computed(() => error.value);
|
||||||
|
const getPagination = computed(() => pagination.value);
|
||||||
|
const getVehicleById = computed(() => (id: number) =>
|
||||||
|
vehicles.value.find((vehicle) => vehicle.id === id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const setLoading = (value: boolean) => {
|
||||||
|
loading.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setError = (value: string | null) => {
|
||||||
|
error.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setVehicles = (value: Vehicle[]) => {
|
||||||
|
vehicles.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentVehicle = (value: Vehicle | null) => {
|
||||||
|
currentVehicle.value = value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setPagination = (meta: any) => {
|
||||||
|
if (!meta) return;
|
||||||
|
|
||||||
|
pagination.value = {
|
||||||
|
current_page: Number(meta.current_page) || 1,
|
||||||
|
last_page: Number(meta.last_page) || 1,
|
||||||
|
per_page: Number(meta.per_page) || 10,
|
||||||
|
total: Number(meta.total) || 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchVehicles = async (params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
status?: string;
|
||||||
|
vehicle_type?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
sort_direction?: string;
|
||||||
|
}) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await VehicleService.getAllVehicles(params);
|
||||||
|
setVehicles(response.data);
|
||||||
|
setPagination(response.meta);
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(
|
||||||
|
err.response?.data?.message || err.message || "Failed to fetch vehicles"
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchVehicle = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await VehicleService.getVehicle(id);
|
||||||
|
setCurrentVehicle(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(
|
||||||
|
err.response?.data?.message || err.message || "Failed to fetch vehicle"
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const createVehicle = async (payload: CreateVehiclePayload) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await VehicleService.createVehicle(payload);
|
||||||
|
vehicles.value.unshift(response.data);
|
||||||
|
setCurrentVehicle(response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(
|
||||||
|
err.response?.data?.message || err.message || "Failed to create vehicle"
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVehicle = async (payload: UpdateVehiclePayload) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await VehicleService.updateVehicle(payload);
|
||||||
|
const updatedVehicle = response.data;
|
||||||
|
const index = vehicles.value.findIndex(
|
||||||
|
(vehicle) => vehicle.id === updatedVehicle.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
vehicles.value[index] = updatedVehicle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVehicle.value?.id === updatedVehicle.id) {
|
||||||
|
setCurrentVehicle(updatedVehicle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedVehicle;
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(
|
||||||
|
err.response?.data?.message || err.message || "Failed to update vehicle"
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteVehicle = async (id: number) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await VehicleService.deleteVehicle(id);
|
||||||
|
vehicles.value = vehicles.value.filter((vehicle) => vehicle.id !== id);
|
||||||
|
|
||||||
|
if (currentVehicle.value?.id === id) {
|
||||||
|
setCurrentVehicle(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(
|
||||||
|
err.response?.data?.message || err.message || "Failed to delete vehicle"
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearError = () => {
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
vehicles,
|
||||||
|
currentVehicle,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
allVehicles,
|
||||||
|
activeVehicles,
|
||||||
|
isLoading,
|
||||||
|
hasError,
|
||||||
|
getError,
|
||||||
|
getPagination,
|
||||||
|
getVehicleById,
|
||||||
|
fetchVehicles,
|
||||||
|
fetchVehicle,
|
||||||
|
createVehicle,
|
||||||
|
updateVehicle,
|
||||||
|
deleteVehicle,
|
||||||
|
clearError,
|
||||||
|
};
|
||||||
|
});
|
||||||
287
thanasoft-front/src/views/pages/CRM/AddLocation.vue
Normal file
287
thanasoft-front/src/views/pages/CRM/AddLocation.vue
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-xl-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header pb-0">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">Ajouter une localisation</h5>
|
||||||
|
<p class="text-sm text-secondary mb-0">
|
||||||
|
Le client est obligatoire et au moins un champ d'adresse doit etre renseigne.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div v-if="generalError" class="alert alert-danger" role="alert">
|
||||||
|
{{ generalError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Client *</label>
|
||||||
|
<select
|
||||||
|
v-model="form.client_id"
|
||||||
|
class="form-select"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.client_id }"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Selectionner un client</option>
|
||||||
|
<option
|
||||||
|
v-for="client in clients"
|
||||||
|
:key="client.id"
|
||||||
|
:value="String(client.id)"
|
||||||
|
>
|
||||||
|
{{ client.company_name || client.name || `Client #${client.id}` }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<div v-if="fieldErrors.client_id" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.client_id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Nom</label>
|
||||||
|
<input
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.name }"
|
||||||
|
maxlength="191"
|
||||||
|
placeholder="Ex: Depot principal"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.name" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Adresse ligne 1</label>
|
||||||
|
<input
|
||||||
|
v-model="form.address_line1"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.address_line1 }"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.address_line1" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.address_line1 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Adresse ligne 2</label>
|
||||||
|
<input
|
||||||
|
v-model="form.address_line2"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.address_line2 }"
|
||||||
|
maxlength="255"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.address_line2" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.address_line2 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Code postal</label>
|
||||||
|
<input
|
||||||
|
v-model="form.postal_code"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.postal_code }"
|
||||||
|
maxlength="20"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.postal_code" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.postal_code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Ville</label>
|
||||||
|
<input
|
||||||
|
v-model="form.city"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.city }"
|
||||||
|
maxlength="191"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.city" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.city }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Code pays</label>
|
||||||
|
<input
|
||||||
|
v-model="form.country_code"
|
||||||
|
type="text"
|
||||||
|
class="form-control text-uppercase"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.country_code }"
|
||||||
|
maxlength="2"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.country_code" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.country_code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Latitude GPS</label>
|
||||||
|
<input
|
||||||
|
v-model="form.gps_lat"
|
||||||
|
type="number"
|
||||||
|
step="0.000001"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.gps_lat }"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.gps_lat" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.gps_lat }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label class="form-label">Longitude GPS</label>
|
||||||
|
<input
|
||||||
|
v-model="form.gps_lng"
|
||||||
|
type="number"
|
||||||
|
step="0.000001"
|
||||||
|
class="form-control"
|
||||||
|
:class="{ 'is-invalid': fieldErrors.gps_lng }"
|
||||||
|
/>
|
||||||
|
<div v-if="fieldErrors.gps_lng" class="invalid-feedback">
|
||||||
|
{{ fieldErrors.gps_lng }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-switch mb-4">
|
||||||
|
<input
|
||||||
|
id="isDefault"
|
||||||
|
v-model="form.is_default"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label class="form-check-label" for="isDefault">
|
||||||
|
Definir comme localisation par defaut
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary mb-0"
|
||||||
|
:disabled="clientLocationStore.isLoading"
|
||||||
|
@click="router.push({ name: 'Localisation clients' })"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn bg-gradient-primary mb-0"
|
||||||
|
:disabled="clientLocationStore.isLoading"
|
||||||
|
>
|
||||||
|
{{ clientLocationStore.isLoading ? "Creation..." : "Ajouter" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { useClientLocationStore } from "@/stores/clientLocation";
|
||||||
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const clientLocationStore = useClientLocationStore();
|
||||||
|
const clientStore = useClientStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
|
const fieldErrors = ref({});
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
client_id: "",
|
||||||
|
name: "",
|
||||||
|
address_line1: "",
|
||||||
|
address_line2: "",
|
||||||
|
postal_code: "",
|
||||||
|
city: "",
|
||||||
|
country_code: "FR",
|
||||||
|
gps_lat: "",
|
||||||
|
gps_lng: "",
|
||||||
|
is_default: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const clients = computed(() => clientStore.clients || []);
|
||||||
|
|
||||||
|
const generalError = computed(() => {
|
||||||
|
const generalFieldError = fieldErrors.value.general;
|
||||||
|
|
||||||
|
if (Array.isArray(generalFieldError)) {
|
||||||
|
return generalFieldError[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return generalFieldError || clientLocationStore.error;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!clients.value.length) {
|
||||||
|
await clientStore.fetchClients({ page: 1, per_page: 100 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizePayload = () => ({
|
||||||
|
client_id: Number(form.client_id),
|
||||||
|
name: form.name || null,
|
||||||
|
address_line1: form.address_line1 || null,
|
||||||
|
address_line2: form.address_line2 || null,
|
||||||
|
postal_code: form.postal_code || null,
|
||||||
|
city: form.city || null,
|
||||||
|
country_code: (form.country_code || "FR").toUpperCase(),
|
||||||
|
gps_lat: form.gps_lat === "" ? null : Number(form.gps_lat),
|
||||||
|
gps_lng: form.gps_lng === "" ? null : Number(form.gps_lng),
|
||||||
|
is_default: form.is_default,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
fieldErrors.value = {};
|
||||||
|
clientLocationStore.clearError();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await clientLocationStore.createClientLocation(normalizePayload());
|
||||||
|
notificationStore.created("Localisation");
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({ name: "Localisation clients" });
|
||||||
|
}, 1500);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 422) {
|
||||||
|
fieldErrors.value = error.response.data.errors || {};
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur de validation",
|
||||||
|
"Veuillez corriger les erreurs dans le formulaire"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage =
|
||||||
|
error.response?.data?.message ||
|
||||||
|
"Impossible de creer la localisation";
|
||||||
|
|
||||||
|
notificationStore.error("Erreur", errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
54
thanasoft-front/src/views/pages/Employes/AddVehicle.vue
Normal file
54
thanasoft-front/src/views/pages/Employes/AddVehicle.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<add-vehicle-presentation
|
||||||
|
:loading="vehicleStore.isLoading"
|
||||||
|
:validation-errors="validationErrors"
|
||||||
|
:success="showSuccess"
|
||||||
|
@create-vehicle="handleCreateVehicle"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import AddVehiclePresentation from "@/components/Organism/Employee/AddVehiclePresentation.vue";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { useVehicleStore } from "@/stores/vehicleStore";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const vehicleStore = useVehicleStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const validationErrors = ref({});
|
||||||
|
const showSuccess = ref(false);
|
||||||
|
|
||||||
|
const handleCreateVehicle = async (form) => {
|
||||||
|
try {
|
||||||
|
validationErrors.value = {};
|
||||||
|
showSuccess.value = false;
|
||||||
|
|
||||||
|
await vehicleStore.createVehicle(form);
|
||||||
|
|
||||||
|
notificationStore.created("Véhicule");
|
||||||
|
showSuccess.value = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push({ name: "Véhicules" });
|
||||||
|
}, 1500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating vehicle:", error);
|
||||||
|
|
||||||
|
if (error.response && error.response.status === 422) {
|
||||||
|
validationErrors.value = error.response.data.errors || {};
|
||||||
|
notificationStore.error(
|
||||||
|
"Erreur de validation",
|
||||||
|
"Veuillez corriger les erreurs dans le formulaire"
|
||||||
|
);
|
||||||
|
} else if (error.response && error.response.data) {
|
||||||
|
const errorMessage =
|
||||||
|
error.response.data.message || "Une erreur est survenue";
|
||||||
|
notificationStore.error("Erreur", errorMessage);
|
||||||
|
} else {
|
||||||
|
notificationStore.error("Erreur", "Une erreur inattendue s'est produite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
47
thanasoft-front/src/views/pages/Employes/VehicleDetails.vue
Normal file
47
thanasoft-front/src/views/pages/Employes/VehicleDetails.vue
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<vehicle-detail-presentation
|
||||||
|
:vehicle="vehicleStore.currentVehicle"
|
||||||
|
:is-loading="vehicleStore.isLoading"
|
||||||
|
:error="vehicleStore.error"
|
||||||
|
:saving="saving"
|
||||||
|
@reload="loadVehicle"
|
||||||
|
@save="handleSave"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import VehicleDetailPresentation from "@/components/Organism/Employee/VehicleDetailPresentation.vue";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
|
import { useVehicleStore } from "@/stores/vehicleStore";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const vehicleStore = useVehicleStore();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
const vehicleId = Number(route.params.id);
|
||||||
|
|
||||||
|
const loadVehicle = async () => {
|
||||||
|
if (vehicleId) {
|
||||||
|
await vehicleStore.fetchVehicle(vehicleId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (payload) => {
|
||||||
|
try {
|
||||||
|
saving.value = true;
|
||||||
|
await vehicleStore.updateVehicle({ id: vehicleId, ...payload });
|
||||||
|
await loadVehicle();
|
||||||
|
notificationStore.updated("Véhicule");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating vehicle:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de mettre à jour le véhicule");
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadVehicle);
|
||||||
|
</script>
|
||||||
@ -1,11 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<vehicle-presentation
|
||||||
<h1>Vehicules</h1>
|
:vehicle-data="vehicleStore.vehicles"
|
||||||
</div>
|
:loading-data="vehicleStore.loading"
|
||||||
|
@delete-vehicle="handleDeleteVehicle"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { onMounted, onUnmounted } from "vue";
|
||||||
name: "Vehicules",
|
import VehiclePresentation from "@/components/Organism/Employee/VehiclePresentation.vue";
|
||||||
|
import { useVehicleStore } from "@/stores/vehicleStore";
|
||||||
|
|
||||||
|
const vehicleStore = useVehicleStore();
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await vehicleStore.fetchVehicles();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
vehicleStore.clearError();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteVehicle = async (vehicleId) => {
|
||||||
|
try {
|
||||||
|
await vehicleStore.deleteVehicle(vehicleId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting vehicle:", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user