From 2e9bd2ab607459ceb4af7d83888dc618a4c66e7d Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Sat, 22 Aug 2026 14:24:35 +0200 Subject: [PATCH] feat(portale): gli stati dei task si distinguono, e "in revisione" dice di chi e' la palla MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nella Timeline — la vista di default — lo stato di un task era solo un cerchietto colorato: ambra col puntino da 1.5px per "in corso", violetto col puntino da 2px per "in revisione". A 20px sono lo stesso oggetto. Solo "in revisione" aveva un title; gli altri tre stati non avevano ne' testo, ne' tooltip, ne' aria-label — l'opposto della regola UX 3 del design system, che chiede colore + testo, mai colore da solo. Il punto pero' non era la somiglianza fra i due colori. "In revisione" non significa quello che dice 5547e55, che lo descriveva come controllo qualita' interno: nel flusso reale vuol dire "consegnato, aspetta l'OK del cliente". E' uno stato che richiede un'azione, e per questo un cerchietto muto era il problema vero. - Le quattro icone si distinguono per forma, non solo per colore, quindi reggono anche in bianco e nero e con un daltonismo: vuoto -> punto -> spunta vuota -> spunta piena. La spunta compare a lavoro finito e si riempie a lavoro confermato. - Pill di testo solo su "in corso" e "in revisione". "Da fare" e' un cerchio vuoto e "fatto" ha il titolo barrato: etichettarli aggiungeva rumore, non informazione. - Il conteggio dei task in attesa sta nell'header della fase, quindi si legge anche a card chiusa. Serve davvero: l'admin puo' forzare una fase su "completata" dalla select, e in quel caso la card parte collassata con dentro la richiesta. - Legenda + micro-copy sopra entrambe le viste: chi revisiona, e che ogni pagina include un giro di revisione — l'informazione commerciale che il cliente non aveva da nessuna parte. Rimanda alla chat della fase, che esiste gia'. - Colonne del kanban cliente colorate con le stesse tinte dell'icona in Timeline: prima erano quattro colonne grigie identiche. - Icona aria-hidden + sr-only, cosi' lo screen reader annuncia lo stato anche sui due che non portano la pill. La barra di avanzamento non e' stata toccata: un task in revisione conta come uno in corso, cioe' zero. Cambia il tipo di lavoro, non l'avanzamento — gonfiare la percentuale le avrebbe fatto dire una cosa che il contatore "x di y task" smentiva una riga sotto. Visual e copy in un solo file, e la legenda si genera da TASK_STATUSES: un quinto stato non lascera' indietro una lista scritta a mano. Stessa disciplina di 5547e55, nata proprio perche' due liste fisse avevano fatto sparire dei task senza un errore. Nessuna migration: e' solo UI. Co-Authored-By: Claude Opus 5 --- .planning/STATE.md | 13 +- STATUS.md | 47 +++++- design-reference/DESIGN-SYSTEM.md | 10 ++ src/components/client/PhaseCard.tsx | 73 ++++---- src/components/client/TaskStatusIndicator.tsx | 158 ++++++++++++++++++ src/components/client/kanban/ClientKanban.tsx | 47 ++++-- .../client/kanban/PhaseViewToggle.tsx | 6 +- 7 files changed, 285 insertions(+), 69 deletions(-) create mode 100644 src/components/client/TaskStatusIndicator.tsx diff --git a/.planning/STATE.md b/.planning/STATE.md index 7006ec7..36134cf 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,9 +3,9 @@ gsd_state_version: 1.0 milestone: v2.5 milestone_name: Audit status: executing -stopped_at: "v2.5 in PAUSA. Modifiche hub: A, B, C1, rifiniture, chat a canali (0021) e modifica messaggi + firma (0022) in prod; C2 (TidyCal) bloccato sulle credenziali API. Niente della chat e' stato provato a mano: .env.local non autentica piu'." -last_updated: "2026-08-21T20:55:00.000Z" -last_activity: 2026-08-21 -- modifica dei messaggi (0022) e firma di chi risponde in chat +stopped_at: "v2.5 in PAUSA. Modifiche hub: A, B, C1, rifiniture, chat a canali (0021) e modifica messaggi + firma (0022) in prod; C2 (TidyCal) bloccato sulle credenziali API. Stati dei task nel portale: scritti, build verde, da vedere a schermo. Niente della chat e' stato provato a mano: .env.local non autentica piu'." +last_updated: "2026-08-22T12:10:00.000Z" +last_activity: 2026-08-22 -- stati dei task leggibili nel portale cliente (icone, pill, legenda) progress: total_phases: 4 completed_phases: 0 @@ -41,6 +41,7 @@ Phase 27 resta a metà — schema e fonti in prod, resto da scrivere. | Portale cliente — stepper compatto/full-width, card offerta | ✅ in prod 2026-08-21 (`0020`); override provato su Caruso Speaker | | Chat a canali — portale + inbox admin per canale | ✅ in prod 2026-08-21 (`0021`); build verde, **mai provata a mano** | | Chat — modifica messaggi + firma admin da `settings` | ✅ in prod 2026-08-21 (`0022`); menzioni rimandate, manca l'attribuzione | +| Portale — stati task leggibili (forma + pill + legenda) | 🔨 scritto 2026-08-22, build verde, **da vedere a schermo** | | D — Whop → audit | ⏸️ dipende dal motore v2.5 | Progress: [███░░░░░░░] 25% (v2.5) @@ -92,7 +93,7 @@ Log completo in `PROJECT.md`. Vive per il lavoro corrente: ## Session Continuity -Last session: 2026-08-21 -Stopped at: **modifica dei messaggi** (stile Slack: illimitata, senza storico, etichetta «modificato» — scelta deliberata, motivata in `STATUS.md`) e **firma di chi risponde**, che era la stringa `iamcavalli` cablata nel pannello. Migration `0022` applicata a prod prima del push e riletta a conferma; il nome e la foto (URL esterno, l'upload su volume non esiste) stanno in `settings`, nessuna migration. La lezione da non perdere: il difficile non era scrivere la modifica ma **propagarla** — il poll filtra su `created_at`, che una modifica non cambia, quindi servono insieme il filtro allargato a `edited_at` **e** il watermark del client sul massimo dei due; una sola delle due e o la modifica non arriva, o arriva a ogni giro per sempre. Build e lint verdi. **Niente di tutto questo è stato provato a mano.** -Next: (1) provare la chat in prod — la verifica che conta è modificare un messaggio admin con la chat del cliente aperta e vedere il testo cambiare da solo entro ~20s senza duplicarsi; (2) sbloccare TidyCal con token o documentazione; (3) `LEAD_WEBHOOK_SECRET` su Coolify — finché manca la route risponde 403 a tutti; (4) poi v2.5 da `src/lib/audit/schema.ts` + `agents/`. +Last session: 2026-08-22 +Stopped at: **stati dei task leggibili nel portale cliente.** Nella Timeline lo stato era solo un cerchietto colorato — ambra e violetto indistinguibili a 20px, e tre stati su quattro senza alcun testo. Ora le quattro icone si distinguono per **forma** (vuoto → punto → spunta vuota → spunta piena), la pill di testo sta solo sui due stati ambigui, l'header della fase conta i task in attesa anche a card chiusa, e una legenda dice chi revisiona e che **ogni pagina include un giro di revisione**. La correzione che conta: **«In revisione» significa «aspetta l'OK del cliente», non controllo qualità interno** come scrive `5547e55` — è uno stato che *richiede un'azione*, e per questo non poteva restare muto. Barra di avanzamento invariata di proposito: cambia il tipo di lavoro, non l'avanzamento. Build, typecheck e lint verdi, nessuna migration. **Non ancora visto a schermo.** +Next: (1) verificare in prod con `?preview=1` su **Caruso Speaker, fase «3 - Esecuzione»** — l'unica dove «In corso» e «In revisione» convivono (4 + 2 su 11), quindi l'unica dove si vede se ora si distinguono; (2) provare la chat in prod: modificare un messaggio admin e vederlo cambiare da solo entro ~20s senza duplicarsi; (3) sbloccare TidyCal con token o documentazione; (4) `LEAD_WEBHOOK_SECRET` su Coolify, senza cui `/api/webhooks/lead` risponde 403 a tutti; (5) poi v2.5 da `src/lib/audit/schema.ts` + `agents/`. Resume file: None diff --git a/STATUS.md b/STATUS.md index 0cf20e0..c25a32e 100644 --- a/STATUS.md +++ b/STATUS.md @@ -1,6 +1,6 @@ # ClientHub (IAMCAVALLI) — Status -_Ultimo aggiornamento: 2026-08-21_ +_Ultimo aggiornamento: 2026-08-22_ Questo è **l'unico documento narrativo** del progetto: a che punto siamo, cosa manca, cosa abbiamo imparato. `.planning/STATE.md` è il digest che leggono i comandi @@ -248,6 +248,51 @@ già annotato in `client-chat.ts` come «un lavoro a sé». Ordine giusto: **att poi notifiche (Resend c'è già), poi eventualmente menzioni. Molto probabilmente vedere *chi* ha scritto risolve gran parte del problema da solo. +### Stati dei task, resi leggibili nel portale (2026-08-22) + +Nella Timeline — la vista di default — lo stato di un task era **solo un cerchietto +colorato**: ambra col puntino da 1.5px per «In corso», violetto col puntino da 2px per +«In revisione». A 20px sono lo stesso oggetto. Solo «In revisione» aveva un `title`; gli +altri tre stati non avevano né testo, né tooltip, né `aria-label`. Il design system chiede +l'opposto, ed è la sua regola UX 3: **colore + testo, mai colore da solo**. + +**«In revisione» non significa quello che dice il commit che l'ha introdotto.** `5547e55` +lo descriveva come controllo qualità interno («finito, ma da controllare prima di +consegnarlo»). Nel flusso reale è **«consegnato, aspetta l'OK del cliente»**: la palla è al +cliente. Un cerchietto muto per uno stato che *richiede un'azione* era il problema vero, +più della somiglianza fra i due colori. + +Cosa è cambiato: + +- **Le quattro icone si distinguono per forma**, non solo per colore, quindi reggono anche + in bianco e nero e con un daltonismo: vuoto → punto → **spunta vuota** → spunta piena. La + spunta compare quando il lavoro è finito e si riempie quando è confermato. +- **Pill di testo solo su «In corso» e «In revisione».** «Da fare» è un cerchio vuoto e + «Fatto» ha il titolo barrato: etichettarli aggiungeva rumore senza aggiungere informazione. +- **Il conteggio dei task in attesa sta nell'header della fase**, accanto a «4 di 8 task», + quindi si legge anche a card chiusa. Serve davvero: l'admin può forzare una fase su + «Completata» dalla select, e in quel caso la card parte collassata con dentro la richiesta. +- **Legenda + micro-copy** sopra entrambe le viste, che dice chi revisiona e che **ogni + pagina include un giro di revisione** — l'informazione commerciale che il cliente non + aveva da nessuna parte. Rimanda alla chat della fase, che esiste già. +- **Colonne del Kanban cliente colorate**, con la stessa tinta dell'icona in Timeline: + prima erano quattro colonne grigie identiche. +- **Lo screen reader legge lo stato su tutti e quattro** (icona `aria-hidden` + `sr-only`), + non solo sui due che portano la pill. + +**La barra di avanzamento non è stata toccata.** Un task in revisione conta come uno in +corso — cioè zero. Cambia il *tipo* di lavoro, non l'avanzamento: gonfiare la percentuale +avrebbe fatto dire alla barra una cosa che il contatore «x di y task» smentiva una riga +sotto. + +Visual e copy stanno in un solo file, `src/components/client/TaskStatusIndicator.tsx`, e la +legenda si genera da `TASK_STATUSES`: aggiungere un quinto stato non lascia indietro una +lista scritta a mano. È la stessa disciplina di `5547e55`, nata proprio perché due liste +fisse avevano fatto sparire dei task dal portale **senza un errore**. + +⚠️ **Scritto e compilato, non ancora visto a schermo.** Build e typecheck verdi, lint +invariato (12 errori preesistenti, nessuno nei file toccati). Nessuna migration: è solo UI. + ### v2.5 — Audit (Phases 27–30), in pausa Il servizio di analisi sito (tre livelli: **Radiografia / Prima-Dopo / Rotta**) diventa diff --git a/design-reference/DESIGN-SYSTEM.md b/design-reference/DESIGN-SYSTEM.md index c7d9ce5..49efc32 100644 --- a/design-reference/DESIGN-SYSTEM.md +++ b/design-reference/DESIGN-SYSTEM.md @@ -389,6 +389,16 @@ The `/client/[token]` portal was migrated to the same "Quiet Luxury" tokens `bg-muted`/`text-muted-foreground` (upcoming) — each with a `dark:` variant. - **Phase cards** (`PhaseCard`): `rounded-xl border-border-light bg-card shadow-card`, progress bar colored per status (emerald/amber/`bg-border`). +- **Stato dei task** (`TaskStatusIndicator`): le quattro forme si distinguono + **senza colore** — `todo` cerchio vuoto `border-border`, `in_progress` cerchio + + punto ambra, `in_review` cerchio + **spunta vuota** violetta, `done` cerchio pieno + + spunta emerald. La spunta compare a lavoro finito e si riempie a lavoro confermato. + La pill di testo compare **solo** su `in_progress` e `in_review`; `todo` e `done` si + leggono dalla forma e dal titolo barrato. Colori come le altre pill del portale (soft + tint `-50/700` + variante `dark:`), tutti raccolti in `TASK_STATUS_VISUALS` e mai + riscritti nei componenti: le stesse tinte colorano le colonne del Kanban cliente e la + legenda, che si genera da `TASK_STATUSES`. L'icona è `aria-hidden` e lo stato viaggia + in uno `sr-only`, così lo screen reader lo annuncia anche sui due senza pill. - Sidebar cards (`OffersSection`, `PaymentStatus`, `DocumentsSection`, `NotesSection`, `TranscriptsSection`) all follow `rounded-xl border-border-light bg-card shadow-card`. diff --git a/src/components/client/PhaseCard.tsx b/src/components/client/PhaseCard.tsx index da7eb9c..00ad613 100644 --- a/src/components/client/PhaseCard.tsx +++ b/src/components/client/PhaseCard.tsx @@ -3,8 +3,8 @@ import { useState } from "react"; import { ApproveButton } from "@/components/client/ApproveButton"; import { useChatContext } from "@/components/client/ChatProvider"; +import { TaskStatusIcon, TaskStatusPill } from "@/components/client/TaskStatusIndicator"; import type { ClientView } from "@/lib/client-view"; -import { TASK_STATUS_LABELS, type TaskStatus } from "@/lib/task-status"; type Phase = ClientView["phases"][number]; @@ -29,36 +29,6 @@ const phaseBarColor: Record<"upcoming" | "active" | "done", string> = { done: "bg-emerald-600", }; -function TaskStatusIcon({ status }: { status: TaskStatus }) { - if (status === "done") { - return ( - - ✓ - - ); - } - if (status === "in_review") { - return ( - - - - ); - } - if (status === "in_progress") { - return ( - - - - ); - } - return ( - - ); -} - export function PhaseCard({ phase, token, @@ -71,6 +41,10 @@ export function PhaseCard({ const [open, setOpen] = useState(defaultOpen); const { openChat } = useChatContext(); const doneCount = phase.tasks.filter((t) => t.status === "done").length; + // I task in revisione aspettano il cliente, non noi. Il conteggio sta nell'header, + // che resta visibile anche a card chiusa: altrimenti la richiesta si nasconde + // dentro una fase collassata e il cliente non sa che tocca a lui. + const reviewCount = phase.tasks.filter((t) => t.status === "in_review").length; return (
@@ -118,9 +92,17 @@ export function PhaseCard({ {/* Progress bar — always visible, colore per stato */}
-
-

{doneCount} di {phase.tasks.length} task

-

{phase.progress_pct}%

+
+

+ {doneCount} di {phase.tasks.length} task + {reviewCount > 0 && ( + + {" · "} + {reviewCount} in attesa di riscontro + + )} +

+

{phase.progress_pct}%

-

- {task.title} -

+ {/* flex-wrap: su un titolo lungo la pill va a capo invece di + schiacciare il testo */} +
+

+ {task.title} +

+ +
{task.description && (

{task.description} diff --git a/src/components/client/TaskStatusIndicator.tsx b/src/components/client/TaskStatusIndicator.tsx new file mode 100644 index 0000000..5c18137 --- /dev/null +++ b/src/components/client/TaskStatusIndicator.tsx @@ -0,0 +1,158 @@ +// ── Come lo stato di un task appare al cliente ─────────────────────────────── +// Le etichette stanno in src/lib/task-status.ts e sono condivise con l'admin. +// Qui c'è solo il livello client-facing: forma, colore e la frase che spiega lo +// stato a chi non lavora al progetto. +// +// Prima di questo file lo stato era un cerchietto senza testo: ambra col puntino +// da 1.5px per "in corso", violetto col puntino da 2px per "in revisione". A 20px +// sono lo stesso oggetto, e il design system chiede il contrario — colore + testo, +// mai colore da solo. Ora le quattro forme si distinguono anche in bianco e nero: +// vuoto → punto → spunta vuota → spunta piena. La spunta compare quando il lavoro +// è finito e si riempie quando è confermato. + +import { TASK_STATUSES, TASK_STATUS_LABELS, type TaskStatus } from "@/lib/task-status"; + +type StatusVisual = { + /** Classi del cerchio nella lista task. */ + ring: string; + /** Classi della pill di testo. `null` = stato che non ne ha bisogno: "da fare" + * è un cerchio vuoto e "fatto" è barrato, si leggono senza etichetta. Tenerlo + * come dato della mappa evita che "quali stati hanno la pill" finisca in un if. */ + pill: string | null; + /** Pallino per legenda e intestazioni del kanban. */ + dot: string; + /** Colore dell'etichetta nelle intestazioni del kanban. */ + label: string; + /** Tinta del contatore in cima alla colonna kanban. */ + count: string; + /** Frase client-facing: legge lo screen reader, e sta nella legenda. */ + hint: string; +}; + +// Colori di stato: l'eccezione sanzionata alla regola dei soli token semantici +// (DESIGN-SYSTEM.md). Soft tint come le altre pill del portale, ogni valore con +// la sua variante dark — che l'icona "in corso" non aveva. +export const TASK_STATUS_VISUALS: Record = { + todo: { + ring: "border-2 border-border bg-card", + pill: null, + dot: "bg-border", + label: "text-muted-foreground", + count: "bg-muted text-muted-foreground", + hint: "Da fare — non ancora iniziato.", + }, + in_progress: { + ring: "border-2 border-amber-400 bg-card dark:border-amber-500", + pill: "bg-amber-50 text-amber-700 border border-amber-100 dark:bg-amber-500/10 dark:text-amber-400 dark:border-amber-500/20", + dot: "bg-amber-500 dark:bg-amber-400", + label: "text-amber-700 dark:text-amber-400", + count: "bg-amber-50 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400", + hint: "In corso — ci stiamo lavorando noi.", + }, + in_review: { + ring: "border-2 border-violet-400 bg-card dark:border-violet-500", + pill: "bg-violet-50 text-violet-700 border border-violet-100 dark:bg-violet-500/10 dark:text-violet-400 dark:border-violet-500/20", + dot: "bg-violet-500 dark:bg-violet-400", + label: "text-violet-700 dark:text-violet-400", + count: "bg-violet-50 text-violet-700 dark:bg-violet-500/10 dark:text-violet-400", + hint: "In revisione — pronto, aspetta il tuo riscontro.", + }, + done: { + ring: "bg-emerald-50 text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400", + pill: null, + dot: "bg-emerald-500 dark:bg-emerald-400", + label: "text-emerald-700 dark:text-emerald-400", + count: "bg-emerald-50 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400", + hint: "Fatto — completato.", + }, +}; + +function CheckGlyph({ className }: { className: string }) { + return ( + + ); +} + +/** + * Il cerchio di stato accanto al titolo del task. + * + * L'icona è decorativa (`aria-hidden`) e lo stato viaggia in uno `sr-only`: così + * lo screen reader lo annuncia su tutti e quattro gli stati, mentre a schermo solo + * i due ambigui portano la pill. + */ +export function TaskStatusIcon({ status }: { status: TaskStatus }) { + const v = TASK_STATUS_VISUALS[status]; + + return ( + <> +

+
    + {TASK_STATUSES.map((status) => ( +
  • +
  • + ))} +
+

+ In revisione — il lavoro è pronto e + aspetta il tuo riscontro: scrivicelo dalla chat della fase. Ogni pagina include un giro di + revisione. +

+
+ ); +} diff --git a/src/components/client/kanban/ClientKanban.tsx b/src/components/client/kanban/ClientKanban.tsx index e28c608..08c2220 100644 --- a/src/components/client/kanban/ClientKanban.tsx +++ b/src/components/client/kanban/ClientKanban.tsx @@ -2,6 +2,7 @@ import type { ClientView } from "@/lib/client-view"; import { ApproveButton } from "@/components/client/ApproveButton"; +import { TASK_STATUS_VISUALS } from "@/components/client/TaskStatusIndicator"; import { TASK_STATUS_LABELS, TASK_STATUSES, type TaskStatus } from "@/lib/task-status"; type Task = ClientView["phases"][number]["tasks"][number] & { @@ -59,24 +60,34 @@ export function ClientKanban({ phases, token }: { phases: ClientView["phases"]; return (
- {COLUMNS.map((col) => ( -
-
- {col.label} - - {tasksByStatus[col.id].length} - + {COLUMNS.map((col) => { + // Stessa tinta dell'icona in Timeline: passando da una vista all'altra + // "in corso" e "in revisione" restano riconoscibili senza rileggere. + const v = TASK_STATUS_VISUALS[col.id]; + return ( +
+
+
+
+ + {tasksByStatus[col.id].length} + +
+
+ {tasksByStatus[col.id].map((task) => ( + + ))} + {tasksByStatus[col.id].length === 0 && ( +

Nessun task

+ )} +
-
- {tasksByStatus[col.id].map((task) => ( - - ))} - {tasksByStatus[col.id].length === 0 && ( -

Nessun task

- )} -
-
- ))} + ); + })}
); -} \ No newline at end of file +} diff --git a/src/components/client/kanban/PhaseViewToggle.tsx b/src/components/client/kanban/PhaseViewToggle.tsx index 1ca1112..638e1f2 100644 --- a/src/components/client/kanban/PhaseViewToggle.tsx +++ b/src/components/client/kanban/PhaseViewToggle.tsx @@ -2,6 +2,7 @@ import { useState, type ReactNode } from "react"; import { ClientKanban } from "./ClientKanban"; +import { TaskStatusLegend } from "@/components/client/TaskStatusIndicator"; import type { ClientView } from "@/lib/client-view"; export function PhaseViewToggle({ @@ -39,7 +40,10 @@ export function PhaseViewToggle({
+ {/* Fuori dallo switch: gli stati sono gli stessi in entrambe le viste */} + + {view === "timeline" ? timelineView : }
); -} \ No newline at end of file +}