# Update 3 — Marques, Catégories, Garantie dynamique, Complétion désignations, Rapport technicien, Signature client, Export PDF

> **Date** : 2026-05-19
> **Scope** : Base de données + Modèles + Seeders + Admin Filament + Livewire Employé

---

## Contexte

Avant cette mise à jour :
- La catégorie produit était un Enum PHP (`ProductCategory`) stocké en string dans `product_models.category`
- Pas de notion de marque sur les modèles produits
- Durée de garantie fixe (2 ans) dans `WarrantyService`, indépendante de la catégorie
- Pas de filtrage des pièces/désignations par catégorie dans le wizard de création
- Le technicien ne pouvait pas cocher les désignations comme terminées
- Pas de signature client, pas de rapport technicien, pas d'export PDF

---

## Changements

### 1. Nouvelle table `brands`

**Fichier** : `database/migrations/2026_05_19_000001_create_brands_table.php`

| Colonne | Type |
|---|---|
| id | bigint PK |
| name | varchar(100) NOT NULL |
| created_at | timestamp |
| updated_at | timestamp |

Brands seedées (`BrandSeeder`) :
Bosch, Chaffoteaux, Riello, Vaillant, Viessmann, Saunier Duval, Atlantic, De Dietrich, Frisquet, Elm Leblanc, Chappée, Baxi, Wolf, Ferroli, Unical

### 2. Nouvelle table `categories`

**Fichier** : `database/migrations/2026_05_19_000002_create_categories_table.php`

| Colonne | Type | Notes |
|---|---|---|
| id | bigint PK | |
| name | varchar(100) NOT NULL | Libellé (ex: "Chaudière") |
| slug | varchar(50) UNIQUE NOT NULL | (ex: "CHAUDIERE") |
| warranty_months | smallint NOT NULL | 24 ou 12 |
| created_at | timestamp | |
| updated_at | timestamp | |

Durée garantie par catégorie :
- Chaudière → 24 mois
- Climatiseur → 24 mois
- Chauffe-bain → 12 mois
- Chauffe-eau électrique → 12 mois

### 3. Migration `product_models` : ajout `brand_id` + remplacement `category` par `category_id`

**Fichier** : `database/migrations/2026_05_19_000003_add_brand_and_category_to_product_models.php`

- Ajoute `brand_id` nullable FK → `brands.id` (nullOnDelete)
- Ajoute `category_id` nullable FK → `categories.id` (nullOnDelete)
- Supprime l'ancienne colonne `category` (string enum)

> **Décision** : `category_id` est nullable pour permettre la migration sans contrainte.
> Le seeder `ProductModelSeeder` le renseigne systématiquement.

### 4. Migration `parts` : ajout `category_id`

**Fichier** : `database/migrations/2026_05_19_000004_add_category_id_to_parts.php`

- Ajoute `category_id` nullable FK → `categories.id` (nullOnDelete)
- Permet de filtrer les pièces affichées selon la catégorie du produit sélectionné

### 5. Migration `designations` : ajout `category_id`

**Fichier** : `database/migrations/2026_05_19_000005_add_category_id_to_designations.php`

- Ajoute `category_id` nullable FK → `categories.id` (nullOnDelete)
- Les désignations sans catégorie (`NULL`) apparaissent toujours dans les sélections

### 6. Migration `client_product_models` : ajout `commissioning_date`

**Fichier** : `database/migrations/2026_05_19_000006_add_commissioning_date_to_client_product_models.php`

- Ajoute `commissioning_date` date nullable — date de mise en marche réelle de la machine
- La garantie est calculée à partir de `commissioning_date` si renseignée, sinon `purchase_date`

### 7. Migration `intervention_designations` : ajout `is_completed`

**Fichier** : `database/migrations/2026_05_19_000007_add_is_completed_to_intervention_designations.php`

- Ajoute `is_completed` boolean NOT NULL DEFAULT false
- Le technicien coche chaque désignation terminée depuis la fiche d'intervention
- Quand toutes les désignations sont cochées → `interventions.status` passe à `DONE` automatiquement

