# Cohérence de l'interface Communications — cap « feeling Missive »

> Statut : **PLAN** (2026-06-18). Refactor d'AFFICHAGE + threading — ne change pas les données stockées.
> Objectif : que notre Boîte soit aussi cohérente et intuitive que Missive.

## Diagnostic : une seule cause méta

Presque toutes les incohérences viennent du même défaut : **le même concept est calculé à
plusieurs endroits avec des logiques différentes** (liste vs détail vs ingestion). Missive est
cohérent parce que chaque concept a **une source de vérité**.
→ **Principe directeur : un *resolver* unique par concept, importé partout.**

## Inventaire (causes racines, vérifiées dans le code)

### 1. « à moi » au lieu de « à support » (attribution du destinataire)
- Rendu : `ConversationPanel.vue:126` (entête, `convTo`) + `:414` (`msgTo` → `msgRecipientList`, :1163-1186).
- Cause : `meAddr = msg.toEmail || convTo` puis `if addr === meAddr → 'moi'`. « moi » = **la boîte qui a REÇU** le courriel, PAS l'utilisateur connecté (`meEmail`, :1395, jamais consulté ici). Aucune table adresse-de-groupe → nom d'équipe.
- Correctif : table `support@→Support`, `facturation@→Facturation`… (depuis `SENDER_IDENTITIES` / `queueLabels`) ; « moi » seulement si l'adresse == `meEmail`.

### 2. Nom d'expéditeur vide / incohérent
- Rendu : liste `MessageList.vue:104` (finit par « Inconnu », OK) ; détail `msgWho` (:1577-1582) + avatar (:1569-1570).
- Cause : `fromName=''` si le From n'a pas de display-name → retombe sur `activeDiscussion.customerName` (peut être un AUTRE expéditeur dans un fil multi-personnes) ; avatar `staffInitials('')` → vide. Et `shortAgent` (local part minuscule, ex. « gilles.drolet ») vs `noteAuthorName` (Capitalisé) = **même personne affichée de 2 façons**.
- Correctif : un resolver `identity()` partagé, jamais vide, agents toujours capitalisés (réutiliser `noteAuthorName`).

### 3. Sur-regroupement (fils fusionnés à tort)
- Lieux : ingestion `conversation.js:722-734` + re-bucketing liste `listConversations` (:435-461).
- Causes : (a) courriel **sans sujet** → `findConversationByEmail:681` fusionne par email seul (garde sautée si un sujet est vide) ; (b) clé `:active` (:444) fusionne TOUS les fils actifs d'un même `baseKey` **sans borne de date** ; (c) SMS/téléphone fusionne par numéro **pour toujours** (sans sujet ni fenêtre) ; (d) `thread_id` Gmail pris tel quel (réutilisation de sujet → vieux fil) ; (e) **In-Reply-To/References stockés mais PAS utilisés** pour regrouper.
- Correctif : exiger un **signal de fil positif** (thread_id OU match References) avant de fusionner ; sujet vide/auto = non-concordant ; **fenêtre de récence** sur la clé `:active`.

### 4. Canal incohérent
- `conv.channel` (conversation) vs `msg.via` (message) — vocabulaires différents ; le **courriel n'a AUCUN badge** (`MessageList.vue:102` le masque) ; pas de badge par message.
- Correctif : `msg.via` = canal par message (vocabulaire normalisé) ; badge cohérent sur CHAQUE ligne/message, courriel inclus.

### 5. FILE + identité éclatées
- FILE : entête `activeConv.queue` (chargé async) vs liste `d.queue` → décalage bref. Identité : titre/chip/message utilisent **3 champs différents** (`customerName` / `coordAccountName` / `email`) → 3 noms pour le même fil.
- Correctif : FILE = une source (réconcilier une fois `activeConv` arrivé) ; « qui » = un seul computed.

### 6. Finitions incohérentes
- **Deux `formatTime`/`formatDate`** : local `ConversationPanel:2070-2087` (≠ canonique `useFormatters`) → liste en « DD/MM » (relTime) vs entête de message « mois jour HH:MM » ; le local **n'a pas le garde `isNaN`** → « Invalid Date ».
- Tri : message `emailDate||ts` vs liste/`lastTs` = `ts` → ordre divergent sur fils re-synchronisés.
- Non-lu / « en train d'écrire » pris du `latest` seul, pas du fil fusionné entier.
- Couleurs d'avatar : clés de hash différentes liste vs détail → même personne, couleurs différentes.

## Les resolvers (colonne vertébrale du correctif)

Un module partagé (ex. `composables/useConversationDisplay.js`) exporte **un resolver par concept**,
utilisé par la liste ET le détail (et côté hub, l'ingestion) :

| Concept | Resolver | Règle |
|---|---|---|
| Qui | `identity(p)` | nom → local part → tél → « Inconnu » ; agents capitalisés ; avatar/couleur/initiales dérivés du MÊME id |
| Destinataire | `recipientLabel(addr)` | adresse de groupe → nom d'équipe ; « moi » seulement si == utilisateur ; sinon nom/adresse |
| Canal | `channelOf(msg)` | depuis `via` normalisé → badge |
| Fil | `threadKey(msg)` | signal positif requis ; sujet vide = non-match ; fenêtre de récence |
| File | `fileOf(conv)` | une seule source |
| Temps | `useFormatters` partout | supprimer les copies locales |

## Patterns Missive à adopter (extraits du tour vidéo)

- **Ligne uniforme** : avatar + **badge de canal** + nom + heure + sujet + extrait + chips.
- **1 fil = 1 sujet** (titre visible → toute fausse fusion saute aux yeux).
- **Fil unifié** : messages client (blocs pleine largeur) + **notes internes (bulles)** + **tâches inline**, avec **2 composeurs** (répondre au client / note d'équipe).
- **Assign / Snooze / Close** en actions de barre supérieure (dropdowns riches).
- **Toasts Undo** sur actions réversibles.

## Plan par phases

| Phase | Contenu | Risque |
|---|---|---|
| **P1 — Cohérence** | resolvers `identity` + `recipientLabel` (« à support ») + temps unifié (tuer les `formatTime` locaux) + `fileOf` une source | **faible** (affichage) |
| **P2 — Threading strict** | signal de fil positif, sujet vide = non-match, fenêtre de récence, In-Reply-To/References utilisés | moyen (logique de fil → tester sur fils réels) |
| **P3 — Canal** | `channelOf` + badge partout (Messenger/courriel/SMS) | faible |
| **P4 — UX Missive** | fil unifié + 2 composeurs + assign/snooze + undo | moyen (UI) |

## Garde-fous

- Refactor d'**affichage** d'abord — ne touche pas aux données stockées.
- Threading (P2) : **préférer sur-séparer que sur-fusionner** — un faux split est bénin ; une fuite de conversation entre deux clients ne l'est pas.
- Tester P2 sur de vrais fils : multi-expéditeur, SMS long, adresse de groupe, sujet réutilisé.
- Chaque resolver = testable isolément (entrées → libellé attendu) avant branchement UI.
