Files
clienthub/.planning/phases/04-progetti-multi-project/04-04-SUMMARY.md
T

4.2 KiB

plan, phase, status, completed_at
plan phase status completed_at
04-04 04-progetti-multi-project complete 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.clientClientView. Key mappings:

  • view.project.accepted_totalclientView.client.accepted_total (project-level, not client-level)
  • view.project.client_idclientView.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.