# Rapport v1.5.4 — Un écran, une décision

Quatre remarques d'usage avant le déploiement de la v1.5.3. La deuxième faussait
un chiffre regardé tous les matins ; la quatrième est un travail de conception
avant d'être un travail de code.

**Recette** : 750 tests, 3 697 assertions, tout au vert (723 avant le lot).
`./vendor/bin/pint` passé, `npm run build` relancé, une migration.

---

## 1. Les barres d'actions ne débordent plus

**Le défaut.** `ViewDomiciliationClient` alignait dix actions de même poids
visuel : déposer des pièces, analyser, vérifier, ajouter un contrat, générer un
document, établir un devis, encaisser, relancer, relevé de compte, modifier. Sur
un écran ordinaire, la moitié sortait du cadre et devenait inatteignable. Ce
n'était pas un problème de largeur mais de hiérarchie : quand tout ressort, rien
ne ressort.

**Le correctif.** Quatre gestes du quotidien restent en évidence — déposer,
analyser, encaisser, modifier —, les six autres tiennent dans un menu unique
intitulé « Autres actions », et non trois points muets.
`ViewDomiciliationClient::ACTIONS_EN_EVIDENCE` fixe le plafond à cinq entrées de
premier niveau, et un test le vérifie.

**La revue.** La fiche client était la pire, elle n'était pas la seule : la page
« Modifier » alignait six boutons, dont quatre destructifs ou secondaires — elle
en garde deux, le reste passe au menu. Les autres barres du panneau restent sous
la limite (l'échéancier, les listes de ressources : deux ou trois entrées, les
actions de ligne déjà regroupées).

## 2. Un dossier annulé ne réclame plus d'argent

**Le défaut.** `EcheancierGlobal::requeteDeBase()` ne filtrait que sur le statut
de l'échéance, jamais sur celui du client. Le dossier LUCALI, passé en
« annulé », gardait son échéance de 732 DH en retard : elle figurait à
l'échéancier, comptait dans les impayés du tableau de bord, et alimentait la
chaîne de relance.

**Deux règles, et surtout pas une seule.** Elles vivent côte à côte dans
`Services\AssainissementEcheances`, et nulle part ailleurs :

| Le dossier | Ce qui se passe | Pourquoi |
|---|---|---|
| **Annulé** | Toutes les échéances non réglées passent à « annulée » | Il n'a jamais pris effet : ses échéances ne sont pas des créances |
| **Résilié** | Seules les échéances **postérieures à la date d'effet** sont annulées | Il a existé : ce qui était dû avant reste dû, et se réclame |

Les confondre ferait perdre de l'argent réellement dû.

**Ce qui n'est jamais touché.** Une échéance réglée. On n'efface pas un
encaissement : l'argent est entré, il est au journal, la facture a été émise. Un
test le vérifie jusqu'à l'opération de trésorerie.

**Annulée, jamais masquée.** L'échéance porte sa `date_annulation` et son
`motif_annulation` — deux colonnes ajoutées par migration. Une créance masquée se
perd de vue sans qu'on sache pourquoi ; une créance annulée avec son motif se
relit six mois plus tard.

**Quand la règle s'applique.** `Observers\DossierClotureObserver`, au moment où
le dossier change d'état ou reçoit sa date d'effet. La règle ne dépend donc pas
qu'on pense à jouer une commande.

**Le rattrapage.** `php artisan reprise:assainir-les-echeances`, avec
`--simulation` qui liste avant d'écrire.

**Ce que la simulation trouve, et ce qu'elle ne peut pas trancher.** Sur le jeu de
données de développement — 5 dossiers annulés, 9 résiliés — elle annule
**10 échéances pour 8 300 DH** sur trois dossiers annulés. Les cinq dossiers
résiliés qui portent encore 16 échéances pour **12 100 DH** n'ont **aucune date
d'effet** : ni sur la fiche, ni dans un acte de résiliation. Rien n'est annulé
chez eux, et c'est volontaire — sans cette date, on ignore ce qui était dû avant,
et deviner reviendrait à jeter une créance réelle.

Se taire aurait été pire : l'exploitant aurait joué le rattrapage et cru le
ménage fait. La commande les nomme, avec ce qu'ils tiennent en suspens, et dit
quoi faire — inscrire la date de résiliation sur la fiche. L'assainissement se
fait alors de lui-même, sans rejouer la commande.

