From 5d5c8eaa7d44e215ad3a0b1bb6fc805b45dad147 Mon Sep 17 00:00:00 2001 From: Simone Cavalli Date: Thu, 14 May 2026 22:13:20 +0200 Subject: [PATCH] =?UTF-8?q?feat(01-04):=20PhaseTimeline=20=E2=80=94=20time?= =?UTF-8?q?line=20laterale=20con=20indicatori,=20progress=20bar=20per=20fa?= =?UTF-8?q?se,=20task=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Layout a due colonne: indicatore cerchio (sinistra) + card fase (destra) - Icone SVG inline per stato fase (done/active/upcoming) e task (done/in_progress/todo) - Badge stato fase con colori semantici (verde/blu/grigio) - Progress bar per fase con contatore task done/totale - Task list con line-through per done, testo grigio chiaro - Deliverable annidati sotto ogni task con badge "Approvato" se approved - Linea verticale tra fasi (non sull'ultima) --- src/components/phase-timeline.tsx | 229 ++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/components/phase-timeline.tsx diff --git a/src/components/phase-timeline.tsx b/src/components/phase-timeline.tsx new file mode 100644 index 0000000..a7f46fb --- /dev/null +++ b/src/components/phase-timeline.tsx @@ -0,0 +1,229 @@ +import type { ClientView } from '@/lib/client-view'; +import { Progress } from '@/components/ui/progress'; +import { Card } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +interface PhaseTimelineProps { + phases: ClientView['phases']; +} + +function PhaseStatusIcon({ status }: { status: 'upcoming' | 'active' | 'done' }) { + if (status === 'done') { + return ( + + ); + } + if (status === 'active') { + return ( + + ); + } + return ( + + ); +} + +function TaskStatusIcon({ status }: { status: 'todo' | 'in_progress' | 'done' }) { + if (status === 'done') { + return ( + + ); + } + if (status === 'in_progress') { + return ( + + ); + } + return ( + + ); +} + +const phaseStatusLabel: Record<'upcoming' | 'active' | 'done', string> = { + upcoming: 'In arrivo', + active: 'In corso', + done: 'Completata', +}; + +const phaseStatusStyle: Record<'upcoming' | 'active' | 'done', string> = { + upcoming: 'border-transparent bg-[#999999] text-white', + active: 'border-transparent bg-[#0066cc] text-white', + done: 'border-transparent bg-[#16a34a] text-white', +}; + +export function PhaseTimeline({ phases }: PhaseTimelineProps) { + if (phases.length === 0) { + return ( +

+ Nessuna fase ancora configurata. +

+ ); + } + + return ( +
+ {phases.map((phase, index) => { + const doneCount = phase.tasks.filter((t) => t.status === 'done').length; + const isLast = index === phases.length - 1; + + return ( +
+ {/* Colonna sinistra: indicatore timeline */} +
+ {/* Cerchio con icona stato */} +
+ +
+ {/* Linea verticale verso la fase successiva */} + {!isLast && ( +
+ )} +
+ + {/* Colonna destra: contenuto fase */} +
+ + {/* Header fase */} +
+

+ {phase.title} +

+ + {phaseStatusLabel[phase.status]} + +
+ + {/* Barra progresso fase (D-08) */} +
+
+

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

+

+ {phase.progress_pct}% +

+
+ +
+ + {/* Lista task */} + {phase.tasks.length === 0 ? ( +

+ Nessun task ancora configurato. +

+ ) : ( +
    + {phase.tasks.map((task) => ( +
  • + +
    +

    + {task.title} +

    + {task.description && ( +

    + {task.description} +

    + )} + + {/* Deliverable annidati */} + {task.deliverables.length > 0 && ( +
      + {task.deliverables.map((d) => ( +
    • + + {d.title} + + {d.status === 'approved' && ( + + Approvato + + )} +
    • + ))} +
    + )} +
    +
  • + ))} +
+ )} +
+
+
+ ); + })} +
+ ); +} \ No newline at end of file