Workflow-App/Workflow/lib/report.ts

720 lines
28 KiB
TypeScript
Raw Normal View History

// 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<string, unknown> };
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<number, string> = {
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:
`<b>Prepared for:</b> ${val(c.enterprise_name)} (Reg. No. ${val(c.enterprise_no)})<br/>` +
`<b>Entity status:</b> ${banner_status}<br/>` +
`<b>Annual return status:</b> ${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: `<b>Annual returns:</b> ${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:
`<b>Prepared for:</b> ${fullName} (ID No. ${val(d.id_no)})<br/>` +
`<b>Disqualification status:</b> ${dirBanner}<br/>` +
`<b>Company affiliations:</b> ${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<YsterbaardCompany & { expand?: Record<string, unknown> }>;
directors: Array<YsterbaardDirector>;
/** companyId → its linked directors (with expand.director) */
companyDirectors?: Map<string, Array<{ director: YsterbaardDirector; director_type?: string }>>;
/** directorId → their linked companies (with expand.company) */
directorCompanies?: Map<string, Array<{ company: YsterbaardCompany; director_type?: string }>>;
}
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<string, unknown> },
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:
`<b>Reg. No.:</b> ${val(c.enterprise_no)} &nbsp;·&nbsp; ` +
`<b>Status:</b> ${banner_status}<br/>` +
`<b>Annual returns:</b> ${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: `<b>Annual returns:</b> ${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:
`<b>ID No.:</b> ${val(d.id_no)} &nbsp;·&nbsp; ` +
`<b>Status:</b> ${dirBanner}<br/>` +
`<b>Companies:</b> ${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,
},
];
}