import { cache } from "react";
import { notFound } from "next/navigation";
import {
getClientWithProjectsByToken,
getProjectView,
type ProjectView,
type ClientView,
type ClientProjectSummary,
} from "@/lib/client-view";
import { getClientGate } from "@/lib/client-gate";
import { ClientDashboard } from "@/components/client-dashboard";
import { OtpGate } from "@/components/client/OtpGate";
import { PreviewBanner } from "@/components/client/PreviewBanner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { normalizeTaskStatus } from "@/lib/task-status";
import { getAdminIdentity } from "@/lib/settings";
export const revalidate = 0;
const getCachedClientData = cache(getClientWithProjectsByToken);
// Adapter: converts ProjectView + client info into ClientView shape for ClientDashboard reuse
function projectViewToClientView(
client: ClientProjectSummary["client"],
view: ProjectView
): ClientView {
return {
client: {
id: view.project.client_id,
name: client.name,
brand_name: client.brand_name,
brief: "",
accepted_total: view.project.accepted_total,
},
phases: view.phases.map((phase) => ({
id: phase.id,
title: phase.title,
status: phase.status as "upcoming" | "active" | "done",
sort_order: phase.sort_order,
tasks: phase.tasks.map((task) => ({
id: task.id,
title: task.title,
description: task.description,
status: normalizeTaskStatus(task.status),
sort_order: task.sort_order,
deliverables: task.deliverables.map((d) => ({
id: d.id,
title: d.title,
url: d.url,
status: d.status as "pending" | "submitted" | "approved",
// approved_at is immutable once set — CLAUDE.md constraint LOCKED
approved_at: d.approved_at instanceof Date ? d.approved_at.toISOString() : null,
})),
})),
progress_pct: phase.progress_pct,
})),
payments: view.payments.map((p) => ({
id: p.id,
label: p.label,
status: p.status as "da_saldare" | "inviata" | "saldato",
due_date: p.due_date instanceof Date ? p.due_date.toISOString() : null,
paid_at: p.paid_at instanceof Date ? p.paid_at.toISOString() : null,
})),
documents: view.documents.map((d) => ({
id: d.id,
label: d.label,
url: d.url,
})),
notes: view.notes.map((n) => ({
id: n.id,
body: n.body,
created_at: n.created_at instanceof Date ? n.created_at.toISOString() : String(n.created_at),
})),
global_progress_pct: view.global_progress_pct,
activeOffers: view.activeOffers,
transcripts: view.transcripts.map((t) => ({
id: t.id,
title: t.title,
call_date: t.call_date,
content: t.content,
created_at: t.created_at instanceof Date ? t.created_at.toISOString() : String(t.created_at),
})),
};
}
export async function generateMetadata({
params,
}: {
params: Promise<{ token: string }>;
}) {
const { token } = await params;
console.log("[generateMetadata] token:", token);
const clientData = await getCachedClientData(token);
if (!clientData) return { title: "Not Found" };
return {
title: `${clientData.client.brand_name} — Stato Progetto | iamcavalli`,
description: "Dashboard stato progetto",
};
}
export default async function ClientPage({
params,
searchParams,
}: {
params: Promise<{ token: string }>;
searchParams: Promise<{ preview?: string }>;
}) {
const { token } = await params;
const { preview: previewParam } = await searchParams;
// ⚠️ Il gate va PRIMA di ogni query sui dati del progetto: se si interroga il
// DB e poi si decide di mostrare il form, i dati sono già nel payload RSC
// dell'HTML anche se non compaiono a schermo. Vedi src/lib/client-gate.ts.
const { client: identity, session, preview } = await getClientGate(token, {
previewRequested: previewParam === "1",
});
if (identity && !session && !preview) {
return
Nessun progetto disponibile al momento.
Progetto non disponibile.
)}