feat(05-03): add getClientActiveOffers query + active offers section on client detail page
- 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
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { getClientWithProjects } from "@/lib/admin-queries";
|
||||
import { getClientWithProjects, getClientActiveOffers } from "@/lib/admin-queries";
|
||||
import { ClientActions } from "@/components/admin/ClientActions";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -11,7 +11,10 @@ export default async function ClientDetailPage({
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const data = await getClientWithProjects(id);
|
||||
const [data, activeOffers] = await Promise.all([
|
||||
getClientWithProjects(id),
|
||||
getClientActiveOffers(id),
|
||||
]);
|
||||
if (!data) notFound();
|
||||
|
||||
const { projects, ...client } = data;
|
||||
@@ -105,6 +108,29 @@ export default async function ClientDetailPage({
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -641,4 +641,47 @@ export async function getClientWithProjects(clientId: string): Promise<ClientWit
|
||||
created_at: p.created_at,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// ── ClientActiveOfferSummary — used by /admin/clients/[id] offers section ─────
|
||||
|
||||
export type ClientActiveOfferSummary = {
|
||||
offer_id: string;
|
||||
project_id: string;
|
||||
project_name: string;
|
||||
public_name: string; // offer_micros.public_name — NEVER internal_name
|
||||
accepted_total: string | null;
|
||||
};
|
||||
|
||||
export async function getClientActiveOffers(clientId: string): Promise<ClientActiveOfferSummary[]> {
|
||||
const projectRows = await db
|
||||
.select({ id: projects.id, name: projects.name })
|
||||
.from(projects)
|
||||
.where(eq(projects.client_id, clientId));
|
||||
|
||||
if (projectRows.length === 0) return [];
|
||||
|
||||
const projectIds = projectRows.map((p) => p.id);
|
||||
const projectNameMap = new Map(projectRows.map((p) => [p.id, p.name]));
|
||||
|
||||
// Explicit column selection — public_name only, NEVER internal_name
|
||||
const rows = await db
|
||||
.select({
|
||||
offer_id: project_offers.id,
|
||||
project_id: project_offers.project_id,
|
||||
public_name: offer_micros.public_name,
|
||||
accepted_total: project_offers.accepted_total,
|
||||
})
|
||||
.from(project_offers)
|
||||
.innerJoin(offer_micros, eq(project_offers.micro_id, offer_micros.id))
|
||||
.where(inArray(project_offers.project_id, projectIds))
|
||||
.orderBy(asc(project_offers.created_at));
|
||||
|
||||
return rows.map((r) => ({
|
||||
offer_id: r.offer_id,
|
||||
project_id: r.project_id,
|
||||
project_name: projectNameMap.get(r.project_id) ?? "—",
|
||||
public_name: r.public_name,
|
||||
accepted_total: r.accepted_total ? String(r.accepted_total) : null,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user