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
+7 -6
View File
@@ -3,9 +3,9 @@ gsd_state_version: 1.0
milestone: v2.5 milestone: v2.5
milestone_name: Audit milestone_name: Audit
status: executing 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'." 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-21T20:55:00.000Z" last_updated: "2026-08-22T12:10:00.000Z"
last_activity: 2026-08-21 -- modifica dei messaggi (0022) e firma di chi risponde in chat last_activity: 2026-08-22 -- stati dei task leggibili nel portale cliente (icone, pill, legenda)
progress: progress:
total_phases: 4 total_phases: 4
completed_phases: 0 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 | | 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 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 | | 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 | | D — Whop → audit | ⏸️ dipende dal motore v2.5 |
Progress: [███░░░░░░░] 25% (v2.5) Progress: [███░░░░░░░] 25% (v2.5)
@@ -92,7 +93,7 @@ Log completo in `PROJECT.md`. Vive per il lavoro corrente:
## Session Continuity ## Session Continuity
Last session: 2026-08-21 Last session: 2026-08-22
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.** 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) 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/`. 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 Resume file: None
+46 -1
View File
@@ -1,6 +1,6 @@
# ClientHub (IAMCAVALLI) — Status # 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, 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 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 poi notifiche (Resend c'è già), poi eventualmente menzioni. Molto probabilmente vedere
*chi* ha scritto risolve gran parte del problema da solo. *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 2730), in pausa ### v2.5 — Audit (Phases 2730), in pausa
Il servizio di analisi sito (tre livelli: **Radiografia / Prima-Dopo / Rotta**) diventa Il servizio di analisi sito (tre livelli: **Radiografia / Prima-Dopo / Rotta**) diventa
+10
View File
@@ -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. `bg-muted`/`text-muted-foreground` (upcoming) — each with a `dark:` variant.
- **Phase cards** (`PhaseCard`): `rounded-xl border-border-light bg-card - **Phase cards** (`PhaseCard`): `rounded-xl border-border-light bg-card
shadow-card`, progress bar colored per status (emerald/amber/`bg-border`). 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`, - Sidebar cards (`OffersSection`, `PaymentStatus`, `DocumentsSection`,
`NotesSection`, `TranscriptsSection`) all follow `rounded-xl border-border-light `NotesSection`, `TranscriptsSection`) all follow `rounded-xl border-border-light
bg-card shadow-card`. bg-card shadow-card`.
+21 -34
View File
@@ -3,8 +3,8 @@
import { useState } from "react"; import { useState } from "react";
import { ApproveButton } from "@/components/client/ApproveButton"; import { ApproveButton } from "@/components/client/ApproveButton";
import { useChatContext } from "@/components/client/ChatProvider"; import { useChatContext } from "@/components/client/ChatProvider";
import { TaskStatusIcon, TaskStatusPill } from "@/components/client/TaskStatusIndicator";
import type { ClientView } from "@/lib/client-view"; import type { ClientView } from "@/lib/client-view";
import { TASK_STATUS_LABELS, type TaskStatus } from "@/lib/task-status";
type Phase = ClientView["phases"][number]; type Phase = ClientView["phases"][number];
@@ -29,36 +29,6 @@ const phaseBarColor: Record<"upcoming" | "active" | "done", string> = {
done: "bg-emerald-600", done: "bg-emerald-600",
}; };
function TaskStatusIcon({ status }: { status: TaskStatus }) {
if (status === "done") {
return (
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-emerald-50 text-[10px] font-bold text-emerald-600 dark:bg-emerald-500/10 dark:text-emerald-400">
</span>
);
}
if (status === "in_review") {
return (
<span
className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-violet-400 bg-card dark:border-violet-500"
title={TASK_STATUS_LABELS.in_review}
>
<span className="h-2 w-2 rounded-full bg-violet-500 dark:bg-violet-400" />
</span>
);
}
if (status === "in_progress") {
return (
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-amber-400 bg-card">
<span className="h-1.5 w-1.5 rounded-full bg-amber-500" />
</span>
);
}
return (
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-border bg-card" />
);
}
export function PhaseCard({ export function PhaseCard({
phase, phase,
token, token,
@@ -71,6 +41,10 @@ export function PhaseCard({
const [open, setOpen] = useState(defaultOpen); const [open, setOpen] = useState(defaultOpen);
const { openChat } = useChatContext(); const { openChat } = useChatContext();
const doneCount = phase.tasks.filter((t) => t.status === "done").length; 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 ( return (
<div className="rounded-xl border border-border-light bg-card p-6 shadow-card"> <div className="rounded-xl border border-border-light bg-card p-6 shadow-card">
@@ -118,9 +92,17 @@ export function PhaseCard({
{/* Progress bar — always visible, colore per stato */} {/* Progress bar — always visible, colore per stato */}
<div className="mb-1"> <div className="mb-1">
<div className="flex justify-between items-center mb-1.5"> <div className="flex justify-between items-center gap-3 mb-1.5">
<p className="text-xs text-muted-foreground font-medium">{doneCount} di {phase.tasks.length} task</p> <p className="text-xs text-muted-foreground font-medium min-w-0">
<p className="text-xs font-semibold text-foreground">{phase.progress_pct}%</p> {doneCount} di {phase.tasks.length} task
{reviewCount > 0 && (
<span className="text-violet-600 dark:text-violet-400">
{" · "}
{reviewCount} in attesa di riscontro
</span>
)}
</p>
<p className="text-xs font-semibold text-foreground shrink-0">{phase.progress_pct}%</p>
</div> </div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted"> <div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div <div
@@ -142,6 +124,9 @@ export function PhaseCard({
<li key={task.id} className="flex items-start gap-3"> <li key={task.id} className="flex items-start gap-3">
<TaskStatusIcon status={task.status} /> <TaskStatusIcon status={task.status} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* flex-wrap: su un titolo lungo la pill va a capo invece di
schiacciare il testo */}
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
<p <p
className={`text-xs leading-snug ${ className={`text-xs leading-snug ${
task.status === "done" task.status === "done"
@@ -151,6 +136,8 @@ export function PhaseCard({
> >
{task.title} {task.title}
</p> </p>
<TaskStatusPill status={task.status} />
</div>
{task.description && ( {task.description && (
<p className="text-[11px] text-muted-foreground mt-0.5 leading-snug"> <p className="text-[11px] text-muted-foreground mt-0.5 leading-snug">
{task.description} {task.description}
@@ -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<TaskStatus, StatusVisual> = {
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 (
<svg
className={className}
fill="none"
stroke="currentColor"
strokeWidth={3}
viewBox="0 0 24 24"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
);
}
/**
* 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 (
<>
<span
aria-hidden="true"
className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full ${v.ring}`}
>
{status === "done" && <CheckGlyph className="h-3 w-3" />}
{status === "in_review" && (
<CheckGlyph className="h-2.5 w-2.5 text-violet-600 dark:text-violet-400" />
)}
{status === "in_progress" && (
<span className="h-1.5 w-1.5 rounded-full bg-amber-500 dark:bg-amber-400" />
)}
</span>
<span className="sr-only">{v.hint}</span>
</>
);
}
/** Etichetta di testo. Nulla per "da fare" e "fatto", che si leggono da soli. */
export function TaskStatusPill({ status }: { status: TaskStatus }) {
const v = TASK_STATUS_VISUALS[status];
if (!v.pill) return null;
return (
<span
className={`shrink-0 rounded px-1.5 py-0.5 text-[9px] font-bold uppercase leading-none tracking-wide ${v.pill}`}
>
{TASK_STATUS_LABELS[status]}
</span>
);
}
/**
* Legenda degli stati + la riga che spiega "in revisione".
*
* Le voci si generano da TASK_STATUSES, mai da una lista riscritta a mano: è la
* disciplina introdotta insieme allo stato "in revisione", dopo che due liste
* fisse avevano già fatto sparire task dal portale senza un errore.
*/
export function TaskStatusLegend() {
return (
<div className="mb-6 rounded-lg bg-muted/50 px-4 py-3">
<ul className="flex flex-wrap items-center gap-x-5 gap-y-2">
{TASK_STATUSES.map((status) => (
<li key={status} className="flex items-center gap-1.5">
<span
aria-hidden="true"
className={`h-1.5 w-1.5 shrink-0 rounded-full ${TASK_STATUS_VISUALS[status].dot}`}
/>
<span className="text-[11px] font-medium text-muted-foreground">
{TASK_STATUS_LABELS[status]}
</span>
</li>
))}
</ul>
<p className="mt-2 text-[11px] leading-snug text-muted-foreground">
<span className="font-semibold text-foreground">In revisione</span> il lavoro è pronto e
aspetta il tuo riscontro: scrivicelo dalla chat della fase. Ogni pagina include un giro di
revisione.
</p>
</div>
);
}
+16 -5
View File
@@ -2,6 +2,7 @@
import type { ClientView } from "@/lib/client-view"; import type { ClientView } from "@/lib/client-view";
import { ApproveButton } from "@/components/client/ApproveButton"; 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"; import { TASK_STATUS_LABELS, TASK_STATUSES, type TaskStatus } from "@/lib/task-status";
type Task = ClientView["phases"][number]["tasks"][number] & { type Task = ClientView["phases"][number]["tasks"][number] & {
@@ -59,11 +60,20 @@ export function ClientKanban({ phases, token }: { phases: ClientView["phases"];
return ( return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-4"> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-4">
{COLUMNS.map((col) => ( {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 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"> <div className="flex items-center justify-between gap-2 border-b border-border-light pb-2">
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">{col.label}</span> <div className="flex min-w-0 items-center gap-2">
<span className="text-[10px] font-bold tabular-nums bg-secondary text-secondary-foreground rounded-full px-2 py-0.5"> <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} {tasksByStatus[col.id].length}
</span> </span>
</div> </div>
@@ -76,7 +86,8 @@ export function ClientKanban({ phases, token }: { phases: ClientView["phases"];
)} )}
</div> </div>
</div> </div>
))} );
})}
</div> </div>
); );
} }
@@ -2,6 +2,7 @@
import { useState, type ReactNode } from "react"; import { useState, type ReactNode } from "react";
import { ClientKanban } from "./ClientKanban"; import { ClientKanban } from "./ClientKanban";
import { TaskStatusLegend } from "@/components/client/TaskStatusIndicator";
import type { ClientView } from "@/lib/client-view"; import type { ClientView } from "@/lib/client-view";
export function PhaseViewToggle({ export function PhaseViewToggle({
@@ -39,6 +40,9 @@ export function PhaseViewToggle({
</div> </div>
</div> </div>
{/* Fuori dallo switch: gli stati sono gli stessi in entrambe le viste */}
<TaskStatusLegend />
{view === "timeline" ? timelineView : <ClientKanban phases={phases} token={token} />} {view === "timeline" ? timelineView : <ClientKanban phases={phases} token={token} />}
</div> </div>
); );