### 8. Migration `interventions` : ajout champs technicien + signature

**Fichier** : `database/migrations/2026_05_19_000008_add_technician_and_signature_to_interventions.php`

- `technician_report` text nullable — rapport libre du technicien (visible dans la fiche + PDF)
- `client_signature_path` varchar(500) nullable — chemin stockage de la signature client (PNG)

---

## Modèles Eloquent

### Nouveaux modèles

#### `app/Models/Brand.php`
```php
final class Brand extends Model {
    protected $fillable = ['name'];
    public function productModels(): HasMany { ... }
}
```

#### `app/Models/Category.php`
```php
final class Category extends Model {
    protected $fillable = ['name', 'slug', 'warranty_months'];
    protected function casts(): array { return ['warranty_months' => 'integer']; }
    public function productModels(): HasMany { ... }
    public function parts(): HasMany { ... }
    public function designations(): HasMany { ... }
}
```

### Modèles modifiés

| Modèle | Changements |
|---|---|
| `ProductModel` | Remplace cast `category → ProductCategory` par relation `category(): BelongsTo` + `brand(): BelongsTo` |
| `Part` | Ajoute `category_id` en `$fillable` + relation `category(): BelongsTo` |
| `Designation` | Ajoute `category_id` en `$fillable` + relation `category(): BelongsTo` |
| `ClientProductModel` | Ajoute `commissioning_date` nullable (cast 'date') |
| `InterventionDesignation` | Ajoute `is_completed` (cast 'boolean') |
| `Intervention` | Ajoute `technician_report`, `client_signature_path` en `$fillable` |

---

## WarrantyService

**Fichier** : `app/Services/WarrantyService.php`

- `WARRANTY_YEARS` constant supprimée
- `getExpirationDate()` utilise `commissioning_date` si présente, sinon `purchase_date`
- La durée en mois vient de `$clientProduct->productModel->category->warranty_months`
- Si la catégorie n'est pas chargée, fallback à 24 mois

```php
private function getWarrantyMonths(ClientProductModel $clientProduct): int
{
    return $clientProduct->productModel?->category?->warranty_months ?? 24;
}

public function getExpirationDate(ClientProductModel $clientProduct): Carbon
{
    $baseDate = $clientProduct->commissioning_date ?? $clientProduct->purchase_date;
    return $baseDate->copy()->addMonths($this->getWarrantyMonths($clientProduct));
}
```

---

## Seeders

### `BrandSeeder`
15 marques : Bosch, Chaffoteaux, Riello, Vaillant, Viessmann, Saunier Duval, Atlantic, De Dietrich, Frisquet, Elm Leblanc, Chappée, Baxi, Wolf, Ferroli, Unical

### `CategorySeeder`
4 catégories avec warranty_months :
- Chaudière (CHAUDIERE, 24 mois)
- Chauffe-bain (CHAUFFE_BAIN, 12 mois)
- Climatiseur (CLIMATISEUR, 24 mois)
- Chauffe-eau électrique (CHAUFFE_EAU_ELECTRIQUE, 12 mois)

### `DesignationSeeder` (remplacement complet)
Nouvelle liste de 44 désignations avec catégorie associée :
- Chaudière : 8 désignations (Mise en marche / Réparation distance 1-4)
- Chauffe-bain : 8 désignations (Intervention / Réparation distance 1-4)
- Chauffe-eau électrique : 3 désignations (Maintenance 1-3) + 5 désignations (Réparation distance 1-4)
- Climatiseur : 8 désignations (Intervention / Réparation distance 1-4) + 5 pièces spécifiques
- Multi-catégorie / sans catégorie : Montage/Démontage chaudière, Montage/Démontage climatiseur, Charge gaz, Réparation fuite gaz

### `ProductModelSeeder` (mise à jour)
Modèles mis à jour avec `brand_id` et `category_id` (lookup par slug).
Les marques sont celles du seeder Brands (Bosch, Thermor, Ariston, Daikin, Samsung, LG, Atlantic remplacés par les nouvelles marques officielles).

