diff --git a/src/app/admin/clients/new/actions.ts b/src/app/admin/clients/new/actions.ts index 1418d65..ad04bef 100644 --- a/src/app/admin/clients/new/actions.ts +++ b/src/app/admin/clients/new/actions.ts @@ -3,9 +3,30 @@ import { redirect } from "next/navigation"; import { revalidatePath } from "next/cache"; import { z } from "zod"; +import { eq } from "drizzle-orm"; import { db } from "@/db"; import { clients, projects, payments } from "@/db/schema"; +function toSlug(name: string): string { + return name + .toLowerCase() + .normalize("NFD") + .replace(/[̀-ͯ]/g, "") + .replace(/[^a-z0-9]+/g, "-") + .replace(/^-+|-+$/g, "") + .slice(0, 50); +} + +async function uniqueSlug(base: string): Promise { + if (base.length < 3) return null; + const candidates = [base, ...Array.from({ length: 8 }, (_, i) => `${base.slice(0, 47)}-${i + 2}`)]; + for (const candidate of candidates) { + const rows = await db.select({ id: clients.id }).from(clients).where(eq(clients.slug, candidate)).limit(1); + if (rows.length === 0) return candidate; + } + return null; +} + const createClientSchema = z.object({ name: z.string().min(1, "Nome richiesto"), brand_name: z.string().min(1, "Nome brand richiesto"), @@ -26,6 +47,9 @@ export async function createClient(formData: FormData) { ); } + // Auto-generate slug from name (e.g. "Mario Rossi" → "mario-rossi") + const slug = await uniqueSlug(toSlug(parsed.data.name)); + // Insert client — token and id are auto-generated by $defaultFn(() => nanoid()) const [newClient] = await db .insert(clients) @@ -33,6 +57,7 @@ export async function createClient(formData: FormData) { name: parsed.data.name, brand_name: parsed.data.brand_name, brief: parsed.data.brief, + slug, }) .returning({ id: clients.id, token: clients.token, brand_name: clients.brand_name }); diff --git a/src/components/admin/ClientRow.tsx b/src/components/admin/ClientRow.tsx index 768e676..aa8ff77 100644 --- a/src/components/admin/ClientRow.tsx +++ b/src/components/admin/ClientRow.tsx @@ -53,12 +53,12 @@ export function ClientRow({ client }: { client: ClientWithPayments }) { - /client/{client.token.slice(0, 8)}… + /client/{(client.slug || client.token).slice(0, 12)}… diff --git a/src/components/admin/ProjectRow.tsx b/src/components/admin/ProjectRow.tsx index 5f21fe6..0f422bb 100644 --- a/src/components/admin/ProjectRow.tsx +++ b/src/components/admin/ProjectRow.tsx @@ -52,6 +52,7 @@ export function ProjectRow({ project }: { project: ProjectWithPayments }) { `coalesce(sum(${clients.accepted_total}::numeric), 0)` }) - .from(clients) + .select({ total: sql`coalesce(sum(${projects.accepted_total}::numeric), 0)` }) + .from(projects) + .innerJoin(clients, eq(projects.client_id, clients.id)) .where(sql`extract(year from ${clients.created_at}) = ${year}`); const [clientsRow] = await db diff --git a/src/proxy.ts b/src/proxy.ts index 417b37b..c48217f 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -38,23 +38,21 @@ export async function proxy(request: NextRequest) { const slugOrToken = slugOrTokenMatch[1]; try { - // Call internal Node.js API route — Edge middleware cannot use postgres-js directly - // postgres-js requires Node.js net/tls which are unavailable in the Edge runtime + // Use localhost to avoid hairpin NAT issues in Docker. + // request.url is the external hostname (via Traefik); inside the container the app is on localhost. + const port = process.env.PORT ?? "3000"; + const base = `http://localhost:${port}`; // Try slug first (D-06) — user-friendly slugs before token fallback - const validateSlugUrl = new URL( - `/api/internal/validate-slug?slug=${encodeURIComponent(slugOrToken)}`, - request.url + let res = await fetch( + `${base}/api/internal/validate-slug?slug=${encodeURIComponent(slugOrToken)}` ); - let res = await fetch(validateSlugUrl.toString()); // If slug not found, fall back to token validation (existing links continue to work) if (!res.ok) { - const validateTokenUrl = new URL( - `/api/internal/validate-token?token=${encodeURIComponent(slugOrToken)}`, - request.url + res = await fetch( + `${base}/api/internal/validate-token?token=${encodeURIComponent(slugOrToken)}` ); - res = await fetch(validateTokenUrl.toString()); } if (!res.ok) {