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 { notFound } from "next/navigation";
|
||||||
import { getClientWithProjects } from "@/lib/admin-queries";
|
import { getClientWithProjects, getClientActiveOffers } from "@/lib/admin-queries";
|
||||||
import { ClientActions } from "@/components/admin/ClientActions";
|
import { ClientActions } from "@/components/admin/ClientActions";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
@@ -11,7 +11,10 @@ export default async function ClientDetailPage({
|
|||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const data = await getClientWithProjects(id);
|
const [data, activeOffers] = await Promise.all([
|
||||||
|
getClientWithProjects(id),
|
||||||
|
getClientActiveOffers(id),
|
||||||
|
]);
|
||||||
if (!data) notFound();
|
if (!data) notFound();
|
||||||
|
|
||||||
const { projects, ...client } = data;
|
const { projects, ...client } = data;
|
||||||
@@ -105,6 +108,29 @@ export default async function ClientDetailPage({
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -641,4 +641,47 @@ export async function getClientWithProjects(clientId: string): Promise<ClientWit
|
|||||||
created_at: p.created_at,
|
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