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:
2026-05-22 11:31:32 +02:00
parent 1d9fa10961
commit 5d785a1c1c
8 changed files with 562 additions and 175 deletions
+30
View File
@@ -11,10 +11,13 @@ import Link from "next/link";
export default async function EditClientPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>;
searchParams: Promise<{ error?: string }>;
}) {
const { id } = await params;
const { error } = await searchParams;
const [client] = await db.select().from(clients).where(eq(clients.id, id)).limit(1);
if (!client) notFound();
@@ -31,6 +34,12 @@ export default async function EditClientPage({
<h1 className="text-xl font-bold text-[#1a1a1a] mb-6">Modifica cliente</h1>
{error === "slug_taken" && (
<div className="mb-4 rounded-lg bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
Slug già in uso da un altro cliente. Scegline uno diverso.
</div>
)}
<form
action={async (fd: FormData) => {
"use server";
@@ -64,6 +73,27 @@ export default async function EditClientPage({
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="slug">Slug personalizzato (opzionale)</Label>
<p className="text-xs text-[#71717a]">
Solo lettere minuscole, numeri e trattini (es. mario-rossi). Min 3, max 50 caratteri.
</p>
<Input
id="slug"
name="slug"
type="text"
defaultValue={client.slug ?? ""}
pattern="[a-z0-9-]{3,50}"
placeholder="mario-rossi"
/>
<p className="text-xs text-[#71717a]">
Link cliente:{" "}
<span className="font-mono text-[#1a1a1a]">
/client/{client.slug || client.token}
</span>
</p>
</div>
<div className="flex gap-3 pt-2">
<Button type="submit">Salva modifiche</Button>
<Button asChild variant="ghost">