### Ordre `DatabaseSeeder`
```php
$this->call([
    GovernorateSeeder::class,
    DepartmentSeeder::class,
    BrandSeeder::class,      // nouveau
    CategorySeeder::class,   // nouveau
    ProductModelSeeder::class,
    DesignationSeeder::class,
    UserSeeder::class,
]);
```

---

## Panel Filament Admin

### Nouvelles Resources

#### `BrandResource` (`/admin/brands`)
- CRUD simple : nom de la marque
- Groupe navigation : "Catalogue"

#### `CategoryResource` (`/admin/categories`)
- CRUD : nom, slug (auto-généré), warranty_months (select 12/24 mois)
- Groupe navigation : "Catalogue"

### Resources modifiées

#### `ProductModelResource`
- Select `brand_id` : liste des marques
- Select `category_id` : liste des catégories (remplace le Select Enum)

#### `PartResource`
- Select `category_id` nullable : catégorie de la pièce (filtrage dans le wizard)

#### `DesignationResource`
- Select `category_id` nullable : catégorie de la désignation

---

## Interface Employé (Livewire)

### `Employee/CreateIntervention` — mises à jour

**Étape 2 — Équipement :**
- Champ `commissioning_date` (date de mise en marche) lors de l'ajout d'une machine existante ou de l'association d'un modèle
- Le badge garantie est maintenant calculé via `WarrantyService` en tenant compte de `category.warranty_months`
- Affichage : "Sous garantie jusqu'au JJ/MM/AAAA" ou "Hors garantie depuis le JJ/MM/AAAA"

**Étape 3 — Désignations :**
- Si un modèle produit avec catégorie est sélectionné → filtrage par `category_id` (+ désignations sans catégorie toujours affichées)
- Sinon toutes les désignations sont affichées

**Étape 4 — Pièces :**
- Si un modèle produit avec catégorie est sélectionné → liste des pièces filtrée par même `category_id` (+ pièces sans catégorie)
- Sinon toutes les pièces sont affichées

### `Employee/ShowIntervention` — mises à jour

**Complétion des désignations :**
- Chaque désignation affiche une checkbox "Terminé"
- Clic → appelle `toggleDesignation(int $designationId)` dans le composant Livewire
- Quand toutes les désignations sont cochées → `InterventionService::changeStatus()` vers `DONE`
- Bouton "Exporter la fiche" activé seulement quand statut = `DONE`

**Rapport technicien :**
- Zone de texte (textarea) "Rapport technicien" modifiable depuis la fiche
- Sauvegardée via `saveReport()` (bouton ou auto-save)

**Signature client :**
- Section "Signature client" avec canvas JavaScript (`signature_pad` CDN)
- Bouton "Valider la signature" → encode le canvas en PNG base64 → envoie à Livewire → stocké dans `storage/app/public/signatures/{uuid}.png`
- Bouton "Effacer" pour recommencer
- Si signature déjà présente → afficher l'image

**Export PDF :**
- Bouton "Exporter la fiche PDF" visible seulement si status = `DONE`
- Ouvre une modale avec 3 options : Télécharger, Envoyer par Email, Envoyer par WhatsApp
- Route dédiée : `GET /interventions/{intervention}/pdf`
- Génération via `barryvdh/laravel-dompdf`

---

## Export PDF

### Contenu de la fiche PDF
- En-tête : logo/nom entreprise, date, numéro d'intervention
- Informations client (nom, téléphone, adresse)
- Machine concernée (modèle, marque, catégorie, n° de série, date de mise en marche)
- Statut garantie
- Désignations réalisées (avec prix)
- Pièces utilisées (avec prix unitaire × quantité)
- Rapport technicien
- Total TTC
- Signature client (image PNG si disponible)
- Signature technicien (image de `users.signature_path` si disponible)

### Route + Controller
```
GET /interventions/{intervention}/pdf
→ App\Http\Controllers\InterventionPdfController@export
→ Middleware auth + role:EMPLOYEE
```

---

## Librairies

### `barryvdh/laravel-dompdf` (PDF)
```bash
composer require barryvdh/laravel-dompdf
```

