// lib/report.ts // SERVER-ONLY — builds the JSON block list that minimax-pdf consumes. // Used by /api/report/[type]/[id]/pdf and .../email routes. // // Block types we emit (see minimax-pdf skill SKILL.md): // h1, h2, callout, table, spacer, divider, caption // h2 automatically carries `keepWithNext` so headings never orphan // at the bottom of a page. import "server-only"; import type { YsterbaardCompany, YsterbaardDirector } from "@/lib/types"; // ─── Public API ────────────────────────────────────────────────────────────── export type ReportType = "company" | "director"; export interface ReportData { type: ReportType; company?: YsterbaardCompany & { expand?: Record }; director?: YsterbaardDirector; linkedCompanies?: Array<{ company: YsterbaardCompany; director_type?: string }>; linkedDirectors?: Array<{ director: YsterbaardDirector; director_type?: string }>; } export type ContentBlock = | { type: "h1"; text: string } | { type: "h2"; text: string } | { type: "h3"; text: string } | { type: "callout"; text: string } | { type: "spacer"; pt?: number } | { type: "divider" } | { type: "caption"; text: string } | { type: "pagebreak" } | { type: "table"; col_widths: number[]; headers: string[]; rows: (string | number)[][] }; export function buildReportBlocks(data: ReportData): ContentBlock[] { return data.type === "company" ? companyBlocks(data) : directorBlocks(data); } export function reportTitle(data: ReportData): string { if (data.type === "company") { return `Company Compliance Report — ${data.company?.enterprise_name || "—"}`; } const name = [data.director?.first_names, data.director?.surname] .filter(Boolean).join(" ").trim() || "—"; return `Director Compliance Report — ${name}`; } export function reportFilename(data: ReportData): string { const slugify = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""); if (data.type === "company") { return `${slugify(data.company?.enterprise_name || "company")}_company_report.pdf`; } const name = slugify( [data.director?.first_names, data.director?.surname].filter(Boolean).join("_") ) || "director"; return `${name}_director_report.pdf`; } // ─── Helpers ──────────────────────────────────────────────────────────────── function val(s: unknown): string { const v = String(s ?? "").trim(); return v || "—"; } function fmtDate(d: unknown): string { if (!d) return "—"; try { return new Date(String(d)).toLocaleDateString("en-ZA", { day: "2-digit", month: "long", year: "numeric", }); } catch { return "—"; } } function fmtNumber(n: unknown): string { const v = Number(n); return isNaN(v) ? "—" : v.toLocaleString("en-ZA"); } const MONTHS = ["", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; function fmtMonth(m: unknown): string { const n = Number(m); if (!n || n < 1 || n > 12) return "—"; return MONTHS[n]; } function fmtSource(s: unknown): string { return val(s).replace(/bizportal/gi, "BizPortal"); } function joinAddr(...parts: unknown[]): string { const filtered = parts .map((p) => (p == null ? null : String(p).trim())) .filter((p) => p !== null && p !== "" && p !== "None"); return filtered.join(", "); } function payePeriod(v: unknown): string { if (!v || v === 0) return "None"; if (v === 1) return "Monthly"; return `Every ${v} months`; } function vatPeriod(v: unknown): string { if (!v || v === 0) return "None"; const map: Record = { 1: "Monthly", 2: "Even months", 3: "Uneven months", 6: "Bi-annually", 12: "Annually", }; return map[Number(v)] ?? `Every ${v} months`; } function arStatusText(c: YsterbaardCompany): string { return c.ar_outstanding ? `OUTSTANDING — ${c.ar_outstanding_details || "see CIPC for the next filing date"}` : `Compliant — ${c.ar_outstanding_details || "all filings up to date"}`; } function entityStatusText(status: unknown): string { const u = String(status ?? "").toUpperCase(); return u.includes("IN BUSINESS") ? "Active — in good standing" : val(status); } // ─── Company report ───────────────────────────────────────────────────────── function companyBlocks(data: ReportData): ContentBlock[] { const c = data.company!; const banner_status = entityStatusText(c.enterprise_status); const ar_status = arStatusText(c); // CIPC physical address (prefer) → fall back to manual fields const phys = joinAddr( c.address_line1 || c.physical_address_line_1, c.address_line2 || c.physical_address_line_2, c.address_street, c.address_city || c.physical_town_city, c.address_province || c.physical_province, c.address_zip || c.physical_postal_code, c.address_country, ); // PO Box / private bag address (manual) const pob = joinAddr( c.pob_address_line1, c.pob_address_line2, c.pob_city, c.pob_state, c.pob_zip, c.pob_country, ); // Postal address: combined field → split manual fields const post = c.postal_address || joinAddr( c.postal_address_line_1, c.postal_address_line_2, c.postal_town_city, c.postal_province, c.postal_postal_code, ); const groups: string[] = ( (c.expand?.groups as Array<{ name: string }> | undefined) ?? [] ).map((g) => g.name); const yearsRegistered = c.registration_date ? String(new Date().getFullYear() - new Date(String(c.registration_date)).getFullYear()) : "—"; const linked = (data.linkedDirectors || []).map((j) => { const d = j.director; const disq = String(d?.disqualification_status ?? "").toUpperCase(); const status = disq.includes("NOT DISQUAL") ? "Clear" : (d?.disqualification_status || "—"); return [ [d?.first_names, d?.surname].filter(Boolean).join(" "), val(d?.id_no), j.director_type || "Director", status, ]; }); const linkedRows: (string | number)[][] = linked.length ? linked : [["—", "—", "—", "—"]]; return [ { type: "h1", text: "Company Compliance Report" }, { type: "callout", text: `Prepared for: ${val(c.enterprise_name)} (Reg. No. ${val(c.enterprise_no)})
` + `Entity status: ${banner_status}
` + `Annual return status: ${ar_status}` }, { type: "h2", text: "At a glance" }, { type: "table", col_widths: [0.25, 0.25, 0.25, 0.25], headers: ["Director status", "Entity status", "Years registered", "Financial year end"], rows: [[ val(c.director_status), val(c.enterprise_status), yearsRegistered, fmtMonth(c.financial_year_end_month) ]], }, { type: "h2", text: "1 · Company identity" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Enterprise number", val(c.enterprise_no)], ["Enterprise name", val(c.enterprise_name)], ["Enterprise type", val(c.enterprise_type)], ["Enterprise status", val(c.enterprise_status)], ["Registration date", fmtDate(c.registration_date)], ["Financial year end", fmtMonth(c.financial_year_end_month)], ["Authorised shares", fmtNumber(c.authorised_shares)], ["Source", fmtSource(c.source)], ["Groups", groups.length ? groups.join(", ") : "None assigned"], ["First seen", fmtDate(c.first_seen)], ["Last checked", fmtDate(c.last_checked)], ]}, { type: "h2", text: "2 · Registered addresses" }, { type: "h3", text: "CIPC physical address" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["CIPC physical address", val(c.physical_address)], ] }, { type: "h3", text: "Physical address (manual)" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Address line 1", val(c.physical_address_line_1)], ["Address line 2", val(c.physical_address_line_2)], ["Town / city", val(c.physical_town_city)], ["Province", val(c.physical_province)], ["Postal code", val(c.physical_postal_code)], ]}, { type: "h3", text: "CIPC postal address" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["CIPC postal address", val(c.postal_address)], ] }, { type: "h3", text: "Postal address (manual)" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Address line 1", val(c.postal_address_line_1)], ["Address line 2", val(c.postal_address_line_2)], ["Town / city", val(c.postal_town_city)], ["Province", val(c.postal_province)], ["Postal code", val(c.postal_postal_code)], ]}, { type: "h3", text: "PO Box address" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["PO Box line 1", val(c.pob_address_line1)], ["PO Box line 2", val(c.pob_address_line2)], ["Town / city", val(c.pob_city)], ["Province", val(c.pob_state)], ["Postal code", val(c.pob_zip)], ]}, { type: "h3", text: "Contact details" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Email", val(c.company_email)], ["Cell phone", val(c.company_cell_phone)], ]}, { type: "h2", text: "3 · Governance & company officers" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Public officer", val(c.public_officer_name)], ["Company secretary", val(c.company_secretary_name)], ["Director status", val(c.director_status)], ["Compliance notice", val(c.compliance_notice)], ]}, { type: "h2", text: "4 · CIPC compliance snapshot" }, { type: "callout", text: `Annual returns: ${ar_status}` }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["B-BBEE status", val(c.bbbee_status)], ["B-BBEE certificate applied", c.bbbee_certificate_applied ? "Yes" : "No"], ["Info Regulator registered", c.info_regulator_registered ? "Yes" : "No"], ["UIF registration number", val(c.uif_registration_number)], ["Compensation fund reg. no.", val(c.compensation_fund_registration_number)], ]}, { type: "h2", text: "5 · SARS eFiling details" }, { type: "callout", text: "Confidential — credentials for the addressee only. Do not distribute." }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["SARS login username", val(c.sars_login_username)], ["SARS login password", val(c.sars_login_password)], ["SARS tax number", val(c.sars_tax_number)], ["SARS PAYE number", val(c.sars_paye_no)], ["PAYE return period", payePeriod(c.sars_paye_return_period)], ["SARS VAT number", val(c.sars_vat_no)], ["VAT return period", vatPeriod(c.sars_vat_returns_period)], ]}, { type: "h2", text: "6 · Linked directors" }, { type: "table", col_widths: [0.40, 0.25, 0.20, 0.15], headers: ["Full name", "ID number", "Appointment", "Status"], rows: linkedRows, }, { type: "spacer", pt: 18 }, { type: "divider" }, { type: "caption", text: "This report is generated from live CIPC and SARS data on file at Ysterbaard Corporate Services. For queries, contact your account manager." }, ]; } // ─── Director report ──────────────────────────────────────────────────────── function directorBlocks(data: ReportData): ContentBlock[] { const d = data.director!; const fullName = [d.first_names, d.surname].filter(Boolean).join(" ") || "—"; const disq = String(d.disqualification_status ?? "").toUpperCase(); const dirBanner = disq.includes("NOT DISQUAL") ? "Director is in good standing" : val(d.disqualification_status); const res = d.residential_address || joinAddr( d.residential_address_line_1, d.residential_address_line_2, d.residential_town_city, d.residential_province, d.residential_postal_code, ); const post = d.postal_address || joinAddr( d.postal_address_line_1, d.postal_address_line_2, d.postal_town_city, d.postal_province, d.postal_postal_code, ); const yearsRegistered = d.appointment_date ? String(new Date().getFullYear() - new Date(String(d.appointment_date)).getFullYear()) : "—"; const linkedCoRows = (data.linkedCompanies || []).map((j) => { const co = j.company; return [ val(co.enterprise_no), val(co.enterprise_name), j.director_type || "Director", val(co.enterprise_status), ]; }); const linkedRows: (string | number)[][] = linkedCoRows.length ? linkedCoRows : [["—", "—", "—", "—"]]; return [ { type: "h1", text: "Director Compliance Report" }, { type: "callout", text: `Prepared for: ${fullName} (ID No. ${val(d.id_no)})
` + `Disqualification status: ${dirBanner}
` + `Company affiliations: ${val(d.total_companies)} total · ${val(d.active_companies)} active · ${val(d.inactive_companies)} inactive` }, { type: "h2", text: "At a glance" }, { type: "table", col_widths: [0.25, 0.25, 0.25, 0.25], headers: ["Total companies", "Active companies", "Years registered", "Status"], rows: [[ val(d.total_companies), val(d.active_companies), yearsRegistered, val(d.disqualification_status) ]], }, { type: "h2", text: "1 · Director identity" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Full name", fullName], ["SA ID number", val(d.id_no)], ["Date of birth", fmtDate(d.date_of_birth)], ["Disqualification status", val(d.disqualification_status)], ["Appointment date", fmtDate(d.appointment_date)], ["First seen", fmtDate(d.first_seen)], ["Last checked", fmtDate(d.last_checked)], ]}, { type: "h2", text: "2 · Contact details" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Telephone", val(d.telephone)], ["Email", val(d.email)], ["Source", fmtSource(d.source)], ]}, { type: "h2", text: "3 · Registered addresses" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Residential address", res], ["Postal address", post], ]}, { type: "h2", text: "4 · Company affiliations" }, { type: "table", col_widths: [0.20, 0.40, 0.20, 0.20], headers: ["Reg. No.", "Company name", "Role", "Status"], rows: linkedRows, }, { type: "spacer", pt: 18 }, { type: "divider" }, { type: "caption", text: "This report is generated from live CIPC data on file at Ysterbaard Corporate Services. For queries, contact your account manager." }, ]; } // ─── Group report ───────────────────────────────────────────────────────────── export interface GroupReportData { group: { id: string; name: string; description?: string }; companies: Array }>; directors: Array; /** companyId → its linked directors (with expand.director) */ companyDirectors?: Map>; /** directorId → their linked companies (with expand.company) */ directorCompanies?: Map>; } export function groupReportFilename(data: GroupReportData): string { const display = data.group.description || data.group.name; const slug = display.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, ""); return `${slug}_group_report.pdf`; } export function groupReportTitle(data: GroupReportData): string { const display = data.group.description || data.group.name; return `Group Compliance Report — ${display}`; } export function buildGroupReportBlocks(data: GroupReportData): ContentBlock[] { const { group, companies, directors, companyDirectors, directorCompanies } = data; const display = group.description || group.name; const today = new Date().toLocaleDateString("en-ZA", { day: "2-digit", month: "long", year: "numeric", }); const blocks: ContentBlock[] = [ { type: "h1", text: `Group Compliance Report — ${display}` }, { type: "spacer", pt: 4 }, // ── Individual company reports ──────────────────────────────── ...companies.flatMap((company, idx) => { const coDirectors = companyDirectors?.get(company.id) ?? []; return [ { type: "pagebreak" as const }, ...buildCompanySectionBlocks(company, coDirectors, idx + 1), ]; }), // ── Individual director reports ───────────────────────────── ...directors.flatMap((director, idx) => { const dCompanies = directorCompanies?.get(String(director.id_no ?? director.id)) ?? []; return [ { type: "pagebreak" as const }, ...buildDirectorSectionBlocks(director, dCompanies, idx + 1), ]; }), { type: "spacer", pt: 18 }, { type: "divider" }, { type: "caption", text: `Group Compliance Report — ${display}. Generated ${today} by Ysterbaard Corporate Services. Confidential — for the intended recipient only.`, }, ]; return blocks; } // ─── Individual company section blocks ─────────────────────────────────────── function buildCompanySectionBlocks( c: YsterbaardCompany & { expand?: Record }, linkedDirectors: Array<{ director: YsterbaardDirector; director_type?: string }>, index: number, ): ContentBlock[] { const banner_status = entityStatusText(c.enterprise_status); const ar_status = arStatusText(c); // CIPC physical address (prefer) → fall back to manual fields const phys = joinAddr( c.address_line1 || c.physical_address_line_1, c.address_line2 || c.physical_address_line_2, c.address_street, c.address_city || c.physical_town_city, c.address_province || c.physical_province, c.address_zip || c.physical_postal_code, c.address_country, ); // PO Box / private bag address (manual) const pob = joinAddr( c.pob_address_line1, c.pob_address_line2, c.pob_city, c.pob_state, c.pob_zip, c.pob_country, ); // Postal address: combined field → split manual fields const post = c.postal_address || joinAddr( c.postal_address_line_1, c.postal_address_line_2, c.postal_town_city, c.postal_province, c.postal_postal_code, ); const groups: string[] = ( (c.expand?.groups as Array<{ name: string }> | undefined) ?? [] ).map((g) => g.name); const yearsRegistered = c.registration_date ? String(new Date().getFullYear() - new Date(String(c.registration_date)).getFullYear()) : "—"; const linked = linkedDirectors.map((j) => { const d = j.director; const disq = String(d?.disqualification_status ?? "").toUpperCase(); const status = disq.includes("NOT DISQUAL") ? "Clear" : (d?.disqualification_status || "—"); return [ [d?.first_names, d?.surname].filter(Boolean).join(" "), val(d?.id_no), j.director_type || "Director", status, ] as (string | number)[]; }); const linkedRows = linked.length ? linked : [["—", "—", "—", "—"]] as (string | number)[][]; return [ { type: "h2", text: `${index}. ${val(c.enterprise_name)}` }, { type: "callout", text: `Reg. No.: ${val(c.enterprise_no)}  ·  ` + `Status: ${banner_status}
` + `Annual returns: ${ar_status}` }, { type: "h2", text: "At a glance" }, { type: "table", col_widths: [0.25, 0.25, 0.25, 0.25], headers: ["Director status", "Entity status", "Years registered", "Financial year end"], rows: [[val(c.director_status), val(c.enterprise_status), yearsRegistered, fmtMonth(c.financial_year_end_month)]], }, { type: "h2", text: "1 · Company identity" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Enterprise number", val(c.enterprise_no)], ["Enterprise name", val(c.enterprise_name)], ["Enterprise type", val(c.enterprise_type)], ["Enterprise status", val(c.enterprise_status)], ["Registration date", fmtDate(c.registration_date)], ["Financial year end", fmtMonth(c.financial_year_end_month)], ["Authorised shares", fmtNumber(c.authorised_shares)], ["Source", fmtSource(c.source)], ["Groups", groups.length ? groups.join(", ") : "None assigned"], ["First seen", fmtDate(c.first_seen)], ["Last checked", fmtDate(c.last_checked)], ] }, { type: "h2", text: "2 · Registered addresses" }, { type: "h3", text: "CIPC physical address" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["CIPC physical address", val(c.physical_address)], ] }, { type: "h3", text: "Physical address (manual)" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Address line 1", val(c.physical_address_line_1)], ["Address line 2", val(c.physical_address_line_2)], ["Town / city", val(c.physical_town_city)], ["Province", val(c.physical_province)], ["Postal code", val(c.physical_postal_code)], ]}, { type: "h3", text: "CIPC postal address" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["CIPC postal address", val(c.postal_address)], ] }, { type: "h3", text: "Postal address (manual)" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Address line 1", val(c.postal_address_line_1)], ["Address line 2", val(c.postal_address_line_2)], ["Town / city", val(c.postal_town_city)], ["Province", val(c.postal_province)], ["Postal code", val(c.postal_postal_code)], ]}, { type: "h3", text: "PO Box address" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["PO Box line 1", val(c.pob_address_line1)], ["PO Box line 2", val(c.pob_address_line2)], ["Town / city", val(c.pob_city)], ["Province", val(c.pob_state)], ["Postal code", val(c.pob_zip)], ]}, { type: "h3", text: "Contact details" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Email", val(c.company_email)], ["Cell phone", val(c.company_cell_phone)], ]}, { type: "h2", text: "3 · Governance & company officers" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Public officer", val(c.public_officer_name)], ["Company secretary", val(c.company_secretary_name)], ["Director status", val(c.director_status)], ["Compliance notice", val(c.compliance_notice)], ] }, { type: "h2", text: "4 · CIPC compliance snapshot" }, { type: "callout", text: `Annual returns: ${ar_status}` }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["B-BBEE status", val(c.bbbee_status)], ["B-BBEE certificate applied", c.bbbee_certificate_applied ? "Yes" : "No"], ["Info Regulator registered", c.info_regulator_registered ? "Yes" : "No"], ["UIF registration number", val(c.uif_registration_number)], ["Compensation fund reg. no.", val(c.compensation_fund_registration_number)], ] }, { type: "h2", text: "5 · SARS eFiling details" }, { type: "callout", text: "Confidential — credentials for the addressee only. Do not distribute." }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["SARS login username", val(c.sars_login_username)], ["SARS login password", val(c.sars_login_password)], ["SARS tax number", val(c.sars_tax_number)], ["SARS PAYE number", val(c.sars_paye_no)], ["PAYE return period", payePeriod(c.sars_paye_return_period)], ["SARS VAT number", val(c.sars_vat_no)], ["VAT return period", vatPeriod(c.sars_vat_returns_period)], ] }, { type: "h2", text: "6 · Linked directors" }, { type: "table", col_widths: [0.40, 0.25, 0.20, 0.15], headers: ["Full name", "ID number", "Appointment", "Status"], rows: linkedRows, }, ]; } // ─── Individual director section blocks ─────────────────────────────────────── function buildDirectorSectionBlocks( d: YsterbaardDirector, linkedCompanies: Array<{ company: YsterbaardCompany; director_type?: string }>, index: number, ): ContentBlock[] { const fullName = [d.first_names, d.surname].filter(Boolean).join(" ") || "—"; const disq = String(d.disqualification_status ?? "").toUpperCase(); const dirBanner = disq.includes("NOT DISQUAL") ? "Director is in good standing" : val(d.disqualification_status); const res = d.residential_address || joinAddr( d.residential_address_line_1, d.residential_address_line_2, d.residential_town_city, d.residential_province, d.residential_postal_code, ); const post = d.postal_address || joinAddr( d.postal_address_line_1, d.postal_address_line_2, d.postal_town_city, d.postal_province, d.postal_postal_code, ); const yearsRegistered = d.appointment_date ? String(new Date().getFullYear() - new Date(String(d.appointment_date)).getFullYear()) : "—"; const linkedCoRows = linkedCompanies.map((j) => [ val(j.company.enterprise_no), val(j.company.enterprise_name), j.director_type || "Director", val(j.company.enterprise_status), ] as (string | number)[]); const linkedRows = linkedCoRows.length ? linkedCoRows : [["—", "—", "—", "—"]] as (string | number)[][]; return [ { type: "h2", text: `${index}. ${fullName}` }, { type: "callout", text: `ID No.: ${val(d.id_no)}  ·  ` + `Status: ${dirBanner}
` + `Companies: ${val(d.total_companies)} total · ${val(d.active_companies)} active · ${val(d.inactive_companies)} inactive` }, { type: "h2", text: "At a glance" }, { type: "table", col_widths: [0.25, 0.25, 0.25, 0.25], headers: ["Total companies", "Active companies", "Years registered", "Status"], rows: [[val(d.total_companies), val(d.active_companies), yearsRegistered, val(d.disqualification_status)]], }, { type: "h2", text: "1 · Director identity" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Full name", fullName], ["SA ID number", val(d.id_no)], ["Date of birth", fmtDate(d.date_of_birth)], ["Disqualification status", val(d.disqualification_status)], ["Appointment date", fmtDate(d.appointment_date)], ["First seen", fmtDate(d.first_seen)], ["Last checked", fmtDate(d.last_checked)], ] }, { type: "h2", text: "2 · Contact details" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Telephone", val(d.telephone)], ["Email", val(d.email)], ["Source", fmtSource(d.source)], ] }, { type: "h2", text: "3 · Registered addresses" }, { type: "table", col_widths: [0.38, 0.62], headers: ["Field", "Value"], rows: [ ["Residential address", res], ["Postal address", post], ] }, { type: "h2", text: "4 · Company affiliations" }, { type: "table", col_widths: [0.20, 0.40, 0.20, 0.20], headers: ["Reg. No.", "Company name", "Role", "Status"], rows: linkedRows, }, ]; }