# Déploiement CRUD Crew Members - ParcApp Backend ## Fichiers créés/modifiés ### Fichiers créés (6 fichiers) 1. **Migration** : `database/migrations/2025_12_30_230602_create_crew_members_table.php` - Table `crew_members` avec colonnes : id, nom, prenom, role, telephone, photo_url, photo_cloudinary_public_id, documents (json), user_id (FK nullable), timestamps - Indexes : role, user_id, (nom, prenom) 2. **Model** : `app/Models/CrewMember.php` - Fillable, casts (documents → array), relation `user()` - Scopes : `search()`, `role()` 3. **FormRequest Store** : `app/Http/Requests/CrewMemberStoreRequest.php` - Validation camelCase : nom, prenom, role (required), telephone, photoUrl, photoCloudinaryPublicId, userId, documents (nullable) - Règle custom `photoUrlRule()` (identique à vehicles) 4. **FormRequest Update** : `app/Http/Requests/CrewMemberUpdateRequest.php` - Mêmes règles avec `sometimes` partout - Route parameter : `crewMember` (via `->parameters()`) 5. **Resource** : `app/Http/Resources/CrewMemberResource.php` - Mapping snake_case → camelCase - `photoUrl` via `MediaUrl::url()` (URL absolue) - Dates en ISO 8601 (`toISOString()`) 6. **Controller** : `app/Http/Controllers/CrewMemberController.php` - `index()` : Liste brute (array via `->resolve()`) - `show()` : Objet brut - `store()` : Création (201) - `update()` : Mise à jour partielle - `destroy()` : Suppression (204) - `uploadPhoto()` : Upload dans `storage/app/public/crew-members/` - `mapApiToDb()` : Conversion camelCase → snake_case ### Fichiers modifiés (1 fichier) 1. **Routes** : `routes/api.php` - Ajout `Route::apiResource('crew-members', CrewMemberController::class)->parameters(['crew-members' => 'crewMember'])` - Ajout `Route::post('/upload/crew-member-photo', [CrewMemberController::class, 'uploadPhoto'])` --- ## Déploiement Production (Shared Hosting Bluehost) ### Étapes de déploiement ```bash # 1. Pull les modifications depuis GitHub git pull origin main # 2. Migration (--force obligatoire en production) php artisan migrate --force # 3. Créer le dossier de stockage (si nécessaire) mkdir -p storage/app/public/crew-members chmod -R 775 storage/app/public/crew-members # 4. Vérifier/créer le symlink storage (si pas déjà fait) php artisan storage:link # 5. Clear caches php artisan config:clear php artisan route:clear php artisan cache:clear # 6. Vérifier les permissions chmod -R 775 storage chmod -R 775 bootstrap/cache ``` ### Vérifications post-déploiement 1. **Migration** : Vérifier que la table `crew_members` existe ```sql SHOW TABLES LIKE 'crew_members'; DESCRIBE crew_members; ``` 2. **Routes** : Vérifier que les routes sont enregistrées ```bash php artisan route:list | grep crew-members ``` 3. **Permissions storage** : Vérifier que le dossier est accessible ```bash ls -la storage/app/public/crew-members ``` --- ## Tests curl (Smoke Tests) **Prérequis** : Récupérer un token Bearer valide via `/api/login` ```bash # Token (à remplacer) TOKEN="votre_token_bearer_ici" BASE_URL="https://apiparcapp.jrbxsolutions.com" # 1. GET liste (doit retourner array, vide ou avec données) curl -X GET "${BASE_URL}/api/crew-members" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/json" \ -H "Content-Type: application/json" # 2. POST création curl -X POST "${BASE_URL}/api/crew-members" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/json" \ -H "Content-Type: 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 "${BASE_URL}/api/crew-members/1" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/json" # 4. PATCH update (remplacer {id}) curl -X PATCH "${BASE_URL}/api/crew-members/1" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -d '{ "telephone": "+226 70 99 88 77" }' # 5. POST upload photo (remplacer /path/to/photo.jpg) curl -X POST "${BASE_URL}/api/upload/crew-member-photo" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/json" \ -F "photo=@/path/to/photo.jpg" # 6. DELETE (remplacer {id}) curl -X DELETE "${BASE_URL}/api/crew-members/1" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/json" ``` ### Réponses attendues **GET /api/crew-members** : ```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-12-30T23:06:02.000000Z", "updatedAt": "2025-12-30T23:06:02.000000Z" } ] ``` **POST /api/upload/crew-member-photo** : ```json { "url": "https://apiparcapp.jrbxsolutions.com/storage/crew-members/crew_member_1234567890_abc123.jpg", "publicId": null } ``` --- ## ⚠️ Risque Upload Domain - Frontend ### Problème identifié Le frontend utilise `fetch("/api/upload/crew-member-photo")` **sans `apiUrl()`** dans : - `frontend/src/components/AddCrewMemberDialog.tsx` (ligne 99) - `frontend/src/components/EditCrewMemberDialog.tsx` (ligne 123) ### Impact En production multi-domaines : - Frontend : `https://parcapp.jrbxsolutions.com` - Backend : `https://apiparcapp.jrbxsolutions.com` L'appel `fetch("/api/upload/crew-member-photo")` pointera vers : - ❌ `https://parcapp.jrbxsolutions.com/api/upload/crew-member-photo` (404) Au lieu de : - ✅ `https://apiparcapp.jrbxsolutions.com/api/upload/crew-member-photo` ### Solution backend (implémentée) L'endpoint est bien créé sur le backend à `/api/upload/crew-member-photo` (domaine API). ### Solution frontend (à corriger) **Fichiers à modifier** : 1. `frontend/src/components/AddCrewMemberDialog.tsx` 2. `frontend/src/components/EditCrewMemberDialog.tsx` **Correction** : ```typescript // AVANT const response = await fetch("/api/upload/crew-member-photo", { method: "POST", body: formDataUpload, }); // APRÈS import { apiUrl } from "@/lib/apiUrl"; import { getToken } from "@/lib/api"; const url = apiUrl("/api/upload/crew-member-photo"); const token = getToken(); const headers: HeadersInit = {}; if (token) { headers["Authorization"] = `Bearer ${token}`; } const response = await fetch(url, { method: "POST", headers, body: formDataUpload, credentials: "include", }); ``` ### Proxy/Rewrite serveur **Vérification nécessaire** : Si un proxy/rewrite existe côté serveur Bluehost pour rediriger `/api/*` du domaine frontend vers le domaine backend, alors le problème n'existe pas. **À vérifier** : - Configuration Apache/Nginx sur Bluehost - Fichier `.htaccess` ou configuration de reverse proxy - Si aucun proxy n'existe → **Correction frontend obligatoire** --- ## Points d'attention ### 1. Type DB (MySQL/MariaDB) - ✅ Utilisé `json()` au lieu de `jsonb()` (compatible MySQL) - ✅ `foreignId('user_id')` pour FK vers `users.id` (bigint) ### 2. Route Model Binding - ✅ Paramètre customisé : `crewMember` (via `->parameters(['crew-members' => 'crewMember'])`) - ✅ FormRequest utilise `$this->route('crewMember')` ### 3. Format réponse - ✅ Array brut pour `GET /api/crew-members` (pas de wrapper) - ✅ Objet brut pour `POST/PATCH/GET {id}` - ✅ Status 204 pour DELETE (pas de body) ### 4. URLs absolues - ✅ `photoUrl` via `MediaUrl::url()` dans Resource - ✅ Upload retourne URL absolue ### 5. Validation - ✅ camelCase dans FormRequests - ✅ `sometimes` partout dans UpdateRequest - ✅ Règle custom `photoUrlRule()` (identique vehicles/trailers) --- ## Checklist de validation - [x] Migration créée (MySQL compatible) - [x] Model avec relations et scopes - [x] FormRequests avec validation complète - [x] Resource avec mapping camelCase + MediaUrl - [x] Controller avec toutes les méthodes - [x] Routes ajoutées sous `auth:sanctum` - [x] Upload photo fonctionnel - [x] Route parameter customisé (`crewMember`) - [x] Tests curl fournis - [ ] **Frontend upload à corriger** (AddCrewMemberDialog + EditCrewMemberDialog) --- **Document généré le** : 2025-12-30 **Statut** : Prêt pour déploiement (sous réserve correction frontend upload)