diff --git a/.planning/STATE.md b/.planning/STATE.md index 732db63..42764b1 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -40,7 +40,7 @@ Phase 27 resta a metà — schema e fonti in prod, resto da scrivere. | Rifiniture — tassonomie, tab pagamenti, riordino task | ✅ in prod 2026-08-20 (`0019`) | | Portale cliente — stepper compatto/full-width, card offerta | ✅ in prod 2026-08-21 (`0020`); override provato su Caruso Speaker | | Chat — canali, modifica messaggi, firma admin | ✅ in prod 2026-08-21 (`0021`, `0022`); **mai provata a mano**; manca l'attribuzione | -| Conversazioni — scrivere per primo, menzioni `@Nome`, mail sul tag | ✅ pushata 2026-09-01 (`41530b5`), nessuna migration. Build + lint puliti, parser provato su 9 casi; **mai vista a schermo, nessuna mail di tag mai partita davvero** | +| Conversazioni — scrivere per primo, menzioni `@Nome`, mail sul tag | ✅ pushata 2026-09-01 (`41530b5`), nessuna migration. Menu dei tag corretto lo stesso giorno: mostrava **una riga per alias** invece che per persona. Build + lint puliti, 23 test sul parser e sul menu; **nessuna mail di tag mai partita davvero** | | Portale — stati task (forma, pill, legenda, «Cancellata») + date dei pagamenti | ✅ in prod 2026-08-22 (`2e9bd2a`, `8b54f48`, `fe76789`, migration `0023`); **mai visto a schermo**, nessuna `due_date` ancora inserita | | D — Whop → audit | ⏸️ dipende dal motore v2.5 | @@ -70,6 +70,7 @@ Passo per passo in `STATUS.md` e in `.claude/plans/v2.5-audit-motore.md`. Log completo in `PROJECT.md`. Vive per il lavoro corrente: +- **[2026-09-01] Una lista che serve a leggere non è una lista da offrire in scrittura** — il menu dei tag mostrava tre voci (nome, brand, nome di battesimo) per una persona sola, perché gli era stata passata la stessa lista che il parser usa per **riconoscere** un tag nel testo. Sono due domande diverse: *come lo si può scrivere* (tutti gli alias, invisibili) e *chi si può scegliere* (una riga per destinatario). Ora `MentionTarget` le tiene separate — `label` per il menu, `aliases` per la rilettura — e il filtro passa da `normalizeForSearch`, la stessa normalizzazione del parser: con un `toLowerCase()` a parte, digitare «nicolo» non troverebbe «Nicolò» nel menu mentre nel messaggio verrebbe riconosciuto. Il difetto scalava peggio del valore: con tre contatti sarebbero state nove righe. - **[2026-09-01] L'accesso a Gitea si recupera dalla sua CLI, non dal web** — il push rispondeva 403 (la credenziale nel portachiavi leggeva ma non scriveva) e l'account web non era piu' accessibile, quindi la via del browser era chiusa in partenza. Si rientra da dentro il container: `docker exec -u git gitea-tgw04ws48sogkso84oogwckk gitea admin user change-password` per la password e `... generate-access-token --scopes write:repository --raw` per il token del push. Il token va messo nel portachiavi con `git credential approve`, **non** nell'URL del remote: li' finirebbe in chiaro dentro `.git/config`. Da ricordare perche' senza push non esiste deploy — Coolify parte da `main` e basta. - **[2026-08-22] Un task cancellato esce dai denominatori, non dalla lista** — resta visibile barrato (il cliente ha letto quella voce e deve capire che fine ha fatto) ma non conta, via un solo `countsTowardProgress()` in `task-status.ts`: contarlo terrebbe la fase sotto il 100% per sempre, contarlo come fatto racconterebbe una consegna mai avvenuta. Se in una fase restano solo cancellati torna «Da iniziare»: degenere, ma «Completata» mentirebbe. - **[2026-08-22] Le date dei pagamenti sì, gli importi per riga no** — LOCKED #2 parla di cifre, non di date. Salvate a **mezzogiorno UTC** (a mezzanotte il giorno civile a Roma è già quello dopo) e contate sui giorni civili a Roma in `src/lib/payment-dates.ts`, lo stesso modulo del futuro promemoria email: mail e portale non devono contraddirsi su quanti giorni mancano. diff --git a/src/components/admin/conversazioni/ConversationsView.tsx b/src/components/admin/conversazioni/ConversationsView.tsx index 728de2f..0e07a42 100644 --- a/src/components/admin/conversazioni/ConversationsView.tsx +++ b/src/components/admin/conversazioni/ConversationsView.tsx @@ -290,7 +290,7 @@ function ActiveThread({ thread }: { thread: ConversationThread }) { key={m.id} message={m} clientId={thread.clientId} - mentionCandidates={thread.mentionCandidates} + mentionCandidates={thread.mentionAliases} /> )) )} @@ -302,7 +302,7 @@ function ActiveThread({ thread }: { thread: ConversationThread }) { channelKey={activeChannel} channelLabel={activeLabel} isGeneral={isGeneral} - mentionCandidates={thread.mentionCandidates} + mentionTargets={thread.mentionTargets} notifyEmailCount={thread.notifyEmailCount} /> > diff --git a/src/components/admin/conversazioni/ReplyComposer.tsx b/src/components/admin/conversazioni/ReplyComposer.tsx index 0e35a82..609cf88 100644 --- a/src/components/admin/conversazioni/ReplyComposer.tsx +++ b/src/components/admin/conversazioni/ReplyComposer.tsx @@ -5,6 +5,7 @@ import { AtSign } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; +import { normalizeForSearch, type MentionTarget } from "@/lib/mentions"; import { replyToConversation } from "@/app/admin/conversazioni/actions"; /** @@ -20,15 +21,19 @@ export function ReplyComposer({ channelKey, channelLabel, isGeneral, - mentionCandidates, + mentionTargets, notifyEmailCount, }: { clientId: string; channelKey: string; channelLabel: string; isGeneral: boolean; - /** I nomi con cui si può taggare questo cliente, dal più completo al più corto. */ - mentionCandidates: string[]; + /** + * Chi si può taggare: **una voce per destinatario**, non una per modo di + * scriverlo. Prima qui arrivava la lista piatta degli alias e il menu mostrava + * tre righe — nome, brand, nome di battesimo — per una persona sola. + */ + mentionTargets: MentionTarget[]; /** Quante mail partirebbero davvero con un tag. Zero = si avvisa, non si tace. */ notifyEmailCount: number; }) { @@ -43,10 +48,14 @@ export function ReplyComposer({ const suggestions = useMemo(() => { if (!trigger) return []; - const q = trigger.query.toLowerCase(); - // Il nome intero per primo: è quello che si vuole quasi sempre. - return mentionCandidates.filter((c) => c.toLowerCase().startsWith(q)).slice(0, 4); - }, [trigger, mentionCandidates]); + const q = normalizeForSearch(trigger.query); + // Si filtra sugli alias ma si tiene il TARGET: digitando «car» il cliente si + // trova passando dal brand, e resta comunque una riga sola col suo nome. + // La normalizzazione è quella del parser, così «nicolo» trova «Nicolò». + return mentionTargets + .filter((t) => t.aliases.some((a) => normalizeForSearch(a).startsWith(q))) + .slice(0, 6); + }, [trigger, mentionTargets]); const open = suggestions.length > 0; @@ -122,8 +131,8 @@ export function ReplyComposer({ aria-label="Tagga il cliente" className="absolute bottom-full left-0 z-20 mb-2 w-64 overflow-hidden rounded-lg border border-border bg-card shadow-lg" > - {suggestions.map((candidate, i) => ( -