feat(portale): gli stati dei task si distinguono, e "in revisione" dice di chi e' la palla

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 14:24:35 +02:00
parent c3d2afa61f
commit 2e9bd2ab60
7 changed files with 285 additions and 69 deletions
+29 -18
View File
@@ -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 (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-4">
{COLUMNS.map((col) => (
<div key={col.id} className="flex flex-col gap-4 rounded-xl border border-border-light bg-muted/60 p-4 min-h-[300px]">
<div className="flex items-center justify-between border-b border-border-light pb-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">{col.label}</span>
<span className="text-[10px] font-bold tabular-nums bg-secondary text-secondary-foreground rounded-full px-2 py-0.5">
{tasksByStatus[col.id].length}
</span>
{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 (
<div key={col.id} className="flex flex-col gap-4 rounded-xl border border-border-light bg-muted/60 p-4 min-h-[300px]">
<div className="flex items-center justify-between gap-2 border-b border-border-light pb-2">
<div className="flex min-w-0 items-center gap-2">
<span aria-hidden="true" className={`h-1.5 w-1.5 shrink-0 rounded-full ${v.dot}`} />
<span className={`truncate text-xs font-bold uppercase tracking-wider ${v.label}`}>
{col.label}
</span>
</div>
<span className={`shrink-0 text-[10px] font-bold tabular-nums rounded-full px-2 py-0.5 ${v.count}`}>
{tasksByStatus[col.id].length}
</span>
</div>
<div className="flex-1 flex flex-col gap-3">
{tasksByStatus[col.id].map((task) => (
<TaskCard key={task.id} task={task} token={token} />
))}
{tasksByStatus[col.id].length === 0 && (
<p className="text-xs text-muted-foreground italic text-center py-10">Nessun task</p>
)}
</div>
</div>
<div className="flex-1 flex flex-col gap-3">
{tasksByStatus[col.id].map((task) => (
<TaskCard key={task.id} task={task} token={token} />
))}
{tasksByStatus[col.id].length === 0 && (
<p className="text-xs text-muted-foreground italic text-center py-10">Nessun task</p>
)}
</div>
</div>
))}
);
})}
</div>
);
}
}