WORKFLOW-APP/app/api/appointments/[id]/route.ts

165 lines
6.8 KiB
TypeScript

// app/api/appointments/[id]/route.ts
// GET — single appointment (by id)
// PATCH — update status / upload signature / mark mail_sent_to_all
import { NextRequest, NextResponse } from "next/server";
import { fetchRecord, updateRecord, updateRecordMultipart } from "@/lib/pb";
import type { YsterbaardDirectorAppointment } from "@/lib/types";
import { readFileSync, writeFileSync, unlinkSync } from "fs";
import { execSync, spawn } from "child_process";
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const record = await fetchRecord<YsterbaardDirectorAppointment>(
"ysterbaard_director_appointments",
id,
"director,company"
);
return NextResponse.json(record);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error: msg }, { status: 500 });
}
}
export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const body = await req.json();
// Signature upload — multipart
if (body.signature_base64) {
// Save signature to PB first (no letter required)
const buf = Buffer.from(body.signature_base64, "base64");
const sigFile = new File([buf], "signature.png", { type: "image/png" });
const record = await updateRecordMultipart<YsterbaardDirectorAppointment>(
"ysterbaard_director_appointments",
id,
{ status: "signature_received" },
[{ field: "signature", file: sigFile }]
);
// Distro email to all directors + company (signature image embedded inline)
try {
await sendSignedDistro({ id, signatureBase64: body.signature_base64 });
await updateRecord("ysterbaard_director_appointments", id, { status: "mail_sent_to_all" });
} catch (distroErr) {
console.error("Distro failed (signature saved):", distroErr);
// Don't fail the request — signature is saved, distro is a follow-up
}
return NextResponse.json(record);
}
// Simple status update
const updates: Record<string, unknown> = {};
if (body.status) updates.status = body.status;
if (body.letter_url) updates.letter = body.letter_url;
const record = await updateRecord<YsterbaardDirectorAppointment>(
"ysterbaard_director_appointments",
id,
updates
);
return NextResponse.json(record);
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : String(err);
return NextResponse.json({ error: msg }, { status: 500 });
}
}
// ─── Send signed letter to all directors + company ─────────────────────────
// ─── Send signed acceptance email (HTML with inline signature) ────────────
async function sendSignedDistro(opts: { id: string; signatureBase64: string }) {
const appt = await fetchRecord<YsterbaardDirectorAppointment>(
"ysterbaard_director_appointments",
opts.id,
"director,company"
);
const director = appt.expand?.director;
const company = appt.expand?.company;
const directorName = [director?.first_names, director?.surname].filter(Boolean).join(" ") || "The Director";
const companyName = company?.enterprise_name || "the Company";
const date = new Date().toISOString().split("T")[0];
// Build recipient list: all directors of this company + company email
const recipients: string[] = [];
if (director?.email) recipients.push(director.email);
const { fetchDirectorsForCompany } = await import("@/lib/pb");
const junctions = await fetchDirectorsForCompany(company!.id) as Array<{
expand?: { director?: { email?: string } };
}>;
for (const j of junctions) {
const em = j.expand?.director?.email;
if (em) recipients.push(em);
}
const companyEmail = (company as { company_email?: string } | undefined)?.company_email;
if (companyEmail) recipients.push(companyEmail);
const uniqueEmails = [...new Set(recipients.filter(Boolean))];
if (uniqueEmails.length === 0) {
console.error("sendSignedDistro: no recipients");
return;
}
for (const to of uniqueEmails) {
await sendSignedEmail({ to, directorName, companyName, date, signatureBase64: opts.signatureBase64 });
}
}
async function sendSignedEmail(opts: { to: string; directorName: string; companyName: string; date: string; signatureBase64: string }) {
const { to, directorName, companyName, date, signatureBase64 } = opts;
const subject = `${directorName} accepted the appointment as director of ${companyName}`;
const eml = `To: ${to}\r
From: SJ@Dyfins.com\r
Subject: ${subject}\r
MIME-Version: 1.0\r
Content-Type: text/html; charset="utf-8"\r
\r
<!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;">Director Acceptance Recorded</h2>
<p>I am glad to inform you that <strong>${directorName}</strong> accepted the appointment of <strong>${companyName}</strong>.</p>
<p>Date: ${date}</p>
<div style="background:#fff;border:1px solid #eee;border-radius:6px;padding:20px;margin:20px 0;">
<h3 style="font-size:14px;text-align:center;margin:0 0 12px;">LETTER OF APPOINTMENT AS DIRECTOR</h3>
<hr style="border:none;border-top:1px solid #333;margin:8px 0 16px;" />
<p>We are pleased to confirm that <strong>${directorName}</strong> has been appointed as a Director of <strong>${companyName}</strong>.</p>
<p>The appointment was accepted on ${date} and is hereby recorded in terms of section 70 of the Companies Act 71 of 2008.</p>
<div style="margin-top:24px;border-top:1px solid #333;padding-top:8px;width:260px;">
<p style="margin:0;"><strong>${directorName}</strong></p>
<p style="margin:0;color:#555;">Director</p>
<p style="margin-top:8px;"><img src="data:image/png;base64,${signatureBase64}" alt="Signature" style="height:60px;display:block;" /></p>
</div>
</div></div></body></html>\r
`;
const emlPath = `/tmp/signed-email-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.eml`;
writeFileSync(emlPath, eml, "utf8");
try {
const himalaya = spawn("/opt/homebrew/bin/himalaya", ["-a", "dyfins", "message", "send", "--save", "Sent"], { stdio: ["pipe", "pipe", "pipe"] });
const emlBuf = readFileSync(emlPath);
himalaya.stdin.write(emlBuf);
himalaya.stdin.end();
let err = "";
himalaya.stderr.on("data", (d) => { err += d.toString(); });
await new Promise<void>((res) => { himalaya.on("close", () => { res(); }); });
if (err.trim()) console.error("himalaya stderr:", err.trim());
} finally {
unlinkSync(emlPath);
}
}