import { and, desc, eq, inArray } from 'drizzle-orm'; import { db } from '@/lib/db'; import { clients, clientContacts } from '@/lib/db/schema/clients'; import { interests } from '@/lib/db/schema/interests'; import { berths, berthWaitingList, berthMaintenanceLog } from '@/lib/db/schema/berths'; import { auditLogs } from '@/lib/db/schema/system'; import { ports } from '@/lib/db/schema/ports'; import { NotFoundError } from '@/lib/errors'; import { generatePdf } from '@/lib/pdf/generate'; import { clientSummaryTemplate, buildClientSummaryInputs, } from '@/lib/pdf/templates/client-summary-template'; import { berthSpecTemplate, buildBerthSpecInputs, } from '@/lib/pdf/templates/berth-spec-template'; import { interestSummaryTemplate, buildInterestSummaryInputs, } from '@/lib/pdf/templates/interest-summary-template'; // ─── Export Client PDF ──────────────────────────────────────────────────────── export async function exportClientPdf(clientId: string, portId: string): Promise { const client = await db.query.clients.findFirst({ where: eq(clients.id, clientId), }); if (!client || client.portId !== portId) { throw new NotFoundError('Client'); } const [contactList, port] = await Promise.all([ db.query.clientContacts.findMany({ where: eq(clientContacts.clientId, clientId), orderBy: (t, { desc }) => [desc(t.isPrimary), desc(t.createdAt)], }), db.query.ports.findFirst({ where: eq(ports.id, portId) }), ]); // Fetch last 20 interests for this client in this port const interestList = await db .select() .from(interests) .where(and(eq(interests.clientId, clientId), eq(interests.portId, portId))) .orderBy(desc(interests.updatedAt)) .limit(20); // Fetch last 20 audit logs for this client const activity = await db .select() .from(auditLogs) .where( and( eq(auditLogs.portId, portId), eq(auditLogs.entityType, 'client'), eq(auditLogs.entityId, clientId), ), ) .orderBy(desc(auditLogs.createdAt)) .limit(20); // Enrich interests with berth mooring numbers const berthIds = interestList .map((i) => i.berthId) .filter(Boolean) as string[]; let berthsMap: Record = {}; if (berthIds.length > 0) { const berthRows = await db .select({ id: berths.id, mooringNumber: berths.mooringNumber }) .from(berths) .where(inArray(berths.id, berthIds)); berthsMap = Object.fromEntries(berthRows.map((b) => [b.id, b.mooringNumber])); } const enrichedInterests = interestList.map((i) => ({ ...i, berthMooringNumber: i.berthId ? (berthsMap[i.berthId] ?? null) : null, })); const inputs = buildClientSummaryInputs(client, contactList, enrichedInterests, activity, port); return generatePdf(clientSummaryTemplate, [inputs]); } // ─── Export Berth PDF ───────────────────────────────────────────────────────── export async function exportBerthPdf(berthId: string, portId: string): Promise { const berth = await db.query.berths.findFirst({ where: eq(berths.id, berthId), }); if (!berth || berth.portId !== portId) { throw new NotFoundError('Berth'); } const port = await db.query.ports.findFirst({ where: eq(ports.id, portId) }); // Waiting list with client names const waitingListRows = await db .select({ id: berthWaitingList.id, position: berthWaitingList.position, priority: berthWaitingList.priority, notes: berthWaitingList.notes, clientId: berthWaitingList.clientId, }) .from(berthWaitingList) .where(eq(berthWaitingList.berthId, berthId)) .orderBy(berthWaitingList.position); const clientIds = waitingListRows.map((w) => w.clientId); let clientsMap: Record = {}; if (clientIds.length > 0) { const clientRows = await db .select({ id: clients.id, fullName: clients.fullName }) .from(clients) .where(inArray(clients.id, clientIds)); clientsMap = Object.fromEntries(clientRows.map((c) => [c.id, c.fullName])); } const enrichedWaitingList = waitingListRows.map((w) => ({ ...w, clientName: clientsMap[w.clientId] ?? 'Unknown', })); // Maintenance log (last 20) const maintenance = await db .select() .from(berthMaintenanceLog) .where(eq(berthMaintenanceLog.berthId, berthId)) .orderBy(desc(berthMaintenanceLog.performedDate)) .limit(20); // Linked interests const linkedInterests = await db .select() .from(interests) .where(and(eq(interests.berthId, berthId), eq(interests.portId, portId))) .orderBy(desc(interests.updatedAt)) .limit(20); const inputs = buildBerthSpecInputs(berth, enrichedWaitingList, maintenance, linkedInterests, port); return generatePdf(berthSpecTemplate, [inputs]); } // ─── Export Interest PDF ────────────────────────────────────────────────────── export async function exportInterestPdf(interestId: string, portId: string): Promise { const interest = await db.query.interests.findFirst({ where: eq(interests.id, interestId), }); if (!interest || interest.portId !== portId) { throw new NotFoundError('Interest'); } const [client, port] = await Promise.all([ db.query.clients.findFirst({ where: eq(clients.id, interest.clientId) }), db.query.ports.findFirst({ where: eq(ports.id, portId) }), ]); let berth = null; if (interest.berthId) { berth = await db.query.berths.findFirst({ where: eq(berths.id, interest.berthId) }); } // Audit timeline (last 20 events for this interest) const timeline = await db .select() .from(auditLogs) .where( and( eq(auditLogs.portId, portId), eq(auditLogs.entityType, 'interest'), eq(auditLogs.entityId, interestId), ), ) .orderBy(desc(auditLogs.createdAt)) .limit(20); const inputs = buildInterestSummaryInputs(interest, client, berth, timeline, port); return generatePdf(interestSummaryTemplate, [inputs]); }