**De combien les impayés baissent, et pourquoi.** Sur ce même jeu de données, les
impayés passent de **97 740 DH à 89 440 DH** : −8 300 DH, exactement les
échéances des trois dossiers annulés. Les 12 100 DH des résiliés sans date
suivront dès que l'exploitant aura renseigné les dates. Ce montant n'était pas
recouvrable — il était seulement compté.

**Et le statut du dossier figure désormais sur chaque ligne**, à l'échéancier
comme au recouvrement : voir « en retard » sans savoir que le dossier est résilié
depuis six mois, c'est relancer quelqu'un qui n'est plus client.

## 3. Le logo ne disparaît plus en thème sombre

**Le défaut.** Sur un téléphone en thème sombre, le mot « Mobdie » et le texte
arabe s'effaçaient : ils sont noirs, et le client de messagerie avait assombri le
fond blanc de l'en-tête. Seules les formes colorées restaient.

**Deux parades combinées, et non une seule.** Aucune ne suffit :

1. `color-scheme` et `supported-color-schemes` déclarés dans l'en-tête, plus une
   requête `@media (prefers-color-scheme: dark)` qui échange `logo-email.png`
   contre `logo-email-sombre.png` — texte en blanc, formes inchangées. Les
   clients qui lisent ces déclarations (Apple Mail, iOS, Outlook.com) cessent
   d'inverser d'autorité et respectent la bascule.
2. **Un fond clair explicite, posé en attribut `bgcolor` et pas seulement en
   style**, sur toute cellule portant le logo. Gmail sur Android force
   l'inversion et ignore la requête de média — et, sur certaines versions, la
   balise `style` elle-même. Seul l'attribut tient.

Choisir l'une des deux, c'est accepter que la moitié des destinataires reçoive un
en-tête vide.

**Le pied de page.** Même mal : un gris clair sur fond clair devient illisible
une fois le fond inversé. Le `#9ca3af` disparaît des gabarits au profit de gris
plus soutenus, les fonds sont posés en attribut, et une classe `texte-discret`
les éclaircit encore en thème sombre.

**Ce que cela change au contrat des gabarits.** L'application avait pour règle de
n'employer aucune feuille de style incorporée — Outlook les ignore. Il y en a
désormais une, et une seule chose y vit : la bascule des logos et des gris. Toute
la mise en page reste portée par les styles en ligne et les attributs. Le test
qui interdisait `<style>` vérifie maintenant que rien d'autre ne s'y glisse
(`padding`, `font-size`, `width` y sont interdits).

**La limite, dite franchement.** La bascule a été vérifiée par le rendu HTML et
les tests, pas sur un parc d'appareils. Sur Gmail Android, c'est le fond
explicite qui protège le logo, non la bascule — le logo affiché y restera celui à
texte noir, sur un fond blanc qui tient. À vérifier sur les deux thèmes avant de
conclure.

## 4. Le recouvrement : un écran par décision

**Le principe.** L'échéancier reste ce qu'il est : la liste exhaustive, celle
qu'on ouvre pour retrouver une échéance. Il ne répond pas à la question du matin.
`Filament\Pages\Recouvrement` y répond, et suit une seule règle de conception :
ce qui sépare deux groupes n'est pas leur âge, c'est le geste qu'ils appellent.
L'âge n'est que le critère qui range.

**Les six étapes**, nommées par ce qu'on y fait :

| Étape | Tranche | Le geste | Registre |
|---|---|---|---|
| À venir | pas encore dû | Préparer, prévenir | info |
| À relancer | < 30 j | Un rappel courtois | warning |
| À rappeler fermement | 1 à 3 mois | Relance ferme, et un appel | warning |
| À mettre en demeure | 3 à 6 mois | La mise en demeure | danger |
| À trancher | 6 à 12 mois | Poursuivre, échelonner, ou sortir | danger |
| À sortir | > 12 mois | Engager résiliation et radiation | **gris** |

« À sortir » n'est pas rouge, et ce n'est pas un oubli : le rouge dit « agis
maintenant », alors qu'une créance de quatorze mois appelle une décision
réfléchie. Un écran qui crie à propos de tout ne fait plus crier personne. La
valeur de cet onglet n'est pas de récupérer de l'argent, c'est de cesser de
porter le risque de l'article 544-4.

