# Phase 19 : Gestion du Matériel (Equipement POS)

**Priorité :** P1
**Complexité :** MOYENNE
**Routes :** ~8 manager
**Entités :** 2

---

## Objectif

Suivi complet du matériel POS (caisses, lecteurs code-barres, imprimantes thermiques, tablettes, balances) avec affectation aux employés/caisses, suivi de maintenance, et historique des événements.

---

## Entités

### `PosEquipment`
Table : `pos_equipment`

| Champ | Type | Description |
|-------|------|-------------|
| id | int PK | |
| company | FK Company CASCADE | |
| name | string 100, NotBlank | Nom descriptif |
| type | string 30 | `cash_register`, `barcode_scanner`, `receipt_printer`, `tablet`, `scale`, `card_reader`, `other` |
| serialNumber | string 100 nullable | Numéro de série |
| brand | string 100 nullable | Marque |
| model | string 100 nullable | Modèle |
| purchaseDate | date nullable | Date d'achat |
| purchasePrice | decimal 12,2 nullable | Prix d'achat |
| warrantyEndDate | date nullable | Fin de garantie |
| status | string 20, default `active` | `active`, `maintenance`, `broken`, `retired`, `lost` |
| assignedTo | FK User SET NULL nullable | Employé affecté |
| assignedRegister | FK Register SET NULL nullable | Caisse associée |
| location | string 255 nullable | Emplacement physique |
| notes | text nullable | |
| TimestampableEntity, SoftDeleteableEntity | | |

**Index :** (company_id, status), (company_id, type)
**Unique :** (company_id, serial_number) — si serial_number non null

**Helpers :**
- `isActive()`, `isBroken()`, `isUnderMaintenance()`, `isRetired()`, `isLost()`
- `getStatusLabel()` : Actif / En maintenance / Hors service / Retiré / Perdu
- `getStatusBadgeColor()` : green / yellow / red / gray / purple
- `getTypeLabel()` : Caisse / Lecteur code-barres / Imprimante / Tablette / Balance / Lecteur CB / Autre
- `getTypeIcon()` : icônes correspondantes
- `isUnderWarranty()` : warrantyEndDate > now

### `PosEquipmentLog` (append-only)
Table : `pos_equipment_log`

| Champ | Type | Description |
|-------|------|-------------|
| id | int PK | |
| equipment | FK PosEquipment CASCADE | |
| action | string 30 | `created`, `assigned`, `unassigned`, `maintenance_start`, `maintenance_end`, `status_changed`, `note_added` |
| performedBy | FK User CASCADE | |
| description | text | Description de l'action |
| oldStatus | string 20 nullable | Statut avant |
| newStatus | string 20 nullable | Statut après |
| createdAt | datetime Timestampable | |

**Constructeur only, pas de setters** (append-only comme StockMovement)

---

## Repositories

### `PosEquipmentRepository`
- `findByCompanyQuery(Company, filters)` : search (name, serial, brand), type, status, assignedTo, hasRegister
- `countByCompany(Company)` : total, par statut, par type
- `findAvailable(Company)` : status=active, assignedTo=null

### `PosEquipmentLogRepository`
- `findByEquipment(PosEquipment, ?limit)` : logs triés par date desc
- `findByCompanyQuery(Company, filters)` : pour consultation globale

---

## Service

### `PosEquipmentService`

**`createEquipment(PosEquipment, Company, User): PosEquipment`**
- Set company, safePersist, log action `created`

**`updateEquipment(PosEquipment, User): PosEquipment`**
- safePersist, log action `updated`

**`assignToUser(PosEquipment, User target, User performer): void`**
- Set assignedTo, log action `assigned`

**`unassign(PosEquipment, User performer): void`**
- Set assignedTo=null, log action `unassigned`

**`assignToRegister(PosEquipment, Register, User): void`**
- Set assignedRegister, log

**`changeStatus(PosEquipment, string newStatus, User, ?string description): void`**
- Log oldStatus/newStatus, set status, safePersist

**`startMaintenance(PosEquipment, User, string reason): void`**
- changeStatus → `maintenance`, log `maintenance_start`

**`endMaintenance(PosEquipment, User, ?string notes): void`**
- changeStatus → `active`, log `maintenance_end`

**`deleteEquipment(PosEquipment, User): void`**
- safeRemove (soft-delete)

---

## Controller Manager — 8 routes

| Route | Méthode | Path | Voter |
|-------|---------|------|-------|
| `manager_pos_equipment_index` | GET | `/manager/pos/equipment` | POS_MANAGE |
| `manager_pos_equipment_create` | GET/POST | `/manager/pos/equipment/create` | POS_MANAGE |
| `manager_pos_equipment_show` | GET | `/manager/pos/equipment/{id}` | POS_MANAGE |
| `manager_pos_equipment_edit` | GET/POST | `/manager/pos/equipment/{id}/edit` | POS_MANAGE |
| `manager_pos_equipment_delete` | POST | `/manager/pos/equipment/{id}/delete` | POS_MANAGE |
| `manager_pos_equipment_assign` | POST | `/manager/pos/equipment/{id}/assign` | POS_MANAGE |
| `manager_pos_equipment_unassign` | POST | `/manager/pos/equipment/{id}/unassign` | POS_MANAGE |
| `manager_pos_equipment_status` | POST | `/manager/pos/equipment/{id}/status` | POS_MANAGE |

---

## Formulaire

### `PosEquipmentFormType`
- name (TextType, required)
- type (ChoiceType : cash_register/barcode_scanner/receipt_printer/tablet/scale/card_reader/other)
- serialNumber (TextType, nullable)
- brand, model (TextType, nullable)
- purchaseDate (DateType, nullable)
- purchasePrice (MoneyType, nullable)
- warrantyEndDate (DateType, nullable)
- location (TextType, nullable)
- notes (TextareaType, nullable)

---

## Templates (4)

- `equipment/index.html.twig` — Stats cards (total, actifs, maintenance, hors service), filtres (type, statut), table paginée, gradient indigo
- `equipment/create.html.twig` — Formulaire Symfony Form
- `equipment/edit.html.twig` — Formulaire Symfony Form
- `equipment/show.html.twig` — Détail + infos assignation + timeline historique (PosEquipmentLog) + boutons action (assigner, maintenance, changer statut) avec modals Flowbite

## Menu
- Ajouter "Matériel" après "Caisses" dans `_menu_manager.html.twig`

## CSRF
- delete : `delete_equipment_{id}`
- assign : `assign_equipment_{id}`
- unassign : `unassign_equipment_{id}`
- status : `status_equipment_{id}`
