--- 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.