211 lines
7.1 KiB
TypeScript
211 lines
7.1 KiB
TypeScript
|
|
// 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<PBRecord>(
|
||
|
|
`/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<PBList<PBRecord>>(
|
||
|
|
`/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<string, unknown> | 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<PBList<PBRecord>>(
|
||
|
|
`/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<string, unknown> | 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 };
|
||
|
|
}
|