docs(04-04): capture phase summary — slug resolution + multi-project dashboard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||||
Reference in New Issue
Block a user