# Plan d'implémentation CRUD Crew Members (Chauffeurs/Équipage) ## 1. Frontend API Contract (crew-members) ### Tableau des endpoints | Endpoint | Method | Query Params | Request Body | Response Shape | Notes | |----------|--------|--------------|--------------|----------------|-------| | `/api/crew-members` | GET | Aucun | - | `Array` (liste brute, pas de wrapper) | Retourne tous les membres d'équipage. Pas de pagination côté frontend (filtrage client-side). | | `/api/crew-members` | POST | - | `InsertCrewMember` (camelCase) | `CrewMember` (objet unique, pas de wrapper) | Création d'un nouveau membre. Status 201. | | `/api/crew-members/{id}` | GET | - | - | `CrewMember` (objet unique) | Récupération d'un membre spécifique (utilisé implicitement via queryKey). | | `/api/crew-members/{id}` | PATCH | - | `Partial` (camelCase) | `CrewMember` (objet unique) | Mise à jour partielle. Tous les champs sont optionnels (`sometimes`). | | `/api/crew-members/{id}` | DELETE | - | - | `null` (status 204) | Suppression. Pas de body dans la réponse. | | `/api/upload/crew-member-photo` | POST | - | `FormData` avec `photo` | `{ url: string, publicId: string \| null }` | Upload de photo. Retourne URL absolue. | ### Format des données #### InsertCrewMember (POST/PATCH) ```typescript { nom: string; // REQUIRED (POST), optional (PATCH) prenom: string; // REQUIRED (POST), optional (PATCH) role: "chauffeur" | "apprenti"; // REQUIRED (POST), optional (PATCH) telephone?: string | null; // OPTIONAL photoUrl?: string | null; // OPTIONAL (URL absolue ou /storage/...) photoCloudinaryPublicId?: string | null; // OPTIONAL userId?: string | null; // OPTIONAL (pour lier/délier un compte utilisateur) documents?: object | null; // OPTIONAL (non utilisé actuellement dans les dialogs) } ``` #### CrewMember (Response) ```typescript { id: number; nom: string; prenom: string; role: "chauffeur" | "apprenti"; telephone: string | null; photoUrl: string | null; // URL absolue (via media_url()) photoCloudinaryPublicId: string | null; userId: string | null; // ID du compte utilisateur lié (nullable) documents: object | null; // JSONB (non utilisé actuellement) createdAt: string; // ISO 8601 (ex: "2025-01-01T12:00:00.000Z") updatedAt: string; // ISO 8601 } ``` #### GET /api/crew-members (liste) - **Format** : Array brut (pas de `{data: [...]}`) - **Pas de pagination** : Frontend récupère tout et filtre côté client - **Recherche** : Frontend filtre par `nom`, `prenom`, `telephone` (client-side) - **Tri** : Non spécifié (utiliser `orderBy('nom')->orderBy('prenom')` par défaut) #### Erreurs attendues - **422 Validation Error** : Format Laravel standard ```json { "message": "The given data was invalid.", "errors": { "nom": ["Le champ nom est obligatoire."], "role": ["Le rôle sélectionné n'est pas valide."] } } ``` - **401 Unauthorized** : Si token manquant/invalide - **404 Not Found** : Si `{id}` n'existe pas (PATCH/DELETE/GET) - **500 Server Error** : Erreur serveur générique ### Authentification - **Méthode** : Bearer token (`Authorization: Bearer {token}`) + cookies (`credentials: "include"`) - **Middleware** : `auth:sanctum` (identique à vehicles/trailers) - **Token source** : `localStorage.getItem("parcapp_token")` ### Upload photo - **Endpoint** : `POST /api/upload/crew-member-photo` - **Content-Type** : `multipart/form-data` - **Field name** : `photo` - **Validation frontend** : image/*, max 5MB - **Response** : `{ url: string, publicId: string | null }` - **URL retournée** : Doit être absolue (via `MediaUrl::url()`) ### Champs spéciaux #### `userId` (liaison compte utilisateur) - **Type** : `string | null` - **Usage** : Lier un membre d'équipage à un compte `User` pour permettre la connexion - **Comportement** : - `null` = aucun compte lié - `string` = ID du compte utilisateur (format UUID ou string selon User model) - **Endpoint Users.tsx** : Utilise `PATCH /api/crew-members/{id}` avec `{ userId: string }` ou `{ userId: null }` #### `documents` (JSONB) - **Type** : `object | null` - **Usage** : Stocker des documents (permis, CNI, etc.) - non utilisé actuellement dans les dialogs - **Validation** : Optionnel, accepter n'importe quel JSON valide --- ## 2. Backend Impacts - Checklist complète ### A. Database #### Migration à créer **Fichier** : `database/migrations/YYYY_MM_DD_HHMMSS_create_crew_members_table.php` **Colonnes** : ```php Schema::create('crew_members', function (Blueprint $table) { $table->id(); $table->string('nom'); // REQUIRED $table->string('prenom'); // REQUIRED $table->string('role'); // REQUIRED: 'chauffeur' | 'apprenti' $table->string('telephone')->nullable(); $table->text('photo_url')->nullable(); // URL absolue ou /storage/... $table->string('photo_cloudinary_public_id')->nullable(); $table->json('documents')->nullable(); // JSONB pour permis, CNI, etc. $table->string('user_id')->nullable(); // Foreign key vers users.id (nullable) $table->timestamps(); // Indexes $table->index('role'); $table->index('user_id'); $table->index(['nom', 'prenom']); // Pour recherche rapide }); ``` **Relations** : - `user_id` → `users.id` (foreign key, `onDelete('set null')`) **Contraintes** : - Pas de `unique` sur `nom`/`prenom` (plusieurs personnes peuvent avoir le même nom) - Pas de `unique` sur `user_id` (un user peut être lié à un seul crew_member, mais pas de contrainte DB) ### B. Model & Relations #### Fichier : `app/Models/CrewMember.php` **Fillable** : ```php protected $fillable = [ 'nom', 'prenom', 'role', 'telephone', 'photo_url', 'photo_cloudinary_public_id', 'documents', 'user_id', ]; ``` **Casts** : ```php protected $casts = [ 'documents' => 'array', // JSONB → array PHP 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; ``` **Relations** : ```php public function user() { return $this->belongsTo(User::class, 'user_id'); } ``` **Scopes** (optionnels, pour recherche future) : ```php public function scopeSearch($query, $search) { return $query->where(function ($q) use ($search) { $q->where('nom', 'like', "%{$search}%") ->orWhere('prenom', 'like', "%{$search}%") ->orWhere('telephone', 'like', "%{$search}%"); }); } public function scopeRole($query, $role) { return $query->where('role', $role); } ``` ### C. API Layer #### Routes (`routes/api.php`) ```php Route::middleware('auth:sanctum')->group(function () { // ... routes existantes ... Route::apiResource('crew-members', CrewMemberController::class); // Upload de photos de membres d'équipage Route::post('/upload/crew-member-photo', [CrewMemberController::class, 'uploadPhoto']); }); ``` #### Controller : `app/Http/Controllers/CrewMemberController.php` **Méthodes** : - `index(Request $request): JsonResponse` - Liste tous les membres (pas de pagination) - `show(CrewMember $crewMember): JsonResponse` - Détails d'un membre - `store(CrewMemberStoreRequest $request): JsonResponse` - Création (201) - `update(CrewMemberUpdateRequest $request, CrewMember $crewMember): JsonResponse` - Mise à jour - `destroy(CrewMember $crewMember): JsonResponse` - Suppression (204) - `uploadPhoto(Request $request): JsonResponse` - Upload photo **Pattern identique à VehicleController** : - Utiliser `mapApiToDb()` pour convertir camelCase → snake_case - Utiliser `MediaUrl::url()` pour les URLs de photos - Retourner `CrewMemberResource::collection()->resolve()` pour index - Retourner `(new CrewMemberResource($crewMember))->resolve()` pour show/store/update #### FormRequests **Fichier** : `app/Http/Requests/CrewMemberStoreRequest.php` ```php public function rules(): array { return [ 'nom' => ['required', 'string', 'max:255'], 'prenom' => ['required', 'string', 'max:255'], 'role' => ['required', 'string', Rule::in(['chauffeur', 'apprenti'])], 'telephone' => ['nullable', 'string', 'max:255'], 'photoUrl' => ['nullable', 'string', 'max:2048', $this->photoUrlRule()], 'photoCloudinaryPublicId' => ['nullable', 'string', 'max:255'], 'userId' => ['nullable', 'string', 'exists:users,id'], 'documents' => ['nullable', 'array'], // JSONB ]; } ``` **Fichier** : `app/Http/Requests/CrewMemberUpdateRequest.php` ```php public function rules(): array { $crewMember = $this->route('crew-member'); $crewMemberId = $crewMember instanceof \App\Models\CrewMember ? $crewMember->id : $crewMember; return [ 'nom' => ['sometimes', 'required', 'string', 'max:255'], 'prenom' => ['sometimes', 'required', 'string', 'max:255'], 'role' => ['sometimes', 'required', 'string', Rule::in(['chauffeur', 'apprenti'])], 'telephone' => ['sometimes', 'nullable', 'string', 'max:255'], 'photoUrl' => ['sometimes', 'nullable', 'string', 'max:2048', $this->photoUrlRule()], 'photoCloudinaryPublicId' => ['sometimes', 'nullable', 'string', 'max:255'], 'userId' => ['sometimes', 'nullable', 'string', 'exists:users,id'], 'documents' => ['sometimes', 'nullable', 'array'], ]; } ``` **Note** : Utiliser la même méthode `photoUrlRule()` que dans `VehicleStoreRequest`. #### Resource : `app/Http/Resources/CrewMemberResource.php` **Mapping camelCase** : ```php public function toArray(Request $request): array { return [ 'id' => $this->id, 'nom' => $this->nom, 'prenom' => $this->prenom, 'role' => $this->role, 'telephone' => $this->telephone, 'photoUrl' => MediaUrl::url($this->photo_url), // URL absolue 'photoCloudinaryPublicId' => $this->photo_cloudinary_public_id, 'userId' => $this->user_id, // string | null 'documents' => $this->documents, // array | null 'createdAt' => $this->created_at?->toISOString(), 'updatedAt' => $this->updated_at?->toISOString(), ]; } ``` ### D. Auth & Middleware - **Middleware** : `auth:sanctum` (identique à vehicles/trailers) - **Pas de Policy** : Pour l'instant, tous les utilisateurs authentifiés peuvent gérer l'équipage - **Pas de multi-tenant** : Pas de `org_id` ou `company_id` pour l'instant ### E. Upload Photo **Endpoint** : `POST /api/upload/crew-member-photo` **Comportement** : - Stocker dans `storage/app/public/crew-members/` - Nom de fichier : `crew_member_{timestamp}_{uniqid}.{ext}` - Retourner URL absolue via `MediaUrl::url()` - Format réponse : `{ url: string, publicId: null }` --- ## 3. Diff Minimal - Fichiers à créer/modifier ### Fichiers à créer 1. `database/migrations/YYYY_MM_DD_HHMMSS_create_crew_members_table.php` 2. `app/Models/CrewMember.php` 3. `app/Http/Controllers/CrewMemberController.php` 4. `app/Http/Requests/CrewMemberStoreRequest.php` 5. `app/Http/Requests/CrewMemberUpdateRequest.php` 6. `app/Http/Resources/CrewMemberResource.php` ### Fichiers à modifier 1. `routes/api.php` - Ajouter routes `crew-members` + upload ### Structure de dossiers ``` api/ ├── app/ │ ├── Http/ │ │ ├── Controllers/ │ │ │ └── CrewMemberController.php (nouveau) │ │ ├── Requests/ │ │ │ ├── CrewMemberStoreRequest.php (nouveau) │ │ │ └── CrewMemberUpdateRequest.php (nouveau) │ │ └── Resources/ │ │ └── CrewMemberResource.php (nouveau) │ └── Models/ │ └── CrewMember.php (nouveau) ├── database/ │ └── migrations/ │ └── YYYY_MM_DD_HHMMSS_create_crew_members_table.php (nouveau) └── routes/ └── api.php (modifier) ``` --- ## 4. Commandes Artisan & Tests ### Commandes de création ```bash # Générer la migration php artisan make:migration create_crew_members_table # Créer le modèle (manuellement, pas de make:model avec options) # Créer les fichiers manuellement selon le plan # Créer le controller php artisan make:controller CrewMemberController --api # Créer les FormRequests php artisan make:request CrewMemberStoreRequest php artisan make:request CrewMemberUpdateRequest # Créer la Resource php artisan make:resource CrewMemberResource ``` ### Migration ```bash # Local php artisan migrate # Production (shared hosting) php artisan migrate --force ``` ### Tests curl (après migration + seed) **Prérequis** : Token Bearer valide (récupéré via `/api/login`) ```bash # 1. GET liste (doit retourner array vide ou avec données) curl -X GET "https://apiparcapp.jrbxsolutions.com/api/crew-members" \ -H "Authorization: Bearer {TOKEN}" \ -H "Accept: application/json" # 2. POST création curl -X POST "https://apiparcapp.jrbxsolutions.com/api/crew-members" \ -H "Authorization: Bearer {TOKEN}" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "nom": "Ouédraogo", "prenom": "Amadou", "role": "chauffeur", "telephone": "+226 70 12 34 56" }' # 3. GET show (remplacer {id} par l'ID créé) curl -X GET "https://apiparcapp.jrbxsolutions.com/api/crew-members/{id}" \ -H "Authorization: Bearer {TOKEN}" \ -H "Accept: application/json" # 4. PATCH update curl -X PATCH "https://apiparcapp.jrbxsolutions.com/api/crew-members/{id}" \ -H "Authorization: Bearer {TOKEN}" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "telephone": "+226 70 99 88 77" }' # 5. POST upload photo curl -X POST "https://apiparcapp.jrbxsolutions.com/api/upload/crew-member-photo" \ -H "Authorization: Bearer {TOKEN}" \ -H "Accept: application/json" \ -F "photo=@/path/to/photo.jpg" # 6. DELETE curl -X DELETE "https://apiparcapp.jrbxsolutions.com/api/crew-members/{id}" \ -H "Authorization: Bearer {TOKEN}" \ -H "Accept: application/json" ``` --- ## 5. Risques & Points d'attention ### Production (Shared Hosting Bluehost) 1. **Permissions storage** : ```bash chmod -R 775 storage/app/public/crew-members chown -R www-data:www-data storage/app/public/crew-members ``` 2. **Symlink storage** : ```bash php artisan storage:link ``` 3. **Cache clear après déploiement** : ```bash php artisan config:clear php artisan route:clear php artisan cache:clear ``` 4. **Migration --force** : Obligatoire en production (pas d'interaction) ### CORS - Vérifier que `config/cors.php` autorise les requêtes depuis `https://parcapp.jrbxsolutions.com` - Headers attendus : `Authorization`, `Content-Type`, `Accept` ### Auth Sanctum - Vérifier que les routes `crew-members` sont bien sous `auth:sanctum` - Tester avec token invalide → doit retourner 401 ### Performance - **Pas de pagination** : Si beaucoup de membres, considérer ajouter pagination future - **N+1 queries** : Si on ajoute des relations (ex: `user`), utiliser `with('user')` dans le controller ### Validation - **`userId` exists** : Vérifier que l'utilisateur existe avant de lier - **`role` enum** : Strictement `['chauffeur', 'apprenti']` - **`documents` JSON** : Valider que c'est un array valide si fourni ### Compatibilité frontend - **Format dates** : ISO 8601 (`toISOString()`) pour `createdAt`/`updatedAt` - **URLs absolues** : Toujours utiliser `MediaUrl::url()` pour `photoUrl` - **camelCase** : Tous les champs de réponse en camelCase (via Resource) ### Upload photo - **Taille max** : 10MB (identique à vehicles/trailers) - **Types acceptés** : `jpeg,jpg,png,webp` - **Dossier** : `storage/app/public/crew-members/` - **URL retournée** : Absolue (via `MediaUrl::url()`) --- ## 6. Checklist de déploiement ### Avant déploiement - [ ] Migration créée et testée localement - [ ] Modèle `CrewMember` avec relations - [ ] Controller avec toutes les méthodes - [ ] FormRequests avec validation complète - [ ] Resource avec mapping camelCase + `MediaUrl::url()` - [ ] Routes ajoutées sous `auth:sanctum` - [ ] Upload photo fonctionnel - [ ] Tests curl réussis localement ### Déploiement - [ ] `git add . && git commit -m "feat: CRUD crew-members"` - [ ] `git push origin main` - [ ] Sur serveur : `git pull origin main` - [ ] `php artisan migrate --force` - [ ] `php artisan storage:link` (si pas déjà fait) - [ ] `chmod -R 775 storage/app/public/crew-members` - [ ] `php artisan config:clear && php artisan route:clear && php artisan cache:clear` ### Vérification post-déploiement - [ ] GET `/api/crew-members` retourne array (vide ou avec données) - [ ] POST création fonctionne - [ ] PATCH update fonctionne - [ ] DELETE fonctionne - [ ] Upload photo retourne URL absolue - [ ] Frontend affiche correctement les membres - [ ] Pas d'erreurs console frontend --- ## 7. Notes importantes ### Conventions respectées - ✅ **Format réponse** : Array/objet brut (pas de wrapper `{data: ...}`) - ✅ **Noms de champs** : camelCase dans les réponses (via Resource) - ✅ **Validation** : camelCase dans les FormRequests - ✅ **Mapping DB** : snake_case dans le Controller (`mapApiToDb()`) - ✅ **URLs médias** : Toujours absolues via `MediaUrl::url()` - ✅ **Auth** : `auth:sanctum` (identique vehicles/trailers) - ✅ **Erreurs** : Format Laravel standard (422 avec `errors`) ### Extensions futures possibles - Pagination si > 100 membres - Recherche backend (actuellement client-side) - Filtres par rôle (`?role=chauffeur`) - Soft deletes si nécessaire - Relations avec missions/trips (déjà prévues dans le schéma frontend) --- ## 8. Exemple de réponse complète ### GET /api/crew-members ```json [ { "id": 1, "nom": "Ouédraogo", "prenom": "Amadou", "role": "chauffeur", "telephone": "+226 70 12 34 56", "photoUrl": "https://apiparcapp.jrbxsolutions.com/storage/crew-members/crew_member_1234567890_abc123.jpg", "photoCloudinaryPublicId": null, "userId": "user-uuid-123", "documents": null, "createdAt": "2025-01-01T12:00:00.000Z", "updatedAt": "2025-01-01T12:00:00.000Z" } ] ``` ### POST /api/crew-members (201 Created) ```json { "id": 1, "nom": "Ouédraogo", "prenom": "Amadou", "role": "chauffeur", "telephone": "+226 70 12 34 56", "photoUrl": null, "photoCloudinaryPublicId": null, "userId": null, "documents": null, "createdAt": "2025-01-01T12:00:00.000Z", "updatedAt": "2025-01-01T12:00:00.000Z" } ``` ### POST /api/upload/crew-member-photo ```json { "url": "https://apiparcapp.jrbxsolutions.com/storage/crew-members/crew_member_1234567890_abc123.jpg", "publicId": null } ``` --- **Document généré le** : 2025-01-01 **Version** : 1.0 **Statut** : Prêt pour implémentation