Files
simone 5d785a1c1c feat(04-04): slug resolution + multi-project client dashboard + slug edit
- validate-slug API route: resolves clients.slug for Edge middleware
- proxy.ts: slug-first resolution (D-06) — tries slug then falls back to token
- client-view.ts: complete rewrite — getClientWithProjectsByToken + getProjectView
  - No quote_items, no payment amounts in client API (CLAUDE.md security invariants)
- client/[token]/page.tsx: multi-project dashboard — 1 project = direct view,
  2+ projects = shadcn Tabs with project names (D-09/D-10)
- edit/page.tsx: slug field with link preview + unique constraint error handling
- actions.ts: updateClient now persists slug, redirects on success/slug_taken

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:31:32 +02:00

3.0 KiB

plan, phase, status, completed_at
plan phase status completed_at
04-03 04-progetti-multi-project complete 2026-05-22

Summary: 04-03 — Project Workspace + Timer Analytics + Settings

What Was Built

/admin/projects/[id] — full project workspace with 7 tabs: Fasi & Task, Pagamenti, Documenti, Note, Commenti, Preventivo, Timer. Reuses all existing tab components (PhasesTab, PaymentsTab, DocumentsTab, CommentsTab, QuoteTab) unchanged by passing projectId as clientId.

ProfitabilityCard (src/components/admin/ProfitabilityCard.tsx) — pure display component (no "use client"). Shows: ore lavorate, importo accettato, €/h reale, €/h target, costo ideale, delta (verde = guadagno, rosso = perdita). Delta shown only when both hours > 0 and accepted > 0.

TimerTab (src/components/admin/tabs/TimerTab.tsx) — client component wrapping TimerCell + ProfitabilityCard. Passes projectId as clientId to TimerCell (prop name is legacy; the underlying startTimer already uses project_id).

/admin/impostazioni — settings page with target_hourly_rate form. Inline server action calls updateSetting(SETTINGS_KEYS.TARGET_HOURLY_RATE, ...). Default value 50€/h from getTargetHourlyRate().

Key Architectural Decision: resolveEntity()

The tab components (PhasesTab, PaymentsTab, etc.) hardcode imports from @/app/admin/clients/[id]/actions. Rather than duplicating tab components or injecting actions as props, added resolveEntity(id) helper to both actions.ts and quote-actions.ts:

async function resolveEntity(id: string): Promise<{ projectId: string | null; path: string }> {
  // Checks if id is a projects.id directly (one PK lookup)
  // Falls back to client_id lookup if not found
  // Returns correct revalidatePath for either context
}

This makes all existing tab actions work transparently with both clientId and projectId. Zero changes to tab components.

updateAcceptedTotal in actions.ts — special case: detects project vs client context and updates the correct table (projects.accepted_total vs clients.accepted_total), with per-project payment stub splitting.

timer-actions.ts — added revalidatePath("/admin/projects") and revalidatePath(\/admin/projects/${projectId}`)` so the project list and workspace refresh after timer start/stop.

Files Changed

File Action
src/app/admin/clients/[id]/actions.ts Updated — added resolveEntity(), all functions now work with projectId
src/app/admin/clients/[id]/quote-actions.ts Updated — same resolveEntity pattern
src/app/admin/timer-actions.ts Updated — added /admin/projects revalidation
src/components/admin/ProfitabilityCard.tsx Created
src/components/admin/tabs/TimerTab.tsx Created
src/app/admin/projects/[id]/page.tsx Created
src/app/admin/impostazioni/page.tsx Created

Notes Tab

No NotesTab component exists — notes rendered inline in the project workspace page. Simple read-only display (no add/edit/delete — consistent with how notes appear in the client dashboard).