# Spécification API Remorques (Trailers) ## A) Fichiers frontend identifiés ### Pages principales - `frontend/src/pages/Trailers.tsx` - Page liste des remorques - `frontend/src/components/TrailerCard.tsx` - Carte d'affichage d'une remorque - `frontend/src/components/AddTrailerDialog.tsx` - Formulaire d'ajout - `frontend/src/components/EditTrailerDialog.tsx` - Formulaire d'édition - `frontend/src/components/TrailerTable.tsx` - Tableau (alternative) ### Utilisation dans d'autres pages - `frontend/src/pages/Trips.tsx` - Liste des remorques pour les voyages - `frontend/src/pages/CreateMissionPage.tsx` - Sélection de remorque - `frontend/src/components/CreateCouplingDialog.tsx` - Sélection de remorque pour attelage - `frontend/src/pages/Maintenances.tsx` - Liste des remorques - `frontend/src/pages/TripDetail.tsx` - Détail d'une remorque (GET `/api/trailers/{id}`) ## B) Contrat API attendu par le frontend ### Endpoints requis #### 1. GET `/api/trailers` **Description:** Liste toutes les remorques **Réponse attendue:** ```json [ { "id": 1, "numeroChassis": "CH-123456", "nom": "La Grande", "couleur": "Rouge", "type": "plateau", "capacite": 20.5, "photoUrl": "/storage/trailers/trailer_xxx.jpg", "photoCloudinaryPublicId": null, "createdAt": "2025-12-30T10:00:00.000Z", "updatedAt": "2025-12-30T10:00:00.000Z" } ] ``` **Format:** Array brut (pas de wrapper `{data: ...}`) **Champs camelCase:** Oui, tous les champs en camelCase #### 2. GET `/api/trailers/{id}` **Description:** Détails d'une remorque **Réponse attendue:** ```json { "id": 1, "numeroChassis": "CH-123456", "nom": "La Grande", "couleur": "Rouge", "type": "plateau", "capacite": 20.5, "photoUrl": "/storage/trailers/trailer_xxx.jpg", "photoCloudinaryPublicId": null, "createdAt": "2025-12-30T10:00:00.000Z", "updatedAt": "2025-12-30T10:00:00.000Z" } ``` **Format:** Object brut (pas de wrapper `{data: ...}`) #### 3. POST `/api/trailers` **Description:** Créer une remorque **Body (camelCase):** ```json { "numeroChassis": "CH-123456", "nom": "La Grande", "couleur": "Rouge", "type": "plateau", "capacite": 20.5, "photoUrl": "/storage/trailers/trailer_xxx.jpg", "photoCloudinaryPublicId": null } ``` **Réponse attendue (201):** ```json { "id": 1, "numeroChassis": "CH-123456", "nom": "La Grande", "couleur": "Rouge", "type": "plateau", "capacite": 20.5, "photoUrl": "/storage/trailers/trailer_xxx.jpg", "photoCloudinaryPublicId": null, "createdAt": "2025-12-30T10:00:00.000Z", "updatedAt": "2025-12-30T10:00:00.000Z" } ``` **Format:** Object brut (pas de wrapper `{data: ...}`) #### 4. PATCH `/api/trailers/{id}` **Description:** Modifier une remorque **Body (camelCase, tous les champs optionnels avec `sometimes`):** ```json { "numeroChassis": "CH-123457", "nom": "La Petite", "couleur": "Bleu", "type": "citerne", "capacite": 15.0, "photoUrl": "/storage/trailers/trailer_yyy.jpg", "photoCloudinaryPublicId": null } ``` **Réponse attendue:** ```json { "id": 1, "numeroChassis": "CH-123457", "nom": "La Petite", "couleur": "Bleu", "type": "citerne", "capacite": 15.0, "photoUrl": "/storage/trailers/trailer_yyy.jpg", "photoCloudinaryPublicId": null, "createdAt": "2025-12-30T10:00:00.000Z", "updatedAt": "2025-12-30T11:00:00.000Z" } ``` **Format:** Object brut (pas de wrapper `{data: ...}`) #### 5. DELETE `/api/trailers/{id}` **Description:** Supprimer une remorque **Réponse attendue:** 204 No Content #### 6. POST `/api/upload/trailer-photo` **Description:** Upload d'une photo de remorque **Body:** FormData avec champ `photo` **Réponse attendue:** ```json { "url": "/storage/trailers/trailer_1767064963_695345831a3bb.jpg", "publicId": null } ``` ### Format d'erreur **Validation:** ```json { "message": "The given data was invalid.", "errors": { "numeroChassis": ["Le numéro de châssis est déjà utilisé."], "type": ["Le type sélectionné n'est pas valide."] } } ``` ### Types de remorques (enum) D'après `trailerTypeValues` dans le schéma TypeScript: - `"plateau"` - `"citerne"` - `"benne"` - `"frigorifique"` - `"autre"` ## C) Schéma DB minimal (MVP) ### Table `trailers` ```sql CREATE TABLE trailers ( id INTEGER PRIMARY KEY AUTOINCREMENT, numero_chassis VARCHAR(255) NOT NULL UNIQUE, nom VARCHAR(255) NULLABLE, couleur VARCHAR(255) NULLABLE, type VARCHAR(255) NOT NULL, -- 'plateau', 'citerne', 'benne', 'frigorifique', 'autre' capacite REAL NULLABLE, -- en tonnes photo_url TEXT NULLABLE, -- URL publique (externe ou /storage/trailers/xxx.jpg) photo_cloudinary_public_id VARCHAR(255) NULLABLE, -- Pour compatibilité future Cloudinary created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_trailers_type ON trailers(type); CREATE INDEX idx_trailers_numero_chassis ON trailers(numero_chassis); ``` **Notes:** - Pas de soft delete pour l'instant - Pas de `status` (contrairement aux véhicules) - Pas de `plaquePhotoUrl` (contrairement aux véhicules) ## D) Plan d'implémentation Laravel ### 1. Migration **Fichier:** `database/migrations/YYYY_MM_DD_HHMMSS_create_trailers_table.php` **Colonnes:** - `id` (bigIncrements) - `numero_chassis` (string, unique, not null) - `nom` (string, nullable) - `couleur` (string, nullable) - `type` (string, not null) - enum: plateau, citerne, benne, frigorifique, autre - `capacite` (decimal(8,2), nullable) - en tonnes - `photo_url` (text, nullable) - `photo_cloudinary_public_id` (string, nullable) - `created_at`, `updated_at` (timestamps) **Index:** - `numero_chassis` (unique) - `type` (index) ### 2. Model **Fichier:** `app/Models/Trailer.php` **Fillable:** ```php protected $fillable = [ 'numero_chassis', 'nom', 'couleur', 'type', 'capacite', 'photo_url', 'photo_cloudinary_public_id', ]; ``` **Casts:** ```php protected $casts = [ 'capacite' => 'decimal:2', 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; ``` **Scopes:** - `scopeSearch($query, $search)` - recherche sur numero_chassis, nom, type - `scopeType($query, $type)` - filtre par type ### 3. Form Requests #### `app/Http/Requests/TrailerStoreRequest.php` **Règles:** - `numeroChassis`: required, string, max:255, unique:trailers,numero_chassis - `nom`: nullable, string, max:255 - `couleur`: nullable, string, max:255 - `type`: required, string, in:plateau,citerne,benne,frigorifique,autre - `capacite`: nullable, numeric, min:0, max:999.99 - `photoUrl`: nullable, string, max:2048, custom rule (http(s) ou /storage/) - `photoCloudinaryPublicId`: nullable, string, max:255 **Important:** `validated()` retourne camelCase, PAS de `prepareForValidation()` #### `app/Http/Requests/TrailerUpdateRequest.php` **Règles:** Même chose que StoreRequest mais avec `sometimes` partout **Unique ignore:** ```php 'numeroChassis' => ['sometimes', 'required', 'string', 'max:255', Rule::unique('trailers', 'numero_chassis')->ignore($trailerId) ], ``` ### 4. Resource **Fichier:** `app/Http/Resources/TrailerResource.php` **Transformation snake_case → camelCase:** ```php return [ 'id' => $this->id, 'numeroChassis' => $this->numero_chassis, 'nom' => $this->nom, 'couleur' => $this->couleur, 'type' => $this->type, 'capacite' => $this->capacite ? (float) $this->capacite : null, 'photoUrl' => $this->photo_url, 'photoCloudinaryPublicId' => $this->photo_cloudinary_public_id, 'createdAt' => $this->created_at?->toISOString(), 'updatedAt' => $this->updated_at?->toISOString(), ]; ``` ### 5. Controller **Fichier:** `app/Http/Controllers/TrailerController.php` **Méthodes:** - `index(Request $request): JsonResponse` - Liste avec recherche/filtre - `show(Trailer $trailer): JsonResponse` - Détails - `store(TrailerStoreRequest $request): JsonResponse` - Création - `update(TrailerUpdateRequest $request, Trailer $trailer): JsonResponse` - Modification - `destroy(Trailer $trailer): JsonResponse` - Suppression - `uploadPhoto(Request $request): JsonResponse` - Upload photo **Mapping camelCase → snake_case:** ```php private function mapApiToDb(array $data): array { $dbData = []; foreach ($data as $key => $value) { if ($key === 'numeroChassis') { $dbData['numero_chassis'] = $value; } elseif ($key === 'photoUrl') { $dbData['photo_url'] = $value; } elseif ($key === 'photoCloudinaryPublicId') { $dbData['photo_cloudinary_public_id'] = $value; } else { $dbData[$key] = $value; } } return $dbData; } ``` **Réponses:** Utiliser `->resolve()` pour retourner du JSON brut (pas de wrapper `{data}`) ### 6. Routes **Fichier:** `routes/api.php` ```php Route::middleware('auth:sanctum')->group(function () { // ... autres routes ... Route::apiResource('trailers', TrailerController::class); Route::post('/upload/trailer-photo', [TrailerController::class, 'uploadPhoto']); }); ``` ### 7. Upload Photo **Endpoint:** `POST /api/upload/trailer-photo` **Stockage:** `storage/app/public/trailers/` **Retour:** `{ url: "/storage/trailers/xxx.jpg", publicId: null }` ## E) Points d'attention 1. **CamelCase strict:** Tous les champs API en camelCase, conversion uniquement dans le Controller 2. **JSON brut:** Pas de wrapper `{data: ...}`, utiliser `->resolve()` 3. **Validation:** FormRequests valident camelCase directement 4. **Upload:** Même logique que véhicules, stockage local `/storage/trailers/` 5. **Type enum:** Validation stricte des types de remorques 6. **Capacité:** Décimal avec 2 décimales, nullable ## F) Checklist d'implémentation - [ ] Migration `create_trailers_table.php` - [ ] Model `Trailer.php` avec fillable, casts, scopes - [ ] FormRequest `TrailerStoreRequest.php` (validation camelCase) - [ ] FormRequest `TrailerUpdateRequest.php` (sometimes + unique ignore) - [ ] Resource `TrailerResource.php` (transformation camelCase) - [ ] Controller `TrailerController.php` (CRUD + upload + mapApiToDb) - [ ] Routes dans `api.php` (apiResource + upload) - [ ] Endpoint upload `/api/upload/trailer-photo` - [ ] Lien symbolique `storage:link` (déjà fait pour véhicules) - [ ] Correction frontend upload (utiliser VITE_API_BASE_URL + Bearer token)