diff --git a/.planning/phases/04-progetti-multi-project/04-04-SUMMARY.md b/.planning/phases/04-progetti-multi-project/04-04-SUMMARY.md new file mode 100644 index 0000000..0deb576 --- /dev/null +++ b/.planning/phases/04-progetti-multi-project/04-04-SUMMARY.md @@ -0,0 +1,67 @@ +--- +plan: "04-04" +phase: "04-progetti-multi-project" +status: complete +completed_at: "2026-05-22" +--- + +# Summary: 04-04 — Slug Resolution + Multi-Project Dashboard + Slug Edit + +## What Was Built + +**`/api/internal/validate-slug/route.ts`** — Internal API route for Edge middleware. Queries `clients.slug`, returns 200 `{ valid: true }` if found, 404 otherwise. Same pattern as `validate-token`. + +**`src/proxy.ts`** — Updated client guard with slug-first resolution (D-06): tries `/api/internal/validate-slug` first, then falls back to `/api/internal/validate-token`. Route path is `/client/[token]` (not `/c/` as referenced in the plan — adapted accordingly). + +**`src/lib/client-view.ts`** — Complete rewrite for multi-project model. Exports: +- `getClientWithProjectsByToken(tokenOrSlug)` — resolves slug → client (slug first, then token fallback), returns `ClientProjectSummary` with active (non-archived) projects only +- `getProjectView(projectId)` — returns full `ProjectView` scoped to one project; **payments select excludes amount** (DASH-07 + CLAUDE.md); **no quote_items** anywhere in the file + +**`src/app/client/[token]/page.tsx`** — Rewritten for multi-project logic: +- 0 active projects → placeholder screen +- 1 project → `ClientDashboard` directly (no selector), using `projectViewToClientView` adapter +- 2+ projects → shared header + shadcn `Tabs` (project names as triggers) + `ClientDashboard` per tab + +**`src/app/admin/clients/[id]/edit/page.tsx`** — Added slug field with: +- Pattern hint: `[a-z0-9-]{3,50}` +- Link preview showing `/client/{slug || token}` +- Error banner when redirected back with `?error=slug_taken` + +**`src/app/admin/clients/[id]/actions.ts`** — Updated `clientSchema` to include `slug` (Zod regex + `.transform(v => v === "" ? null : v)` to convert empty string to null). `updateClient` now: +1. Parses slug from form data +2. Persists to `clients.slug` via db.update +3. Catches unique constraint violation → redirects to edit page with `?error=slug_taken` +4. On success: redirects to `/admin/clients/[id]` (previously no redirect) + +## Key Architectural Decisions + +**Adapter pattern `projectViewToClientView`**: `ClientDashboard` was written for `ClientView` shape. Rather than modifying it, an adapter function in the page converts `ProjectView` + `ClientProjectSummary.client` → `ClientView`. Key mappings: +- `view.project.accepted_total` → `clientView.client.accepted_total` (project-level, not client-level) +- `view.project.client_id` → `clientView.client.id` (used by ChatSection for comment threading) +- `deliverables[].approved_at: Date | null` → `.toISOString() | null` (ClientView expects string) +- `documents[].created_at` stripped (ClientView.documents only has id/label/url) + +**Multi-project tabs with full ClientDashboard**: Each tab content renders a complete `ClientDashboard` (including its header/footer). shadcn Tabs shows only the active one via CSS. This is MVP-acceptable: the outer page provides the tab navigation, and within each tab the full dashboard renders. + +**Slug empty string → null**: Zod schema transforms `""` to `null` so saving with empty slug removes it. This lets admins clear a slug without special UI. + +**Security invariants verified** (grep confirmed): +- `grep "quote_items" src/lib/client-view.ts` → only in comments, not in queries +- `grep "amount" src/lib/client-view.ts` → only in comments documenting the exclusion + +## Files Changed + +| File | Action | +|------|--------| +| `src/app/api/internal/validate-slug/route.ts` | Created | +| `src/proxy.ts` | Updated — slug-first resolution in client guard | +| `src/lib/client-view.ts` | Complete rewrite — getClientWithProjectsByToken + getProjectView | +| `src/app/client/[token]/page.tsx` | Complete rewrite — multi-project dashboard | +| `src/app/admin/clients/[id]/edit/page.tsx` | Updated — slug field + error banner | +| `src/app/admin/clients/[id]/actions.ts` | Updated — clientSchema + slug in updateClient | + +## Notes + +- The plan referenced `/c/[token]` path but actual route has always been `/client/[token]`. Proxy and all references use `/client/` consistently. +- `ClientDashboard` component unchanged — the adapter absorbs all type differences. +- `getClientWithProjectsByToken` filters out archived projects; archived projects are invisible to the client dashboard. \ No newline at end of file