// lib/resolution-documents.ts // // Collects EVERY document that belongs to a resolution — not just the one file // slot on the resolution record itself. // // Why this exists: the paperwork for one resolution is spread across several // PocketBase records. The resolution carries its own copy, but each director's // resignation/appointment lives on its OWN record in // `ysterbaard_director_resignations` / `ysterbaard_director_appointments`, with // its own `letter` and `signature` file fields. // // "View signed document" used to read only `signed_letter` off the resolution, // so the operator saw one document and concluded the rest had gone missing. // // This module is the single source of truth for that list; both the // /documents page and any API consumer read from here. import { pbFetch, type PBList, type PBRecord } from "./pb"; export const RESOLUTIONS = "pbc_266170881"; export const RESIGNATIONS = "ysterbaard_director_resignations"; export const APPOINTMENTS = "ysterbaard_director_appointments"; export interface ResolutionDocument { /** Stable key for React lists. */ key: string; /** Human label, e.g. "Sune Pieters — signed resignation". */ label: string; /** Which group it belongs to, for section headings. */ group: "Resolution" | "Resignation" | "Appointment"; /** PocketBase collection KEY as the /document route expects it. */ collection: string; /** Record id inside that collection. */ recordId: string; /** File field name. */ field: string; /** Stored filename. */ filename: string; /** App URL that streams this file inline. */ href: string; } interface DirectorLike { id: string; first_names?: string; surname?: string; } function directorName(d: unknown): string { if (!d || typeof d !== "object") return ""; const x = d as DirectorLike; return [x.first_names, x.surname].filter(Boolean).join(" ").trim(); } /** Build the app URL that streams one stored file inline. */ function hrefFor(collection: string, recordId: string, field: string): string { const q = new URLSearchParams({ collection, field, download: "false" }); return `/api/resolutions/${encodeURIComponent(recordId)}/document?${q.toString()}`; } function pushFile( out: ResolutionDocument[], opts: { group: ResolutionDocument["group"]; label: string; collection: string; recordId: string; field: string; value: unknown; } ) { const filename = typeof opts.value === "string" ? opts.value : ""; if (!filename) return; // field is empty — nothing to show out.push({ key: `${opts.collection}:${opts.recordId}:${opts.field}`, label: opts.label, group: opts.group, collection: opts.collection, recordId: opts.recordId, field: opts.field, filename, href: hrefFor(opts.collection, opts.recordId, opts.field), }); } function asArray(v: unknown): string[] { return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []; } /** * Every document tied to a resolution. * * Matching rules for the per-director records: * - same company, AND * - the director appears in the resolution's resigned_directors * (for resignations) or appointed_directors (for appointments). * * A resignation is also matched when the director is merely named on the * resolution: the operator may have attached paperwork without ticking the * right box, and losing sight of an uploaded document is worse than showing one * that turns out to be unrelated. */ export async function collectResolutionDocuments( resolutionId: string ): Promise<{ resolution: PBRecord; documents: ResolutionDocument[] }> { const resolution = await pbFetch( `/api/collections/${RESOLUTIONS}/records/${encodeURIComponent(resolutionId)}` ); const docs: ResolutionDocument[] = []; // 1. The resolution's own files. pushFile(docs, { group: "Resolution", label: "Signed resolution", collection: "ysterbaard_resolutions", recordId: resolution.id, field: "signed_letter", value: resolution.signed_letter, }); pushFile(docs, { group: "Resolution", label: "Signed share certificates", collection: "ysterbaard_resolutions", recordId: resolution.id, field: "signed_certificates", value: resolution.signed_certificates, }); pushFile(docs, { group: "Resolution", label: "Generated resolution letter", collection: "ysterbaard_resolutions", recordId: resolution.id, field: "generated_letter", value: resolution.generated_letter, }); const resignedIds = asArray(resolution.resigned_directors); const appointedIds = asArray(resolution.appointed_directors); const namedIds = Array.from(new Set([...resignedIds, ...appointedIds])); const company = typeof resolution.company === "string" ? resolution.company : ""; if (company && namedIds.length > 0) { // 2. Resignations for any named director at the same company. // NOTE: PB filter fields are NOT collection-qualified. Writing // `ysterbaard_director_resignations.company = "..."` returns HTTP 400 — the // filter is already scoped to the collection being queried. const resignations = await pbFetch>( `/api/collections/${RESIGNATIONS}/records`, { perPage: 200, filter: `company = "${company}"`, expand: "director", } ); for (const r of resignations.items ?? []) { const dirId = typeof r.director === "string" ? r.director : ""; if (dirId && !namedIds.includes(dirId)) continue; const who = directorName((r.expand as Record | undefined)?.director) || "Director"; pushFile(docs, { group: "Resignation", label: `${who} — resignation letter`, collection: "ysterbaard_director_resignations", recordId: r.id, field: "letter", value: r.letter, }); pushFile(docs, { group: "Resignation", label: `${who} — signed resignation`, collection: "ysterbaard_director_resignations", recordId: r.id, field: "signature", value: r.signature, }); } // 3. Appointments for any named director at the same company. const appointments = await pbFetch>( `/api/collections/${APPOINTMENTS}/records`, { perPage: 200, filter: `company = "${company}"`, expand: "director", } ); for (const a of appointments.items ?? []) { const dirId = typeof a.director === "string" ? a.director : ""; if (dirId && !namedIds.includes(dirId)) continue; const who = directorName((a.expand as Record | undefined)?.director) || "Director"; pushFile(docs, { group: "Appointment", label: `${who} — appointment letter`, collection: "ysterbaard_director_appointments", recordId: a.id, field: "letter", value: a.letter, }); pushFile(docs, { group: "Appointment", label: `${who} — signed appointment`, collection: "ysterbaard_director_appointments", recordId: a.id, field: "signature", value: a.signature, }); } } return { resolution, documents: docs }; }