docs(01-foundation-client-dashboard): complete phase 1 planning with 5-plan structure
Create comprehensive phase plans for Foundation & Client Dashboard: - 01-01-PLAN.md: Walking Skeleton (Next.js 15 bootstrap + DB connection) - 01-02-PLAN.md: Database schema (11 tables, Drizzle ORM, drizzle-kit push) - 01-03-PLAN.md: Middleware token validation + ClientView type + data fetching - 01-04-PLAN.md: Client dashboard UI (header, timeline, progress, payments, docs, notes) - 01-05-PLAN.md: Seed script + DNS CNAME configuration Also create SKELETON.md documenting locked architectural decisions for all future phases: - Next.js 15 + Drizzle + postgres-js driver (Coolify Postgres) - Token as separate rotatable field (not PK) - ClientView enforcement (no quote_items exposed to client API) - Approved_at immutable audit trail - Two independent auth systems (client token + admin session) - Vercel deployment with custom domain Update ROADMAP.md to mark Phase 1 as planned (5 plans created) and ready for execution. All plans follow MVP vertical-slice structure with 2-3 tasks per plan. Walking Skeleton proves the entire stack works end-to-end. Requirements mapping: DASH-01 through DASH-04, DASH-07 through DASH-10 covered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,861 @@
|
||||
---
|
||||
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: Update tailwind.config.ts with light & clean design tokens and extend globals.css</name>
|
||||
<files>
|
||||
tailwind.config.ts
|
||||
src/app/globals.css
|
||||
</files>
|
||||
<read_first>
|
||||
tailwind.config.ts (current bootstrap)
|
||||
src/app/globals.css (current bootstrap)
|
||||
</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;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
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;
|
||||
}
|
||||
|
||||
/* Subtle border utilities */
|
||||
.border-subtle {
|
||||
@apply border border-border-light;
|
||||
}
|
||||
|
||||
.bg-subtle {
|
||||
@apply bg-bg-subtle;
|
||||
}
|
||||
```
|
||||
</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>npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "TypeScript errors" || echo "Build OK"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `tailwind.config.ts` contains color tokens: primary, secondary, accent, success, warning
|
||||
- `globals.css` includes Tailwind directives and base typography
|
||||
- `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, DocumentsSection, and NotesSection components</name>
|
||||
<files>
|
||||
src/components/payment-status.tsx
|
||||
src/components/documents-section.tsx
|
||||
src/components/notes-section.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
src/lib/client-view.ts (payments, documents, notes shapes)
|
||||
.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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
- PaymentStatus: shows accepted_total + 2 payment rows (Acconto 50%, Saldo 50%) with status badges (no amounts)
|
||||
- DocumentsSection: clickable external links with ExternalLink icon
|
||||
- NotesSection: read-only notes with formatted timestamps
|
||||
- All use Card + Badge components from shadcn/ui
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f src/components/payment-status.tsx && echo "PaymentStatus component exists"</automated>
|
||||
<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 "accepted_total" src/components/payment-status.tsx && echo "Total displayed"</automated>
|
||||
<automated>grep -q "ExternalLink" src/components/documents-section.tsx && echo "Link icon present"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- All three components exist and are exported
|
||||
- PaymentStatus displays accepted_total + 2 payment rows with status (no amounts)
|
||||
- DocumentsSection shows clickable external links
|
||||
- NotesSection shows read-only notes with timestamps (or empty state)
|
||||
- All components use shadcn/ui Card and Badge
|
||||
</acceptance_criteria>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: Update app/c/[token]/page.tsx to render ClientDashboard with real data</name>
|
||||
<files>
|
||||
app/c/[token]/page.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
src/components/client-dashboard.tsx
|
||||
src/lib/client-view.ts
|
||||
</read_first>
|
||||
<action>
|
||||
Update `app/c/[token]/page.tsx`:
|
||||
|
||||
```typescript
|
||||
import { getClientView } from '@/lib/client-view';
|
||||
import { ClientDashboard } from '@/components/client-dashboard';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const revalidate = 60; // ISR: revalidate every 60 seconds
|
||||
|
||||
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 | ClientHub`,
|
||||
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} />;
|
||||
}
|
||||
```
|
||||
|
||||
This page:
|
||||
- Fetches ClientView data
|
||||
- Returns 404 if not found
|
||||
- Generates dynamic metadata with client brand name
|
||||
- Renders ClientDashboard with real data
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f app/c/\[token\]/page.tsx && echo "Page file exists"</automated>
|
||||
<automated>grep -q "ClientDashboard" app/c/\[token\]/page.tsx && echo "ClientDashboard rendered"</automated>
|
||||
<automated>grep -q "getClientView" app/c/\[token\]/page.tsx && echo "Data fetched"</automated>
|
||||
<automated>npm run build 2>&1 | grep -v "warning" | grep -q "error" && echo "Build errors" || echo "Build OK"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- Page renders ClientDashboard component with ClientView data
|
||||
- 404 is returned if token is invalid
|
||||
- Page metadata is dynamic (includes client brand name)
|
||||
- `npm run build` succeeds
|
||||
</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>
|
||||
Reference in New Issue
Block a user