### `signature_pad` (Signature cliente)
Chargé via CDN dans la vue :
```html
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.0.0/dist/signature_pad.umd.min.js"></script>
```

---

## Fichiers créés / modifiés

| Fichier | Action |
|---|---|
| `database/migrations/2026_05_19_000001_create_brands_table.php` | Créé |
| `database/migrations/2026_05_19_000002_create_categories_table.php` | Créé |
| `database/migrations/2026_05_19_000003_add_brand_and_category_to_product_models.php` | Créé |
| `database/migrations/2026_05_19_000004_add_category_id_to_parts.php` | Créé |
| `database/migrations/2026_05_19_000005_add_category_id_to_designations.php` | Créé |
| `database/migrations/2026_05_19_000006_add_commissioning_date_to_client_product_models.php` | Créé |
| `database/migrations/2026_05_19_000007_add_is_completed_to_intervention_designations.php` | Créé |
| `database/migrations/2026_05_19_000008_add_technician_and_signature_to_interventions.php` | Créé |
| `app/Models/Brand.php` | Créé |
| `app/Models/Category.php` | Créé |
| `app/Models/ProductModel.php` | Modifié |
| `app/Models/Part.php` | Modifié |
| `app/Models/Designation.php` | Modifié |
| `app/Models/ClientProductModel.php` | Modifié |
| `app/Models/InterventionDesignation.php` | Modifié |
| `app/Models/Intervention.php` | Modifié |
| `app/Services/WarrantyService.php` | Modifié |
| `database/seeders/BrandSeeder.php` | Créé |
| `database/seeders/CategorySeeder.php` | Créé |
| `database/seeders/DesignationSeeder.php` | Remplacé |
| `database/seeders/ProductModelSeeder.php` | Remplacé |
| `database/seeders/DatabaseSeeder.php` | Modifié |
| `app/Filament/Resources/Brands/BrandResource.php` | Créé |
| `app/Filament/Resources/Categories/CategoryResource.php` | Créé |
| `app/Filament/Resources/ProductModels/Schemas/ProductModelForm.php` | Modifié |
| `app/Filament/Resources/Parts/Schemas/PartForm.php` | Modifié |
| `app/Filament/Resources/Designations/Schemas/DesignationForm.php` | Modifié |
| `app/Http/Livewire/Employee/CreateIntervention.php` | Modifié |
| `app/Http/Livewire/Employee/ShowIntervention.php` | Modifié |
| `resources/views/livewire/employee/show-intervention.blade.php` | Modifié |
| `resources/views/livewire/employee/create-intervention.blade.php` | Modifié |
| `app/Http/Controllers/InterventionPdfController.php` | Créé |
| `resources/views/pdf/intervention.blade.php` | Créé |
| `routes/web.php` | Modifié (route PDF) |

---

## Migrations

```bash
php artisan migrate
# Applique les 8 nouvelles migrations

php artisan db:seed --class=BrandSeeder
php artisan db:seed --class=CategorySeeder
# Ou fresh seed complet :
php artisan migrate:fresh --seed
```

---

## Notes importantes

- **`ProductCategory` enum** : conservé tel quel pour ne pas casser les règles de projet. Il n'est plus utilisé comme cast sur `ProductModel` mais peut servir de référence de valeurs pour les slugs de catégories.
- **Garantie dynamique** : `WarrantyService` utilise `commissioning_date` (date de mise en marche) en priorité sur `purchase_date`. La durée vient de `category.warranty_months`.
- **Signature stockage** : `storage/app/public/signatures/` — nécessite `php artisan storage:link`.
- **PDF** : la route est protégée par `auth + role:EMPLOYEE`. Le PDF est généré à la volée (pas stocké).
- **Envoi WhatsApp** : via lien `https://wa.me/{phone}?text=...` avec URL encodée du lien PDF (ou message de confirmation). Pas d'API WhatsApp Business — lien direct mobile.
- **Envoi Email** : via `Mail::to($client->email)->send(new InterventionReportMail($intervention))` — Mailable à créer si l'email client est renseigné.
