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>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
---
|
||||
plan: "04-02"
|
||||
phase: "04-progetti-multi-project"
|
||||
status: complete
|
||||
completed_at: "2026-05-22"
|
||||
---
|
||||
|
||||
# Summary: 04-02 — Admin Projects List + Client Detail Project Cards
|
||||
|
||||
## What Was Built
|
||||
|
||||
**NavBar** — added Progetti (`/admin/projects`) and Impostazioni (`/admin/impostazioni`) links in the correct order: Clienti → Progetti → Statistiche → Catalogo → Impostazioni.
|
||||
|
||||
**ProjectRow** (`src/components/admin/ProjectRow.tsx`) — new table row component cloned from ClientRow, adapted for projects. Columns: Nome+Cliente, Valore, Acconto badge, Saldo badge, TimerCell, €/h. The €/h is computed inline: `accepted_total / (totalTrackedSeconds / 3600)`, showing `—` when no hours tracked.
|
||||
|
||||
**`/admin/projects`** (`src/app/admin/projects/page.tsx`) — table listing all projects via `getAllProjectsWithPayments()`. Empty state shows helpful message.
|
||||
|
||||
**`/admin/projects/new`** (`src/app/admin/projects/new/page.tsx`) — creation form with client `<select>`. Accepts `?client_id` query param to pre-select the client (used when clicking "+ Nuovo Progetto" from a client's detail page). On submit calls `createProject` server action and redirects to the new project's workspace.
|
||||
|
||||
**project-actions** (`src/app/admin/projects/project-actions.ts`) — server actions: `createProject`, `archiveProject`, `unarchiveProject`, `updateProjectAcceptedTotal`. Each guards with local `requireAdmin()` (same pattern as other actions files — not imported from `@/lib/auth` which doesn't export it).
|
||||
|
||||
**`/admin/clients/[id]`** — fully rewritten from tabbed workspace to project cards grid. Active projects shown as clickable cards linking to `/admin/projects/[id]`. Archived projects shown below with reduced opacity. Header retains "Link cliente →" and `ClientActions` component.
|
||||
|
||||
**`getAllClientsWithPayments`** — extended to also fetch project names/amounts per client, computing:
|
||||
- `projectBrands: string[]` — non-archived project names, used as secondary labels under client name
|
||||
- `ltv: string` — sum of all projects' `accepted_total` (including archived), replaces single `accepted_total` in the LTV column
|
||||
|
||||
**ClientRow** — shows `projectBrands.join(" | ")` under client name (falls back to `brand_name` if no projects). LTV column now uses `client.ltv`.
|
||||
|
||||
**`/admin/page.tsx`** — column header renamed from "Totale" to "LTV".
|
||||
|
||||
## Key Implementation Decisions
|
||||
|
||||
- **`?client_id` pre-selection in new project form**: used `defaultValue={client_id ?? ""}` on the `<select>` — React controlled default, works with server components.
|
||||
- **No `requireAdmin` import**: all server actions files define it locally with the same `getServerSession(authOptions)` pattern — consistent with existing codebase.
|
||||
- **`clientProjects` query extended**: instead of a separate parallel query for projectBrands/ltv, the existing `clientProjects` fetch was extended to also select `name`, `accepted_total`, `archived` — avoids an extra DB round-trip.
|
||||
|
||||
## Files Modified/Created
|
||||
|
||||
| File | Action |
|
||||
|------|--------|
|
||||
| `src/components/admin/NavBar.tsx` | Updated — added 2 links |
|
||||
| `src/components/admin/ProjectRow.tsx` | Created |
|
||||
| `src/app/admin/projects/page.tsx` | Created |
|
||||
| `src/app/admin/projects/new/page.tsx` | Created |
|
||||
| `src/app/admin/projects/project-actions.ts` | Created |
|
||||
| `src/app/admin/clients/[id]/page.tsx` | Rewritten |
|
||||
| `src/app/admin/page.tsx` | Updated — column header |
|
||||
| `src/lib/admin-queries.ts` | Updated — projectBrands + ltv |
|
||||
| `src/components/admin/ClientRow.tsx` | Updated — projectBrands + ltv |
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
plan: "04-03"
|
||||
phase: "04-progetti-multi-project"
|
||||
status: complete
|
||||
completed_at: "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`:
|
||||
|
||||
```typescript
|
||||
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).
|
||||
Reference in New Issue
Block a user