20f4fd8603
- Add ClientActiveOfferSummary type to admin-queries.ts - Add getClientActiveOffers() function joining project_offers with offer_micros - Only selects public_name (never internal_name) per CLAUDE.md constraint - Update /admin/clients/[id] to fetch active offers in parallel with project data - Add Offerte Attive section at bottom of client detail page
136 lines
5.1 KiB
TypeScript
136 lines
5.1 KiB
TypeScript
import { notFound } from "next/navigation";
|
|
import { getClientWithProjects, getClientActiveOffers } from "@/lib/admin-queries";
|
|
import { ClientActions } from "@/components/admin/ClientActions";
|
|
import Link from "next/link";
|
|
|
|
export const revalidate = 0;
|
|
|
|
export default async function ClientDetailPage({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>;
|
|
}) {
|
|
const { id } = await params;
|
|
const [data, activeOffers] = await Promise.all([
|
|
getClientWithProjects(id),
|
|
getClientActiveOffers(id),
|
|
]);
|
|
if (!data) notFound();
|
|
|
|
const { projects, ...client } = data;
|
|
const activeProjects = projects.filter((p) => !p.archived);
|
|
const archivedProjects = projects.filter((p) => p.archived);
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-4">
|
|
<Link href="/admin" className="text-sm text-[#71717a] hover:text-[#1a1a1a]">
|
|
← Clienti
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="mb-6 flex items-start justify-between gap-4 flex-wrap">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-[#1a1a1a]">{client.name}</h1>
|
|
<p className="text-sm text-[#71717a]">{client.brand_name}</p>
|
|
{client.archived && (
|
|
<span className="inline-block mt-1 text-xs font-medium bg-[#f4f4f5] text-[#71717a] px-2 py-0.5 rounded-full">
|
|
Archiviato
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<Link
|
|
href={`/admin/projects/new?client_id=${id}`}
|
|
className="text-sm bg-[#1A463C] text-white px-4 py-2 rounded-lg hover:bg-[#1A463C]/90 transition-colors"
|
|
>
|
|
+ Nuovo Progetto
|
|
</Link>
|
|
<a
|
|
href={`/client/${client.token}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-[#1A463C] hover:underline font-mono bg-[#1A463C]/5 px-2 py-1 rounded"
|
|
>
|
|
Link cliente →
|
|
</a>
|
|
<ClientActions clientId={client.id} archived={client.archived ?? false} />
|
|
</div>
|
|
</div>
|
|
|
|
{activeProjects.length === 0 && (
|
|
<div className="bg-white rounded-xl border border-[#e5e7eb] p-12 text-center">
|
|
<p className="text-[#71717a] mb-4">Nessun progetto ancora per questo cliente.</p>
|
|
<Link
|
|
href={`/admin/projects/new?client_id=${id}`}
|
|
className="text-sm text-[#1A463C] hover:underline"
|
|
>
|
|
+ Crea il primo progetto
|
|
</Link>
|
|
</div>
|
|
)}
|
|
|
|
{activeProjects.length > 0 && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{activeProjects.map((project) => (
|
|
<Link
|
|
key={project.id}
|
|
href={`/admin/projects/${project.id}`}
|
|
className="bg-white border border-[#e5e7eb] rounded-xl p-5 hover:shadow-md hover:border-[#1A463C]/20 transition-all"
|
|
>
|
|
<h3 className="font-bold text-[#1a1a1a] mb-1">{project.name}</h3>
|
|
<p className="text-sm text-[#71717a]">
|
|
{project.accepted_total && parseFloat(project.accepted_total) > 0
|
|
? `€${parseFloat(project.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}`
|
|
: "Preventivo non impostato"}
|
|
</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{archivedProjects.length > 0 && (
|
|
<div className="mt-8">
|
|
<p className="text-xs text-[#71717a] font-semibold uppercase tracking-wider mb-3">
|
|
Archiviati ({archivedProjects.length})
|
|
</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 opacity-60">
|
|
{archivedProjects.map((project) => (
|
|
<Link
|
|
key={project.id}
|
|
href={`/admin/projects/${project.id}`}
|
|
className="bg-white border border-[#e5e7eb] rounded-xl p-5 hover:shadow-md transition-all"
|
|
>
|
|
<h3 className="font-bold text-[#1a1a1a] mb-1">{project.name}</h3>
|
|
<p className="text-xs text-[#71717a]">Archiviato</p>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeOffers.length > 0 && (
|
|
<div className="mt-8">
|
|
<p className="text-xs text-[#71717a] font-semibold uppercase tracking-wider mb-3">
|
|
Offerte Attive ({activeOffers.length})
|
|
</p>
|
|
<div className="bg-white rounded-xl border border-[#e5e7eb] divide-y divide-[#e5e7eb]">
|
|
{activeOffers.map((offer) => (
|
|
<div key={offer.offer_id} className="flex items-center justify-between px-4 py-3">
|
|
<div>
|
|
<p className="text-sm font-medium text-[#1a1a1a]">{offer.public_name}</p>
|
|
<p className="text-xs text-[#71717a]">{offer.project_name}</p>
|
|
</div>
|
|
{offer.accepted_total && (
|
|
<span className="text-sm font-mono text-[#1a1a1a]">
|
|
€{parseFloat(offer.accepted_total).toLocaleString("it-IT", { minimumFractionDigits: 2 })}
|
|
</span>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |