# Phase 20 : Signalement Pertes & Casses

**Priorité :** P1
**Complexité :** MOYENNE
**Routes :** ~8 (6 manager + 2 employer)
**Entités :** 1

---

## Objectif

Système formel de déclaration de pertes, casses, vols et péremptions avec workflow d'approbation manager, impact automatique sur le stock, et traçabilité complète.

---

## Entité

### `StockLossReport`
Table : `pos_stock_loss_report`

| Champ | Type | Description |
|-------|------|-------------|
| id | int PK | |
| company | FK Company CASCADE | Isolation multi-tenant |
| reference | string 30, unique/company | Auto : `PRT-{PREFIX}-{YEAR}-{SEQ}` |
| type | string 20 | `breakage`, `theft`, `expiry`, `damage`, `other` |
| status | string 20, default `draft` | `draft`, `submitted`, `approved`, `rejected` |
| product | FK Product SET NULL | Produit concerné |
| quantity | int | Quantité perdue |
| unitValue | decimal 12,2 | Coût unitaire (snapshot costPrice au moment) |
| totalValue | decimal 14,2 | quantity * unitValue |
| reason | text | Explication de la perte |
| evidence | text nullable | Description preuve (photo, témoin, etc.) |
| reportedBy | FK User CASCADE | Employé qui signale |
| reportedAt | datetime | Date du signalement |
| approvedBy | FK User SET NULL nullable | Manager qui approuve |
| approvedAt | datetime nullable | |
| rejectedBy | FK User SET NULL nullable | Manager qui rejette |
| rejectedAt | datetime nullable | |
| rejectedReason | text nullable | Motif du rejet |
| notes | text nullable | |
| TimestampableEntity | | createdAt, updatedAt |

**Index :** (company_id, status), (company_id, created_at), (company_id, product_id)
**Unique :** (company_id, reference)

**Helpers :**
- `isDraft()`, `isSubmitted()`, `isApproved()`, `isRejected()`
- `getStatusLabel()` : Brouillon / Soumis / Approuvé / Rejeté
- `getStatusBadgeColor()` : gray / yellow / green / red
- `getTypeLabel()` : Casse / Vol / Péremption / Dommage / Autre
- `getTypeBadgeColor()` : red / purple / orange / yellow / gray

---

## Repository

### `StockLossReportRepository`
- `findByCompanyQuery(Company, filters)` : search (reference, productName), type, status, dateFrom, dateTo, productId
- `getNextReference(Company)` : COUNT+1
- `countByCompany(Company)` : total, draft, submitted, approved, rejected
- `getTotalLossValue(Company, ?from, ?to)` : SUM approved totalValue
- `getTopLostProducts(Company, ?from, ?to, limit)` : GROUP BY product, SUM quantity

---

## Service

### `StockLossService`

**Constructor :** StockLossReportRepository, ProductRepository, EntityManagerService, PosAuditService, PosSettingsService, FileLogger

**`createReport(Product, int quantity, string type, string reason, User reporter, Company company, ?string evidence, ?string notes): StockLossReport`**
- Génère reference : `PRT-{PREFIX}-{YEAR}-{SEQ}`
- Snapshot unitValue = product.getCostPrice() ?? '0'
- Calcule totalValue = bcmul(unitValue, quantity)
- Status = DRAFT si reporter est employer, SUBMITTED si manager
- safePersist
- Audit : ACTION_LOSS_REPORTED

**`submitReport(StockLossReport, User): void`**
- Valide : status == DRAFT
- Status → SUBMITTED, reportedAt = now
- Audit : ACTION_LOSS_SUBMITTED

**`approveReport(StockLossReport, User): void`** — en transaction
- Valide : status == SUBMITTED
- executeInTransaction :
  - product.setCurrentStock(currentStock - quantity)
  - Crée StockMovement inline TYPE_LOSS, REF_LOSS_REPORT, referenceId=report.id
  - Status → APPROVED, approvedBy, approvedAt
- Audit : ACTION_LOSS_APPROVED

**`rejectReport(StockLossReport, string reason, User): void`**
- Status → REJECTED, rejectedBy, rejectedAt, rejectedReason
- Audit : ACTION_LOSS_REJECTED

**`getReportsQuery(Company, filters): Query`**
**`getStats(Company): array`** — total, par type, valeur totale

---

## Constantes à ajouter

### `StockMovement.php`
```php
public const REF_LOSS_REPORT = 'loss_report';
```

### `PosAuditLog.php`
```php
public const ACTION_LOSS_REPORTED = 'loss_reported';
public const ACTION_LOSS_SUBMITTED = 'loss_submitted';
public const ACTION_LOSS_APPROVED = 'loss_approved';
public const ACTION_LOSS_REJECTED = 'loss_rejected';
public const ENTITY_LOSS_REPORT = 'loss_report';
```

---

## Controller Manager

### `StockLossController` — 6 routes

| Route | Méthode | Path | Voter |
|-------|---------|------|-------|
| `manager_pos_losses_index` | GET | `/manager/pos/losses` | POS_STOCK |
| `manager_pos_losses_create` | GET/POST | `/manager/pos/losses/create` | POS_STOCK |
| `manager_pos_losses_show` | GET | `/manager/pos/losses/{id}` | POS_STOCK |
| `manager_pos_losses_approve` | POST | `/manager/pos/losses/{id}/approve` | POS_MANAGE |
| `manager_pos_losses_reject` | POST | `/manager/pos/losses/{id}/reject` | POS_MANAGE |
| `manager_pos_losses_export_csv` | GET | `/manager/pos/losses/export` | POS_REPORTS |

## Controller Employer

### Routes ajoutées dans `EmployerStockController`

| Route | Méthode | Path | Voter |
|-------|---------|------|-------|
| `employer_pos_losses_create` | GET/POST | `/employer/pos/stock/losses/create` | POS_STOCK |
| `employer_pos_losses_index` | GET | `/employer/pos/stock/losses` | POS_STOCK |

---

## Templates

### Manager (4)
- `losses/index.html.twig` — Stats cards (total/soumis/approuvés/rejetés + valeur totale), filtres, table paginée, gradient rouge-orange
- `losses/create.html.twig` — Sélection produit (autocomplete), quantité, type dropdown, raison textarea, evidence textarea
- `losses/show.html.twig` — Détail avec gradient dynamique selon statut, timeline, modals Flowbite approuver/rejeter (pas de confirm())

### Employer (2)
- `employer/pos/stock/losses_create.html.twig` — Formulaire simplifié (produit, quantité, type, raison)
- `employer/pos/stock/losses_index.html.twig` — Liste de ses propres signalements

---

## Menu
- Manager : ajouter "Pertes & Casses" après "Stock" dans `_menu_manager.html.twig`
- Employer : ajouter lien "Signaler perte" dans la page stock employer

## Rapports
- Ajouter section "Pertes" dans `PosReportService.getStockReport()`
- Widget "Pertes récentes" sur dashboard rapports

## CSRF
- `approve` : `isCsrfTokenValid('approve_loss_{id}')`
- `reject` : `isCsrfTokenValid('reject_loss_{id}')`
- `create` : `isCsrfTokenValid('create_loss_report')`

## Validation
```bash
php bin/console doctrine:schema:update --force
php bin/console doctrine:schema:validate
php bin/console lint:container
php bin/console lint:twig templates/
php bin/console debug:router | grep losses
```
