Adds the full Next.js app (app/, components/, lib/, scripts/, assets/, docs/, Dockerfile, compose, configs) to version control. Security: - CIPC BizPortal customer-code logins (Login 1 + Login 3) now read CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE / CIPC_LOGIN3_PASSWORD from .env.local instead of source. - BizPortal personal login in apply-bizportal route reads BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD. - Added .env.example as the committed template. .gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright storage_state.json (live CIPC cookies), scratch dirs, client PDFs and debug screenshots.
502 lines
23 KiB
TypeScript
502 lines
23 KiB
TypeScript
// app/api/resignations/resend/route.ts
|
|
// POST /api/resignations/resend
|
|
//
|
|
// Re-send an existing resignation's email.
|
|
// mail=initial → re-send the initial letter
|
|
// mail=signed → re-send the signed distro (needs stored signature)
|
|
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
import { fetchRecord, updateRecordMultipart } from "@/lib/pb";
|
|
import type { YsterbaardDirectorResignation } from "@/lib/types";
|
|
import { spawn } from "child_process";
|
|
import { HIMALAYA_BIN, himalayaSendArgs } from "@/lib/himalaya";
|
|
|
|
const PDFD_URL = process.env.PDFD_URL || "http://127.0.0.1:3070";
|
|
const PB_URL = "https://pb.agent.apphub.co.za";
|
|
const PB_TOKEN = process.env.PB_TOKEN || "";
|
|
|
|
interface ResigExpand {
|
|
director?: Record<string, unknown>;
|
|
company?: Record<string, unknown>;
|
|
}
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const body = await req.json();
|
|
const { id, mail } = body as { id?: string; mail?: "initial" | "signed" };
|
|
|
|
if (!id) return NextResponse.json({ error: "id required" }, { status: 400 });
|
|
if (mail !== "initial" && mail !== "signed") {
|
|
return NextResponse.json({ error: "mail must be 'initial' or 'signed'" }, { status: 400 });
|
|
}
|
|
|
|
const appt = await fetchRecord<YsterbaardDirectorResignation>(
|
|
"ysterbaard_director_resignations",
|
|
id,
|
|
"director,company"
|
|
);
|
|
const director = (appt.expand as ResigExpand | undefined)?.director ?? {};
|
|
const company = (appt.expand as ResigExpand | undefined)?.company ?? {};
|
|
|
|
const directorName = [director.first_names, director.surname].filter(Boolean).join(" ") || "Director";
|
|
const companyName = String(company.enterprise_name ?? "the Company");
|
|
const signUrl = `${process.env.NEXT_PUBLIC_APP_URL || "http://localhost:6001"}/resign/${appt.sign_token}`;
|
|
const to = appt.sent_to || "sj@dyfins.com";
|
|
|
|
const dirAddr = pickAddress(
|
|
{
|
|
line_1: director.residential_address_line_1 as string | null | undefined,
|
|
line_2: director.residential_address_line_2 as string | null | undefined,
|
|
town_city: director.residential_town_city as string | null | undefined,
|
|
province: director.residential_province as string | null | undefined,
|
|
postal_code: director.residential_postal_code as string | null | undefined,
|
|
},
|
|
director.residential_address as string | undefined
|
|
);
|
|
const coAddr = pickAddress(
|
|
{
|
|
line_1: company.physical_address_line_1 as string | null | undefined,
|
|
line_2: company.physical_address_line_2 as string | null | undefined,
|
|
town_city: company.physical_town_city as string | null | undefined,
|
|
province: company.physical_province as string | null | undefined,
|
|
postal_code: company.physical_postal_code as string | null | undefined,
|
|
},
|
|
company.physical_address as string | undefined
|
|
);
|
|
|
|
let eml: string;
|
|
let pdfBytes: Buffer | null = null;
|
|
let pdfFilename = "";
|
|
|
|
if (mail === "initial") {
|
|
const today = new Date().toISOString().split("T")[0];
|
|
const html = letterHtml({
|
|
directorName,
|
|
directorIdNo: String(director.id_no ?? ""),
|
|
directorEmail: String(director.email ?? ""),
|
|
directorCellPhone: String(director.telephone ?? ""),
|
|
residentialAddressLine1: dirAddr.line_1,
|
|
residentialAddressLine2: dirAddr.line_2,
|
|
residentialTownCity: dirAddr.town_city,
|
|
residentialProvince: dirAddr.province,
|
|
residentialPostalCode: dirAddr.postal_code,
|
|
companyName,
|
|
physicalAddressLine1: coAddr.line_1,
|
|
physicalAddressLine2: coAddr.line_2,
|
|
physicalTownCity: coAddr.town_city,
|
|
physicalProvince: coAddr.province,
|
|
physicalPostalCode: coAddr.postal_code,
|
|
signUrl,
|
|
letterDate: today,
|
|
});
|
|
pdfFilename = `resignation-letter-${companyName.replace(/[^A-Za-z0-9]/g, "_")}.pdf`;
|
|
pdfBytes = await renderPdf(html, `resignation-letter-${companyName.replace(/[^A-Za-z0-9]/g, "_")}`);
|
|
eml = buildInitialEml({ to, directorName, companyName, signUrl, pdfBytes, pdfFilename });
|
|
} else {
|
|
// signed
|
|
const signatureBase64 = await fetchSignatureBase64(appt);
|
|
if (!signatureBase64) {
|
|
return NextResponse.json({ error: "No signature stored — cannot resend signed copy." }, { status: 400 });
|
|
}
|
|
const date = new Date().toISOString().split("T")[0];
|
|
const directorForRender = {
|
|
...director,
|
|
residential_address_line_1: dirAddr.line_1,
|
|
residential_address_line_2: dirAddr.line_2,
|
|
residential_town_city: dirAddr.town_city,
|
|
residential_province: dirAddr.province,
|
|
residential_postal_code: dirAddr.postal_code,
|
|
};
|
|
const companyForRender = {
|
|
...company,
|
|
physical_address_line_1: coAddr.line_1,
|
|
physical_address_line_2: coAddr.line_2,
|
|
physical_town_city: coAddr.town_city,
|
|
physical_province: coAddr.province,
|
|
physical_postal_code: coAddr.postal_code,
|
|
};
|
|
const html = signedLetterHtml(directorForRender, companyForRender, date, signatureBase64, signUrl);
|
|
pdfFilename = `signed-resignation-${companyName.replace(/[^A-Za-z0-9]/g, "_")}.pdf`;
|
|
pdfBytes = await renderPdf(html, `signed-resignation-${companyName.replace(/[^A-Za-z0-9]/g, "_")}`);
|
|
eml = buildSignedEml({ to, directorName, companyName, date, pdfBytes, pdfFilename });
|
|
}
|
|
|
|
// Persist to PB
|
|
if (pdfBytes) {
|
|
try {
|
|
const pdfFile = new File([pdfBytes], pdfFilename, { type: "application/pdf" });
|
|
await updateRecordMultipart(
|
|
"ysterbaard_director_resignations",
|
|
appt.id,
|
|
{},
|
|
[{ field: "letter", file: pdfFile }]
|
|
);
|
|
} catch (saveErr) {
|
|
console.error("Resend: failed to persist PDF to PB:", saveErr);
|
|
}
|
|
}
|
|
|
|
await sendViaHimalaya(eml);
|
|
return NextResponse.json({ ok: true, mail });
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
console.error("Resend error:", msg);
|
|
return NextResponse.json({ error: msg }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
function pickAddress(
|
|
structured: { line_1?: string | null; line_2?: string | null; town_city?: string | null; province?: string | null; postal_code?: string | null },
|
|
flat?: string | null
|
|
): { line_1: string; line_2: string; town_city: string; province: string; postal_code: string } {
|
|
const s = (v: string | null | undefined) => (v ?? "").trim();
|
|
const line1 = s(structured.line_1);
|
|
const town = s(structured.town_city);
|
|
const zip = s(structured.postal_code);
|
|
const structuredLooksReal =
|
|
(line1 && !/FIXED|TEST|PLACEHOLDER|XXX/i.test(line1)) ||
|
|
(town && !/^(JOHANNESBURG|PRETORIA|CAPE TOWN|TEST)$/i.test(town)) ||
|
|
(zip && zip !== "2000" && zip !== "0000");
|
|
if (structuredLooksReal && (line1 || town || zip)) {
|
|
return { line_1: line1, line_2: s(structured.line_2), town_city: town, province: s(structured.province), postal_code: zip };
|
|
}
|
|
if (!flat) return { line_1: "", line_2: "", town_city: "", province: "", postal_code: "" };
|
|
const parts = flat.split(",").map((p) => p.trim()).filter(Boolean);
|
|
if (parts.length >= 5) {
|
|
return { line_1: parts[0], line_2: "", town_city: [parts[1], parts[2]].filter(Boolean).join(", "), province: parts[3], postal_code: parts[4] };
|
|
}
|
|
if (parts.length === 4) return { line_1: parts[0], line_2: "", town_city: parts[1], province: parts[2], postal_code: parts[3] };
|
|
if (parts.length === 3) return { line_1: parts[0], line_2: "", town_city: parts[1], province: "", postal_code: parts[2] };
|
|
if (parts.length === 2) return { line_1: parts[0], line_2: "", town_city: parts[1], province: "", postal_code: "" };
|
|
return { line_1: parts[0] || "", line_2: "", town_city: "", province: "", postal_code: "" };
|
|
}
|
|
|
|
/** Render the PDF, or return null if the local renderer is down.
|
|
*
|
|
* The pdfd service on PDFD_URL is often not running. Previously a failed
|
|
* render threw, which aborted the whole resend BEFORE himalaya was ever
|
|
* called — so a dead PDF service silently blocked email delivery and the
|
|
* user saw a bare 500. A missing attachment is recoverable; a lost letter
|
|
* is not, so we degrade to an HTML-only email and say so. */
|
|
async function renderPdf(html: string, filename: string): Promise<Buffer | null> {
|
|
try {
|
|
const res = await fetch(`${PDFD_URL}/pdf`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ html, filename }),
|
|
signal: AbortSignal.timeout(20000),
|
|
});
|
|
if (!res.ok) {
|
|
console.error(`[resend] pdfd returned ${res.status} — sending without attachment`);
|
|
return null;
|
|
}
|
|
const data = (await res.json()) as { pdf: string };
|
|
return Buffer.from(data.pdf, "base64");
|
|
} catch (e) {
|
|
console.error("[resend] PDF render failed — sending without attachment:", e instanceof Error ? e.message : e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function fetchSignatureBase64(appt: YsterbaardDirectorResignation): Promise<string | null> {
|
|
if (!appt.signature) return null;
|
|
const url = `${PB_URL}/api/files/${appt.collectionId}/${appt.id}/${appt.signature}`;
|
|
const res = await fetch(url, { headers: PB_TOKEN ? { Authorization: PB_TOKEN } : {} });
|
|
if (!res.ok) return null;
|
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
return buf.toString('base64');
|
|
}
|
|
|
|
function buildInitialEml(opts: {
|
|
to: string; directorName: string; companyName: string; signUrl: string;
|
|
pdfBytes: Buffer | null; pdfFilename: string;
|
|
}): string {
|
|
const boundary = "----YSTERBAARD_" + Date.now();
|
|
let eml = `To: ${opts.to}\r\nFrom: SJ@Dyfins.com\r\nSubject: Letter of Resignation — ${opts.companyName}\r\nMIME-Version: 1.0\r\n`;
|
|
eml += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n`;
|
|
eml += `--${boundary}\r\nContent-Type: text/html; charset="utf-8"\r\nContent-Transfer-Encoding: 7bit\r\n\r\n`;
|
|
eml += initialEmailBodyHtml(opts.directorName, opts.companyName, opts.signUrl);
|
|
// The PDF service is optional — fall back to a text/plain-only message so
|
|
// the letter still reaches the director when the renderer is down.
|
|
if (opts.pdfBytes && opts.pdfFilename) {
|
|
eml += `\r\n--${boundary}\r\nContent-Type: application/pdf; name="${opts.pdfFilename}"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename="${opts.pdfFilename}"\r\n\r\n`;
|
|
eml += opts.pdfBytes.toString("base64").replace(/(.{76})/g, "$1\r\n");
|
|
}
|
|
eml += `\r\n--${boundary}--\r\n`;
|
|
return eml;
|
|
}
|
|
|
|
function initialEmailBodyHtml(directorName: string, companyName: string, signUrl: string): string {
|
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"></head>
|
|
<body style="font-family:Arial,sans-serif;font-size:14px;color:#222;max-width:600px;margin:40px auto;padding:0 20px;">
|
|
<div style="background:#f8f9fa;border:1px solid #ddd;border-radius:8px;padding:24px;">
|
|
<p>Dear ${directorName},</p>
|
|
<p>Please find attached your <strong>Letter of Resignation</strong> for <strong>${companyName}</strong>.</p>
|
|
<div style="background:#fff;border:1px solid #eee;border-radius:6px;padding:20px;margin:20px 0;">
|
|
<p><strong>To sign electronically, click the button below and draw your signature:</strong></p>
|
|
<p style="text-align:center;margin:24px 0;">
|
|
<a href="${signUrl}" style="display:inline-block;background:#0066cc;color:#fff;padding:12px 24px;border-radius:6px;text-decoration:none;font-size:14px;font-weight:bold;">Sign Resignation</a>
|
|
</p>
|
|
<p>Or copy this link:<br/><span style="color:#0066cc;font-size:12px;word-break:break-all;">${signUrl}</span></p>
|
|
</div>
|
|
<p>Yours sincerely,<br/><strong>${directorName}</strong></p>
|
|
</div></body></html>`;
|
|
}
|
|
|
|
function buildSignedEml(opts: {
|
|
to: string; directorName: string; companyName: string; date: string;
|
|
pdfBytes: Buffer | null; pdfFilename: string;
|
|
}): string {
|
|
const boundary = "----YSTERBAARD_" + Date.now();
|
|
let eml = `To: ${opts.to}\r\nFrom: SJ@Dyfins.com\r\nSubject: ${opts.directorName} signed resignation from ${opts.companyName}\r\nMIME-Version: 1.0\r\n`;
|
|
eml += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n\r\n`;
|
|
eml += `--${boundary}\r\nContent-Type: text/html; charset="utf-8"\r\nContent-Transfer-Encoding: 7bit\r\n\r\n`;
|
|
eml += signedEmailBodyHtml(opts.directorName, opts.companyName, opts.date);
|
|
if (opts.pdfBytes && opts.pdfFilename) {
|
|
eml += `\r\n--${boundary}\r\nContent-Type: application/pdf; name="${opts.pdfFilename}"\r\nContent-Transfer-Encoding: base64\r\nContent-Disposition: attachment; filename="${opts.pdfFilename}"\r\n\r\n`;
|
|
eml += opts.pdfBytes.toString("base64").replace(/(.{76})/g, "$1\r\n");
|
|
}
|
|
eml += `\r\n--${boundary}--\r\n`;
|
|
return eml;
|
|
}
|
|
|
|
function signedEmailBodyHtml(directorName: string, companyName: string, date: string): string {
|
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"></head>
|
|
<body style="font-family:Arial,sans-serif;font-size:14px;color:#222;max-width:600px;margin:40px auto;padding:0 20px;">
|
|
<div style="background:#f8f9fa;border:1px solid #ddd;border-radius:8px;padding:24px;">
|
|
<h2 style="text-align:center;color:#0a0a0a;font-size:18px;margin:0 0 16px;">Letter of Resignation — Signed</h2>
|
|
<p>This is to confirm that <strong>${directorName}</strong> has signed the Letter of Resignation from <strong>${companyName}</strong>.</p>
|
|
<p>Date: ${date}</p>
|
|
<p style="color:#888;font-style:italic;">[Signature on file — see attached signed letter]</p>
|
|
<p>The full signed letter is attached as a PDF.</p>
|
|
</div></body></html>`;
|
|
}
|
|
|
|
async function sendViaHimalaya(eml: string): Promise<void> {
|
|
return new Promise<void>((resolve, reject) => {
|
|
const child = spawn(
|
|
HIMALAYA_BIN,
|
|
himalayaSendArgs("dyfins"),
|
|
{ stdio: ["pipe", "pipe", "pipe"] }
|
|
);
|
|
let err = "";
|
|
child.stderr.on("data", (d) => { err += d.toString(); });
|
|
child.on("error", (e) => reject(e));
|
|
child.on("close", (code) => {
|
|
if (code !== 0) reject(new Error(`himalaya exited ${code}: ${err}`));
|
|
else resolve();
|
|
});
|
|
child.stdin.write(eml);
|
|
child.stdin.end();
|
|
});
|
|
}
|
|
|
|
function letterHtml(opts: {
|
|
directorName: string;
|
|
directorIdNo: string;
|
|
directorEmail: string;
|
|
directorCellPhone: string;
|
|
residentialAddressLine1: string;
|
|
residentialAddressLine2: string;
|
|
residentialTownCity: string;
|
|
residentialProvince: string;
|
|
residentialPostalCode: string;
|
|
companyName: string;
|
|
physicalAddressLine1: string;
|
|
physicalAddressLine2: string;
|
|
physicalTownCity: string;
|
|
physicalProvince: string;
|
|
physicalPostalCode: string;
|
|
signUrl: string;
|
|
letterDate: string;
|
|
}): string {
|
|
const esc = (s: string) => String(s ?? "")
|
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
.replace(/"/g, """).replace(/'/g, "'");
|
|
const dir = {
|
|
name: esc(opts.directorName), id: esc(opts.directorIdNo),
|
|
email: esc(opts.directorEmail), cell: esc(opts.directorCellPhone),
|
|
addr1: esc(opts.residentialAddressLine1), addr2: esc(opts.residentialAddressLine2),
|
|
town: esc(opts.residentialTownCity), province: esc(opts.residentialProvince),
|
|
zip: esc(opts.residentialPostalCode),
|
|
};
|
|
const co = {
|
|
name: esc(opts.companyName),
|
|
p1: esc(opts.physicalAddressLine1), p2: esc(opts.physicalAddressLine2),
|
|
pt: esc(opts.physicalTownCity), pp: esc(opts.physicalProvince), pz: esc(opts.physicalPostalCode),
|
|
};
|
|
const d = new Date(opts.letterDate);
|
|
const dayOfMonth = d.getUTCDate();
|
|
const monthNames = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
const monthName = monthNames[d.getUTCMonth()];
|
|
const year = d.getUTCFullYear();
|
|
|
|
return `<!DOCTYPE html>
|
|
<html><head><meta charset="utf-8"><title>Letter of Resignation</title>
|
|
<style>
|
|
@page { size: A4; margin: 25mm; }
|
|
body { font-family: Arial, sans-serif; font-size: 11pt; color: #000; line-height: 1.2; margin: 0; }
|
|
.top-block { margin: 0; padding: 0; }
|
|
.addressee-block { margin: 0; padding: 0; line-height: 1.4; text-align: left; }
|
|
.hdr { font-weight: bold; margin: 14pt 0 4pt 0; }
|
|
p { margin: 6pt 0; text-align: justify; }
|
|
.sign-line { border-top: 1px solid #000; padding-top: 4pt; width: 60%; margin-top: 4pt; }
|
|
.sign-link-box { margin: 12pt 0; padding: 8pt; background: #f4f4f4; border: 1px solid #ccc; font-family: monospace; font-size: 9pt; text-align: left; word-break: break-all; }
|
|
.footer { margin-top: 24pt; font-size: 9pt; color: #555; text-align: center; font-style: italic; }
|
|
.signature-block { text-align: left; margin-top: 28pt; }
|
|
.signature-name { margin-top: 4pt; }
|
|
table { border-collapse: collapse; }
|
|
</style></head>
|
|
<body>
|
|
|
|
<table class="top-block" cellspacing="0" cellpadding="0" border="0" width="100%">
|
|
<tr>
|
|
<td style="text-align:left;width:35%;vertical-align:top;"> </td>
|
|
<td style="text-align:right;width:65%;vertical-align:top;font-weight:bold;">${dir.name}<br/>
|
|
<span style="font-weight:normal;">${dir.addr1}${dir.addr2 ? `<br/>${dir.addr2}` : ``}<br/>
|
|
${[dir.town, dir.zip].filter(Boolean).join(", ")}${dir.province ? `<br/>${dir.province}` : ``}<br/>
|
|
${dir.email}<br/>
|
|
${dir.cell}<br/>
|
|
<br/>
|
|
<span style="font-weight:normal;">${opts.letterDate}</span></span>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div class="addressee-block"><strong>The Board</strong><br/>
|
|
${co.name}<br/>
|
|
${co.p1 ? `${co.p1}<br/>` : ``}${co.p2 ? `${co.p2}<br/>` : ``}
|
|
${co.pt ? `${co.pt}<br/>` : ``}
|
|
${co.pp ? `${co.pp}<br/>` : ``}
|
|
${co.pz ? `${co.pz}<br/>` : ``}
|
|
</div>
|
|
|
|
<div class="hdr">Dear Members of The Board</div>
|
|
|
|
<p>Please accept this letter as formal notification that I am resigning from my position as Director of the Company, effective immediately, in accordance with the provisions of the Companies Act 71 of 2008 (as amended).</p>
|
|
|
|
<p>I acknowledge that I have no claim for compensation for loss of office or otherwise against the Company, its officers, or its employees. I further acknowledge that, from this date forward, I am no longer bound by the duties, responsibilities, and liabilities of this office as defined by the Act, common law, and any other applicable legislation.</p>
|
|
|
|
<p>Please confirm receipt of this letter and the effective date of my resignation.</p>
|
|
|
|
<p>Signed at ${dir.town || "_____________"} on this ${dayOfMonth} day of ${monthName} ${year}.</p>
|
|
|
|
<div class="signature-block">
|
|
<div class="sign-line"> </div>
|
|
<p class="signature-name">
|
|
<strong>${dir.name}</strong><br/>
|
|
ID ${dir.id}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="sign-link-box"><strong>To sign electronically, visit:</strong><br/>${opts.signUrl}</div>
|
|
|
|
<p>Alternatively, you may print this letter, sign in the space provided above, and return the signed copy to the Company Secretary at the company’s registered address.</p>
|
|
|
|
<div class="footer">
|
|
Issued in terms of section 71 of the Companies Act 71 of 2008
|
|
</div>
|
|
|
|
</body></html>`;
|
|
}
|
|
|
|
function signedLetterHtml(
|
|
director: Record<string, unknown>,
|
|
company: Record<string, unknown>,
|
|
date: string,
|
|
signatureBase64: string,
|
|
signUrl: string
|
|
): string {
|
|
const esc = (s: string) => String(s ?? "")
|
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
.replace(/"/g, """).replace(/'/g, "'");
|
|
const dir = {
|
|
name: esc([director.first_names, director.surname].filter(Boolean).join(" ")),
|
|
id: esc(String(director.id_no ?? "")),
|
|
addr1: esc(String(director.residential_address_line_1 ?? "")),
|
|
addr2: esc(String(director.residential_address_line_2 ?? "")),
|
|
town: esc(String(director.residential_town_city ?? "")),
|
|
province: esc(String(director.residential_province ?? "")),
|
|
zip: esc(String(director.residential_postal_code ?? "")),
|
|
};
|
|
const co = {
|
|
name: esc(String(company.enterprise_name ?? "")),
|
|
p1: esc(String(company.physical_address_line_1 ?? "")),
|
|
p2: esc(String(company.physical_address_line_2 ?? "")),
|
|
pt: esc(String(company.physical_town_city ?? "")),
|
|
pp: esc(String(company.physical_province ?? "")),
|
|
pz: esc(String(company.physical_postal_code ?? "")),
|
|
};
|
|
const d = new Date(date);
|
|
const dayOfMonth = d.getUTCDate();
|
|
const monthNames = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
const monthName = monthNames[d.getUTCMonth()];
|
|
const year = d.getUTCFullYear();
|
|
const hasSig = !!signatureBase64;
|
|
|
|
return `<!DOCTYPE html>
|
|
<html><head><meta charset="utf-8"><title>Letter of Resignation</title>
|
|
<style>
|
|
@page { size: A4; margin: 25mm; }
|
|
body { font-family: Arial, sans-serif; font-size: 11pt; color: #000; line-height: 1.2; margin: 0; }
|
|
.top-block { margin: 0; padding: 0; }
|
|
.addressee-block { margin: 0; padding: 0; line-height: 1.4; text-align: left; }
|
|
.hdr { font-weight: bold; margin: 14pt 0 4pt 0; }
|
|
p { margin: 6pt 0; text-align: justify; }
|
|
.sign-line { border-top: 1px solid #000; padding-top: 4pt; width: 60%; margin-top: 4pt; }
|
|
.sig-img { height: 60pt; display: block; margin: 0; }
|
|
.footer { margin-top: 24pt; font-size: 9pt; color: #555; text-align: center; font-style: italic; }
|
|
.signature-block { text-align: left; margin-top: 28pt; }
|
|
.signature-name { margin-top: 4pt; }
|
|
table { border-collapse: collapse; }
|
|
</style></head>
|
|
<body>
|
|
|
|
<table class="top-block" cellspacing="0" cellpadding="0" border="0" width="100%">
|
|
<tr>
|
|
<td style="text-align:left;width:35%;vertical-align:top;"> </td>
|
|
<td style="text-align:right;width:65%;vertical-align:top;"><strong>${dir.name}</strong><br/>
|
|
${dir.addr1 ? `${dir.addr1}<br/>` : ``}
|
|
${dir.addr2 ? `${dir.addr2}<br/>` : ``}
|
|
${dir.town ? `${dir.town}<br/>` : ``}
|
|
${dir.province ? `${dir.province}<br/>` : ``}
|
|
${dir.zip ? `${dir.zip}<br/>` : ``}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<div class="addressee-block"><strong>The Board</strong><br/>
|
|
${co.name}<br/>
|
|
${co.p1 ? `${co.p1}<br/>` : ``}${co.p2 ? `${co.p2}<br/>` : ``}
|
|
${co.pt ? `${co.pt}<br/>` : ``}
|
|
${co.pp ? `${co.pp}<br/>` : ``}
|
|
${co.pz ? `${co.pz}<br/>` : ``}
|
|
</div>
|
|
|
|
<div class="hdr">Dear Members of The Board</div>
|
|
|
|
<p>Please accept this letter as formal notification that I am resigning from my position as Director of the Company, effective immediately, in accordance with the provisions of the Companies Act 71 of 2008 (as amended).</p>
|
|
|
|
<p>I acknowledge that I have no claim for compensation for loss of office or otherwise against the Company, its officers, or its employees. I further acknowledge that, from this date forward, I am no longer bound by the duties, responsibilities, and liabilities of this office as defined by the Act, common law, and any other applicable legislation.</p>
|
|
|
|
<p>Please confirm receipt of this letter and the effective date of my resignation.</p>
|
|
|
|
<p>Signed at ${dir.town || "_____________"} on this ${dayOfMonth} day of ${monthName} ${year}.</p>
|
|
|
|
<div class="signature-block">
|
|
${hasSig
|
|
? `<img class="sig-img" src="data:image/png;base64,${signatureBase64}" alt="Signature"/>`
|
|
: `<div class="sign-line"> </div>`}
|
|
<div class="sign-line"> </div>
|
|
<p class="signature-name">
|
|
<strong>${dir.name}</strong><br/>
|
|
ID ${dir.id}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
Filed in terms of section 71 of the Companies Act 71 of 2008
|
|
</div>
|
|
|
|
</body></html>`;
|
|
}
|