**Ce qui fait qu'une étape est une étape.** Une action dominante, la même pour
toutes les lignes, en bouton sur chaque ligne et en action groupée en tête
(« Relancer les échéances choisies ») — elle met le message en file d'envoi, et
rien ne part sans validation humaine, comme partout ailleurs. Les autres actions
— encaisser, facturer, ouvrir la fiche, relevé — restent, en retrait, dans un
menu.

**Des colonnes propres à chaque décision.** À relancer : client, statut du
dossier, montant, ancienneté, dernière relance. À rappeler : le téléphone en
plus, marqué en rouge quand il manque — on n'appelle pas un injoignable. À
sortir : le dû total, le dernier paiement reçu, depuis quand on domicilie. Une
colonne qui ne sert pas à la décision de cet onglet n'y figure pas.

**Un en-tête qui dit l'enjeu** en une phrase et deux chiffres : combien
d'échéances, combien de dirhams. Rien de plus. Et un onglet vide le dit —
« Aucune échéance à mettre en demeure », suivi de « Rien à faire ici aujourd'hui
— et c'est une bonne nouvelle ».

**Les impayés en deux parts, partout.** `Recouvrement::impayes()` fait la coupure
à la borne de la mise en demeure, et les trois écrans la lisent : le tableau de
bord annonce « À aller chercher » et « À solder » au lieu d'un montant unique ;
l'échéancier fait de même. « 63 152 DH d'impayés » décourage ; deux chiffres
appellent deux gestes.

**Les bornes sont des réglages.** Un onglet « Recouvrement » dans les réglages
porte les quatre seuils. `Support\Recouvrement` est seul à les lire, et un test
vérifie qu'en déplaçant la borne de 30 à 60 jours, une échéance de 46 jours
change bien d'onglet.

**Le jour même n'est pas un retard.** Une échéance qui tombe aujourd'hui
appartient au bloc « Aujourd'hui » de l'onglet « À venir » : le client a jusqu'à
ce soir. Le retard commence le lendemain, et un test le vérifie.

**Une différence de comptage qu'il faut connaître.** Les étapes se calculent sur
la **date d'échéance**, non sur la colonne `statut`. Sur le jeu de données de
développement, `statut = 'en_retard'` compte 62 échéances pour 56 400 DH, quand
la lecture par date en trouve 78 pour 97 740 DH : l'écart vient d'échéances dont
le statut n'avait pas été rafraîchi. La date fait foi, et les trois écrans
comptent désormais pareil. L'exploitant verra donc les impayés monter d'un côté
(les retards que le statut ignorait) et baisser de l'autre (les dossiers clos) —
les deux mouvements vont dans le sens de la justesse.

**Une réserve.** Les quatre blocs de l'onglet « À venir » — aujourd'hui, cette
semaine, ce mois-ci, plus tard — sont bien des blocs repliables, mais Filament
n'offre pas de replier sélectivement : ils s'ouvrent tous les quatre, dans cet
ordre, et se replient à la main. Les deux premiers étant en tête, l'effet
pratique est proche de ce qui était demandé.

---

## Ce qu'il faut vérifier après déploiement

1. `php artisan reprise:assainir-les-echeances --simulation` : lire la première
   table (ce qui sera annulé) **et la seconde** (les résiliés sans date d'effet).
   Jouer ensuite la commande sans `--simulation`.
2. Renseigner la date de résiliation des dossiers listés dans la seconde table :
   leurs échéances postérieures s'annuleront d'elles-mêmes à l'enregistrement.
3. Tableau de bord : « À aller chercher » et « À solder » remplacent le montant
   unique. Le total des deux doit égaler celui du recouvrement.
4. `Domiciliation → Recouvrement` : parcourir les six onglets, vérifier que les
   comptes des pastilles correspondent aux tableaux, et qu'aucune échéance d'un
   dossier annulé n'y figure plus.
5. Réglages → Recouvrement : déplacer une borne, revenir au recouvrement,
   vérifier que les échéances ont changé d'onglet.
6. Fiche client : quatre boutons et un menu « Autres actions », rien qui déborde.
7. S'envoyer un message dans chaque mise en page, et le lire sur un téléphone en
   thème clair **puis** en thème sombre.
