2123dc9d00
- 01-02: wave corrected from 1 to 2 (has depends_on: ["01-01"]) - 01-03: middleware rewritten to Edge-compatible fetch pattern; internal API route app/api/internal/validate-token/route.ts handles DB query in Node.js runtime; tasks/deliverables queries scoped with inArray(); accepted_total null-coalesced - 01-04: Task 1 and Task 6 merged → 5 tasks total (was 6, exceeded threshold) - STATE.md: updated to reflect Phase 1 planning verified, ready for execution Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
865 lines
32 KiB
Markdown
865 lines
32 KiB
Markdown
---
|
|
phase: "01-foundation-client-dashboard"
|
|
plan: 04
|
|
type: execute
|
|
wave: 2
|
|
depends_on:
|
|
- "01-01"
|
|
- "01-02"
|
|
- "01-03"
|
|
files_modified:
|
|
- app/c/[token]/page.tsx
|
|
- src/components/client-dashboard.tsx
|
|
- src/components/phase-timeline.tsx
|
|
- src/components/payment-status.tsx
|
|
- src/components/documents-section.tsx
|
|
- src/components/notes-section.tsx
|
|
- src/app/globals.css
|
|
- tailwind.config.ts
|
|
autonomous: true
|
|
requirements:
|
|
- DASH-02
|
|
- DASH-03
|
|
- DASH-04
|
|
- DASH-07
|
|
- DASH-08
|
|
- DASH-09
|
|
- DASH-10
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Client dashboard displays client brand name prominently with iamcavalli logo in corner"
|
|
- "Global progress bar at top shows % of all tasks completed"
|
|
- "Phases are displayed as lateral timeline (left indicator, content right)"
|
|
- "Each phase shows progress bar (% from completed tasks) + task list with status badges"
|
|
- "Tasks are nested within phases with status visible (todo/in_progress/done)"
|
|
- "Payment section always visible: accepted_total + Acconto 50% status + Saldo 50% status (NO amounts)"
|
|
- "Document links are clickable (opens external URL)"
|
|
- "Notes/decision log is visible (read-only, may be empty)"
|
|
- "Layout is mobile-responsive and light & clean visual style"
|
|
artifacts:
|
|
- path: "app/c/[token]/page.tsx"
|
|
provides: "Server Component rendering ClientDashboard"
|
|
min_lines: 20
|
|
- path: "src/components/client-dashboard.tsx"
|
|
provides: "Layout wrapper + main sections (header, progress, phases, payments, documents, notes)"
|
|
min_lines: 50
|
|
- path: "src/components/phase-timeline.tsx"
|
|
provides: "Lateral timeline rendering with phase cards and task lists"
|
|
min_lines: 80
|
|
- path: "src/components/payment-status.tsx"
|
|
provides: "Payment section: accepted_total + 2 payment rows with status"
|
|
min_lines: 30
|
|
- path: "src/components/documents-section.tsx"
|
|
provides: "List of external document links"
|
|
min_lines: 20
|
|
- path: "src/components/notes-section.tsx"
|
|
provides: "Read-only notes list with timestamps"
|
|
min_lines: 20
|
|
- path: "tailwind.config.ts"
|
|
provides: "Light & clean design tokens (updated from bootstrap)"
|
|
contains: "colors"
|
|
key_links:
|
|
- from: "app/c/[token]/page.tsx"
|
|
to: "ClientDashboard component"
|
|
via: "import { ClientDashboard }"
|
|
pattern: "<ClientDashboard"
|
|
- from: "ClientDashboard"
|
|
to: "PhaseTimeline + PaymentStatus + DocumentsSection + NotesSection"
|
|
via: "nested component props"
|
|
pattern: "view\\.phases"
|
|
- from: "PhaseTimeline"
|
|
to: "task status badges"
|
|
via: "status className mapping"
|
|
pattern: "status.*todo.*in_progress.*done"
|
|
|
|
---
|
|
|
|
<objective>
|
|
**Client Dashboard UI — Vertical Slice:** Render the complete client dashboard with all UI sections: header with branding, global progress bar, lateral phase timeline, task lists with status, payment status section, external document links, and read-only notes log. Implement light & clean visual style with mobile-first responsive design using Tailwind CSS and shadcn/ui components.
|
|
|
|
Purpose: Deliver the core user-facing product: a client can open their secret link and see the complete project status at a glance, with clear progress indicators, task hierarchy, payment overview, and documents.
|
|
|
|
Output: Fully rendered client portal with all DASH-02 through DASH-10 requirements implemented in the UI.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (Decisions D-04 through D-12)
|
|
@.planning/phases/01-foundation-client-dashboard/01-03-SUMMARY.md
|
|
@src/lib/client-view.ts (ClientView interface)
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Configure design tokens (tailwind.config.ts + globals.css) and wire app/c/[token]/page.tsx to ClientDashboard</name>
|
|
<files>
|
|
tailwind.config.ts
|
|
src/app/globals.css
|
|
app/c/[token]/page.tsx
|
|
</files>
|
|
<read_first>
|
|
tailwind.config.ts (current bootstrap)
|
|
src/app/globals.css (current bootstrap)
|
|
src/components/client-dashboard.tsx (will exist after Task 2 — read after Task 2 completes)
|
|
src/lib/client-view.ts (ClientView interface)
|
|
</read_first>
|
|
<action>
|
|
Update `tailwind.config.ts` to define light & clean design tokens:
|
|
|
|
```typescript
|
|
import type { Config } from 'tailwindcss';
|
|
|
|
const config: Config = {
|
|
content: [
|
|
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
],
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
// Light & clean palette
|
|
'primary': '#1a1a1a', // deep charcoal for text
|
|
'secondary': '#666666', // medium gray for secondary text
|
|
'tertiary': '#999999', // light gray for hints
|
|
'bg-light': '#ffffff', // pure white
|
|
'bg-subtle': '#f9f9f9', // very light gray
|
|
'border-light': '#e5e5e5', // subtle border
|
|
'accent': '#0066cc', // blue accent (will be brand-aware in Phase 2)
|
|
'success': '#22c55e', // green for done
|
|
'warning': '#eab308', // yellow for in-progress
|
|
'info': '#3b82f6', // blue for pending
|
|
},
|
|
spacing: {
|
|
'xs': '0.5rem',
|
|
'sm': '1rem',
|
|
'md': '1.5rem',
|
|
'lg': '2rem',
|
|
'xl': '3rem',
|
|
},
|
|
fontSize: {
|
|
'xs': '0.75rem',
|
|
'sm': '0.875rem',
|
|
'base': '1rem',
|
|
'lg': '1.125rem',
|
|
'xl': '1.25rem',
|
|
'2xl': '1.5rem',
|
|
'3xl': '1.875rem',
|
|
},
|
|
fontFamily: {
|
|
'sans': [
|
|
'system-ui',
|
|
'-apple-system',
|
|
'BlinkMacSystemFont',
|
|
'"Segoe UI"',
|
|
'Roboto',
|
|
'"Helvetica Neue"',
|
|
'sans-serif',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
};
|
|
|
|
export default config;
|
|
```
|
|
|
|
Update `src/app/globals.css`:
|
|
|
|
```css
|
|
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
body {
|
|
@apply bg-white text-primary font-sans;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
h1 {
|
|
@apply text-3xl font-bold tracking-tight;
|
|
}
|
|
|
|
h2 {
|
|
@apply text-2xl font-bold;
|
|
}
|
|
|
|
h3 {
|
|
@apply text-xl font-semibold;
|
|
}
|
|
|
|
p {
|
|
@apply text-base text-secondary;
|
|
}
|
|
|
|
a {
|
|
@apply text-accent hover:underline transition-colors;
|
|
}
|
|
|
|
.border-subtle {
|
|
@apply border border-border-light;
|
|
}
|
|
|
|
.bg-subtle {
|
|
@apply bg-bg-subtle;
|
|
}
|
|
```
|
|
|
|
Update `app/c/[token]/page.tsx` to replace the Plan 03 placeholder with the full ClientDashboard render:
|
|
|
|
```typescript
|
|
import { getClientView } from '@/lib/client-view';
|
|
import { ClientDashboard } from '@/components/client-dashboard';
|
|
import { notFound } from 'next/navigation';
|
|
|
|
export const revalidate = 60;
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: {
|
|
params: { token: string };
|
|
}) {
|
|
const view = await getClientView(params.token);
|
|
|
|
if (!view) {
|
|
return { title: 'Not Found' };
|
|
}
|
|
|
|
return {
|
|
title: `${view.client.brand_name} — Project Status | iamcavalli`,
|
|
description: view.client.brief || 'Project status dashboard',
|
|
};
|
|
}
|
|
|
|
export default async function ClientPage({
|
|
params,
|
|
}: {
|
|
params: { token: string };
|
|
}) {
|
|
const view = await getClientView(params.token);
|
|
|
|
if (!view) {
|
|
notFound();
|
|
}
|
|
|
|
return <ClientDashboard view={view} />;
|
|
}
|
|
```
|
|
|
|
Note: `getClientView` is called twice (once in `generateMetadata`, once in `ClientPage`). Next.js 15 deduplicates fetch calls within the same render, and since this is a DB query via Drizzle (not fetch), use React `cache()` in `client-view.ts` if double-call is a concern — acceptable for Phase 1 given low traffic.
|
|
</action>
|
|
<verify>
|
|
<automated>grep -q "colors:" tailwind.config.ts && echo "Color tokens defined"</automated>
|
|
<automated>grep -q "primary\|accent\|success" tailwind.config.ts && echo "Key colors present"</automated>
|
|
<automated>grep -q "@tailwind" src/app/globals.css && echo "Tailwind directives in globals.css"</automated>
|
|
<automated>grep -q "ClientDashboard" app/c/\[token\]/page.tsx && echo "ClientDashboard wired in page"</automated>
|
|
<automated>grep -q "generateMetadata" app/c/\[token\]/page.tsx && echo "Dynamic metadata present"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `tailwind.config.ts` contains color tokens: primary, secondary, accent, success, warning
|
|
- `globals.css` includes Tailwind directives and base typography
|
|
- `app/c/[token]/page.tsx` renders `<ClientDashboard view={view} />` with dynamic metadata
|
|
- 404 returned if token invalid
|
|
- `npm run build` succeeds
|
|
</acceptance_criteria>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create ClientDashboard wrapper component with header, global progress, and section layout</name>
|
|
<files>
|
|
src/components/client-dashboard.tsx
|
|
</files>
|
|
<read_first>
|
|
src/lib/client-view.ts (ClientView interface)
|
|
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-06 through D-10)
|
|
</read_first>
|
|
<action>
|
|
Create `src/components/client-dashboard.tsx`:
|
|
|
|
```typescript
|
|
'use client';
|
|
|
|
import { ClientView } from '@/lib/client-view';
|
|
import { Progress } from '@/components/ui/progress';
|
|
import { PhaseTimeline } from './phase-timeline';
|
|
import { PaymentStatus } from './payment-status';
|
|
import { DocumentsSection } from './documents-section';
|
|
import { NotesSection } from './notes-section';
|
|
|
|
interface ClientDashboardProps {
|
|
view: ClientView;
|
|
}
|
|
|
|
export function ClientDashboard({ view }: ClientDashboardProps) {
|
|
return (
|
|
<div className="min-h-screen bg-white">
|
|
{/* Header: Logo + Brand Name */}
|
|
<header className="bg-white border-b border-subtle sticky top-0 z-10">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<div className="flex items-center justify-between">
|
|
{/* iamcavalli logo (small, corner) */}
|
|
<div className="text-xs font-semibold text-tertiary">iamcavalli</div>
|
|
|
|
{/* Client brand name (prominent) */}
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-primary flex-1 text-center mx-4">
|
|
{view.client.brand_name}
|
|
</h1>
|
|
|
|
{/* Spacer for balance */}
|
|
<div className="w-20" />
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Global Progress Bar */}
|
|
<section className="bg-bg-subtle border-b border-subtle">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-semibold text-primary">Project Progress</p>
|
|
<Progress
|
|
value={view.global_progress_pct}
|
|
className="h-2"
|
|
/>
|
|
<p className="text-xs text-tertiary">
|
|
{view.global_progress_pct}% Complete
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Main Content */}
|
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Brief */}
|
|
{view.client.brief && (
|
|
<section className="mb-12">
|
|
<p className="text-lg text-secondary italic">
|
|
"{view.client.brief}"
|
|
</p>
|
|
</section>
|
|
)}
|
|
|
|
{/* Phase Timeline */}
|
|
<section className="mb-12">
|
|
<h2 className="text-2xl font-bold mb-8">Project Phases</h2>
|
|
<PhaseTimeline phases={view.phases} />
|
|
</section>
|
|
|
|
{/* Payment Status */}
|
|
<section className="mb-12">
|
|
<h2 className="text-2xl font-bold mb-6">Payment Status</h2>
|
|
<PaymentStatus
|
|
accepted_total={view.client.accepted_total}
|
|
payments={view.payments}
|
|
/>
|
|
</section>
|
|
|
|
{/* Documents */}
|
|
{view.documents.length > 0 && (
|
|
<section className="mb-12">
|
|
<h2 className="text-2xl font-bold mb-6">Documents & Files</h2>
|
|
<DocumentsSection documents={view.documents} />
|
|
</section>
|
|
)}
|
|
|
|
{/* Notes / Decision Log */}
|
|
{view.notes.length > 0 && (
|
|
<section className="mb-12">
|
|
<h2 className="text-2xl font-bold mb-6">Notes & Decisions</h2>
|
|
<NotesSection notes={view.notes} />
|
|
</section>
|
|
)}
|
|
</main>
|
|
|
|
{/* Footer */}
|
|
<footer className="bg-bg-subtle border-t border-subtle mt-16">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
<p className="text-xs text-tertiary text-center">
|
|
This is a private project dashboard. Do not share your unique link.
|
|
</p>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
Key points:
|
|
- Header: small "iamcavalli" logo (top-left), client brand_name centered (prominent)
|
|
- Global progress bar shows % of all tasks done
|
|
- Section headers are h2 (consistent sizing)
|
|
- Responsive layout: max-width container with mobile padding
|
|
- Brief is quoted and italicized
|
|
- Documents and Notes sections show only if data exists
|
|
</action>
|
|
<verify>
|
|
<automated>test -f src/components/client-dashboard.tsx && echo "ClientDashboard component exists"</automated>
|
|
<automated>grep -q "export function ClientDashboard" src/components/client-dashboard.tsx && echo "Component exported"</automated>
|
|
<automated>grep -q "iamcavalli" src/components/client-dashboard.tsx && echo "Logo text present"</automated>
|
|
<automated>grep -q "brand_name" src/components/client-dashboard.tsx && echo "Brand name rendered"</automated>
|
|
<automated>grep -q "global_progress_pct" src/components/client-dashboard.tsx && echo "Progress bar displays"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Component is exported and accepts ClientView props
|
|
- Header displays iamcavalli logo (small) + brand_name (prominent)
|
|
- Global progress bar shows project completion %
|
|
- Main sections: brief, phases, payments, documents (conditional), notes (conditional)
|
|
- Responsive layout with max-width container
|
|
</acceptance_criteria>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Create PhaseTimeline component for lateral timeline layout with task lists</name>
|
|
<files>
|
|
src/components/phase-timeline.tsx
|
|
</files>
|
|
<read_first>
|
|
src/lib/client-view.ts (phase and task structure)
|
|
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-07, D-08)
|
|
</read_first>
|
|
<action>
|
|
Create `src/components/phase-timeline.tsx`:
|
|
|
|
```typescript
|
|
'use client';
|
|
|
|
import { ClientView } from '@/lib/client-view';
|
|
import { Progress } from '@/components/ui/progress';
|
|
import { Card } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { CheckCircle2, Circle, Clock } from 'lucide-react';
|
|
|
|
interface PhaseTimelineProps {
|
|
phases: ClientView['phases'];
|
|
}
|
|
|
|
export function PhaseTimeline({ phases }: PhaseTimelineProps) {
|
|
return (
|
|
<div className="space-y-8">
|
|
{phases.map((phase, index) => (
|
|
<div key={phase.id} className="flex gap-6">
|
|
{/* Left: Timeline Indicator */}
|
|
<div className="flex flex-col items-center gap-2">
|
|
{/* Circle indicator */}
|
|
<div className="relative z-10 w-10 h-10 bg-white border-2 border-accent rounded-full flex items-center justify-center shadow-sm">
|
|
{phase.status === 'done' ? (
|
|
<CheckCircle2 className="w-6 h-6 text-success" />
|
|
) : phase.status === 'active' ? (
|
|
<Circle className="w-6 h-6 text-accent" />
|
|
) : (
|
|
<Clock className="w-6 h-6 text-tertiary" />
|
|
)}
|
|
</div>
|
|
|
|
{/* Vertical line (not on last) */}
|
|
{index < phases.length - 1 && (
|
|
<div className="flex-1 w-0.5 bg-border-light" style={{ minHeight: '120px' }} />
|
|
)}
|
|
</div>
|
|
|
|
{/* Right: Phase Content */}
|
|
<div className="flex-1 pb-8">
|
|
{/* Phase Card */}
|
|
<Card className="p-6 border-subtle hover:shadow-md transition-shadow">
|
|
{/* Phase Header */}
|
|
<div className="flex items-start justify-between mb-4">
|
|
<h3 className="text-xl font-bold text-primary">
|
|
{phase.title}
|
|
</h3>
|
|
<Badge
|
|
className={`capitalize ${
|
|
phase.status === 'done' ? 'bg-success text-white' :
|
|
phase.status === 'active' ? 'bg-accent text-white' :
|
|
'bg-tertiary text-white'
|
|
}`}
|
|
>
|
|
{phase.status === 'upcoming' ? 'Upcoming' :
|
|
phase.status === 'active' ? 'In Progress' : 'Done'}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Phase Progress Bar */}
|
|
<div className="mb-6 space-y-2">
|
|
<div className="flex justify-between items-center">
|
|
<p className="text-xs font-semibold text-secondary">
|
|
Phase Progress
|
|
</p>
|
|
<p className="text-xs text-tertiary">
|
|
{phase.progress_pct}%
|
|
</p>
|
|
</div>
|
|
<Progress value={phase.progress_pct} className="h-2" />
|
|
</div>
|
|
|
|
{/* Task List */}
|
|
<div className="space-y-3">
|
|
<p className="text-sm font-semibold text-secondary">
|
|
Tasks ({phase.tasks.filter(t => t.status === 'done').length} of {phase.tasks.length})
|
|
</p>
|
|
{phase.tasks.length === 0 ? (
|
|
<p className="text-sm text-tertiary italic">No tasks yet</p>
|
|
) : (
|
|
<ul className="space-y-2">
|
|
{phase.tasks.map((task) => (
|
|
<li
|
|
key={task.id}
|
|
className="flex items-start gap-3 p-2 rounded hover:bg-bg-subtle transition-colors"
|
|
>
|
|
{/* Task Status Icon */}
|
|
{task.status === 'done' ? (
|
|
<CheckCircle2 className="w-5 h-5 text-success mt-0.5 flex-shrink-0" />
|
|
) : task.status === 'in_progress' ? (
|
|
<Circle className="w-5 h-5 text-warning mt-0.5 flex-shrink-0" />
|
|
) : (
|
|
<Circle className="w-5 h-5 text-info mt-0.5 flex-shrink-0" />
|
|
)}
|
|
|
|
{/* Task Content */}
|
|
<div className="flex-1">
|
|
<p className={`text-sm ${task.status === 'done' ? 'line-through text-tertiary' : 'text-primary'}`}>
|
|
{task.title}
|
|
</p>
|
|
{task.description && (
|
|
<p className="text-xs text-tertiary mt-1">
|
|
{task.description}
|
|
</p>
|
|
)}
|
|
{/* Deliverables */}
|
|
{task.deliverables.length > 0 && (
|
|
<div className="mt-2 space-y-1">
|
|
{task.deliverables.map((d) => (
|
|
<div
|
|
key={d.id}
|
|
className="text-xs p-1 bg-bg-subtle rounded flex items-center justify-between gap-2"
|
|
>
|
|
<span className="text-secondary truncate">
|
|
{d.title}
|
|
</span>
|
|
{d.status === 'approved' && (
|
|
<Badge className="bg-success text-white text-xs">
|
|
Approved
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
Key points:
|
|
- Left indicator: circle with icon (checkmark for done, dot for upcoming/active)
|
|
- Vertical line connects phases (not on last phase)
|
|
- Right content: phase card with title, status badge, progress bar, task list
|
|
- Task status shown with icons and colors (success/warning/info)
|
|
- Deliverables nested under tasks with "Approved" badge if applicable
|
|
- Empty state if phase has no tasks
|
|
</action>
|
|
<verify>
|
|
<automated>test -f src/components/phase-timeline.tsx && echo "PhaseTimeline component exists"</automated>
|
|
<automated>grep -q "export function PhaseTimeline" src/components/phase-timeline.tsx && echo "Component exported"</automated>
|
|
<automated>grep -q "CheckCircle2\|Circle" src/components/phase-timeline.tsx && echo "Icons imported"</automated>
|
|
<automated>grep -q "progress_pct" src/components/phase-timeline.tsx && echo "Progress bar displays"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Component renders lateral timeline layout
|
|
- Each phase shows: title, status badge, progress bar, task count
|
|
- Tasks show status with icons (checkmark/circle)
|
|
- Deliverables are nested and show "Approved" badge if applicable
|
|
- Empty state for phases with no tasks
|
|
</acceptance_criteria>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 4: Create PaymentStatus component (accepted_total + payment rows with status badges)</name>
|
|
<files>
|
|
src/components/payment-status.tsx
|
|
</files>
|
|
<read_first>
|
|
src/lib/client-view.ts (payments shape, PaymentStatus type)
|
|
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-10, D-11)
|
|
</read_first>
|
|
<action>
|
|
Create `src/components/payment-status.tsx`:
|
|
|
|
```typescript
|
|
'use client';
|
|
|
|
import { Card } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { ClientView } from '@/lib/client-view';
|
|
import { CheckCircle2, Clock, AlertCircle } from 'lucide-react';
|
|
|
|
interface PaymentStatusProps {
|
|
accepted_total: string;
|
|
payments: ClientView['payments'];
|
|
}
|
|
|
|
export function PaymentStatus({ accepted_total, payments }: PaymentStatusProps) {
|
|
const statusConfig = {
|
|
da_saldare: { color: 'bg-info', icon: Clock, label: 'Da Saldare', text: 'white' },
|
|
inviata: { color: 'bg-warning', icon: AlertCircle, label: 'Inviata', text: 'white' },
|
|
saldato: { color: 'bg-success', icon: CheckCircle2, label: 'Saldato', text: 'white' },
|
|
};
|
|
|
|
return (
|
|
<Card className="p-6 border-subtle">
|
|
{/* Total */}
|
|
<div className="mb-6 pb-6 border-b border-subtle">
|
|
<p className="text-sm text-secondary font-semibold mb-2">
|
|
Totale Preventivo Accettato
|
|
</p>
|
|
<p className="text-3xl font-bold text-primary">
|
|
€{parseFloat(accepted_total || '0').toLocaleString('it-IT', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2,
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Payment Rows */}
|
|
<div className="space-y-4">
|
|
{payments.map((payment) => {
|
|
const config = statusConfig[payment.status as keyof typeof statusConfig];
|
|
const Icon = config?.icon || Clock;
|
|
|
|
return (
|
|
<div
|
|
key={payment.id}
|
|
className="flex items-center justify-between p-4 bg-bg-subtle rounded-lg border border-subtle"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<Icon className="w-5 h-5 text-secondary flex-shrink-0" />
|
|
<p className="text-sm font-semibold text-primary">
|
|
{payment.label}
|
|
</p>
|
|
</div>
|
|
<Badge
|
|
className={`capitalize ${config?.color} text-${config?.text}`}
|
|
>
|
|
{config?.label || payment.status}
|
|
</Badge>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Note */}
|
|
<p className="text-xs text-tertiary italic mt-6 pt-6 border-t border-subtle">
|
|
I pagamenti sono suddivisi in due rate da 50% ciascuna.
|
|
Contattaci per domande sui dettagli.
|
|
</p>
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
|
|
Key points:
|
|
- Shows `accepted_total` formatted as Euro currency — NEVER individual line-item amounts
|
|
- Two payment rows (Acconto 50%, Saldo 50%) with status badges only
|
|
- Status badge colors: da_saldare = blue, inviata = yellow, saldato = green
|
|
- Card + Badge from shadcn/ui
|
|
</action>
|
|
<verify>
|
|
<automated>test -f src/components/payment-status.tsx && echo "PaymentStatus component exists"</automated>
|
|
<automated>grep -q "export function PaymentStatus" src/components/payment-status.tsx && echo "Component exported"</automated>
|
|
<automated>grep -q "accepted_total" src/components/payment-status.tsx && echo "Total displayed"</automated>
|
|
<automated>grep -q "da_saldare\|inviata\|saldato" src/components/payment-status.tsx && echo "Status config present"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Component exists and is exported
|
|
- Displays accepted_total formatted as Euro (no individual amounts)
|
|
- Renders payment rows with status badges (da_saldare/inviata/saldato)
|
|
- Uses shadcn/ui Card and Badge
|
|
</acceptance_criteria>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 5: Create DocumentsSection and NotesSection components (external links + read-only notes)</name>
|
|
<files>
|
|
src/components/documents-section.tsx
|
|
src/components/notes-section.tsx
|
|
</files>
|
|
<read_first>
|
|
src/lib/client-view.ts (documents and notes shapes)
|
|
.planning/phases/01-foundation-client-dashboard/01-CONTEXT.md (D-12)
|
|
</read_first>
|
|
<action>
|
|
Create `src/components/documents-section.tsx`:
|
|
|
|
```typescript
|
|
'use client';
|
|
|
|
import { ClientView } from '@/lib/client-view';
|
|
import { Card } from '@/components/ui/card';
|
|
import { ExternalLink } from 'lucide-react';
|
|
|
|
interface DocumentsSectionProps {
|
|
documents: ClientView['documents'];
|
|
}
|
|
|
|
export function DocumentsSection({ documents }: DocumentsSectionProps) {
|
|
return (
|
|
<div className="space-y-3">
|
|
{documents.map((doc) => (
|
|
<Card
|
|
key={doc.id}
|
|
className="p-4 border-subtle hover:shadow-md transition-shadow"
|
|
>
|
|
<a
|
|
href={doc.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-between gap-3 text-accent hover:text-accent hover:underline group"
|
|
>
|
|
<span className="font-semibold text-primary group-hover:text-accent">
|
|
{doc.label}
|
|
</span>
|
|
<ExternalLink className="w-4 h-4 flex-shrink-0" />
|
|
</a>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
Create `src/components/notes-section.tsx`:
|
|
|
|
```typescript
|
|
'use client';
|
|
|
|
import { ClientView } from '@/lib/client-view';
|
|
import { Card } from '@/components/ui/card';
|
|
|
|
interface NotesSectionProps {
|
|
notes: ClientView['notes'];
|
|
}
|
|
|
|
export function NotesSection({ notes }: NotesSectionProps) {
|
|
if (notes.length === 0) {
|
|
return (
|
|
<p className="text-secondary italic text-sm">
|
|
No notes yet. Decisions will appear here as they are made.
|
|
</p>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{notes.map((note) => (
|
|
<Card key={note.id} className="p-4 border-subtle">
|
|
<p className="text-sm text-primary leading-relaxed">
|
|
{note.body}
|
|
</p>
|
|
<p className="text-xs text-tertiary mt-3">
|
|
{new Date(note.created_at).toLocaleDateString('it-IT', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
</p>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
Key points:
|
|
- DocumentsSection: clickable external links with ExternalLink icon, `rel="noopener noreferrer"` for security
|
|
- NotesSection: read-only, client never writes (admin writes in Phase 2 admin area)
|
|
- NotesSection: empty state shown as italic hint when no notes exist
|
|
- Timestamps formatted in Italian locale
|
|
</action>
|
|
<verify>
|
|
<automated>test -f src/components/documents-section.tsx && echo "DocumentsSection component exists"</automated>
|
|
<automated>test -f src/components/notes-section.tsx && echo "NotesSection component exists"</automated>
|
|
<automated>grep -q "export function DocumentsSection" src/components/documents-section.tsx && echo "DocumentsSection exported"</automated>
|
|
<automated>grep -q "export function NotesSection" src/components/notes-section.tsx && echo "NotesSection exported"</automated>
|
|
<automated>grep -q "noopener noreferrer" src/components/documents-section.tsx && echo "External link security present"</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- Both components exist and are exported
|
|
- DocumentsSection renders clickable external links with ExternalLink icon and secure rel attributes
|
|
- NotesSection shows read-only notes with Italian-formatted timestamps
|
|
- NotesSection shows empty state hint when notes array is empty
|
|
</acceptance_criteria>
|
|
</task>
|
|
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Client browser → CSS/HTML | UI rendering is client-safe; no admin secrets in HTML source |
|
|
| Link click → External URL | External document links open in new tab with `rel="noopener noreferrer"` |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-04-001 | Information Disclosure | Payment amounts | mitigate | Payments row shows status only; amounts never rendered on client dashboard |
|
|
| T-04-002 | Tampering | External links | accept | Links are user-provided URLs; client-side link validation (hostname check) could be added in Phase 2 |
|
|
| T-04-003 | Denial of Service | Image rendering | accept | Dashboard contains only text and icons; no resource-heavy assets |
|
|
|
|
</threat_model>
|
|
|
|
<verification>
|
|
After plan execution:
|
|
1. Run `npm run build` → no errors
|
|
2. Verify all component files exist: client-dashboard, phase-timeline, payment-status, documents-section, notes-section
|
|
3. Check page rendering logic in `app/c/[token]/page.tsx`
|
|
4. Verify mobile responsiveness: layout scales correctly on narrow screens
|
|
5. Check that payment amounts are NOT displayed (only status)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- All UI components are created and exported
|
|
- Client dashboard renders complete project status
|
|
- Global progress bar and per-phase progress bars display correctly
|
|
- Payment section shows only status (no amounts)
|
|
- Document links are clickable
|
|
- Notes section shows read-only list (or empty state)
|
|
- Layout is responsive and uses light & clean design
|
|
- Mobile-first design works on small screens
|
|
- Ready to proceed to Plan 05 (Seed script + DNS)
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/01-foundation-client-dashboard/01-04-SUMMARY.md`
|
|
</output>
|