// lib/api.ts // Browser-safe thin client that calls our own /api/pb proxy. // This module is "use client" friendly — it never imports PocketBase // directly and never touches process.env.PB_TOKEN. import type { PBList, ListParams, SarsSubmissionReturnType, YsterbaardSarsSubmissionFlag } from "./types"; export interface ClientListParams extends ListParams { collection: string; } export interface ClientGetParams { collection: string; id: string; expand?: string; } /** Type-safe client fetcher for paginated lists. */ export async function pbList(params: ClientListParams): Promise> { const qs = new URLSearchParams(); qs.set("collection", params.collection); if (params.page) qs.set("page", String(params.page)); if (params.perPage) qs.set("perPage", String(params.perPage)); if (params.sort) qs.set("sort", params.sort); if (params.filter) qs.set("filter", params.filter); if (params.expand) qs.set("expand", params.expand); if (params.fields) qs.set("fields", params.fields); const res = await fetch(`/api/pb/records/list?${qs.toString()}`, { cache: "no-store" }); if (!res.ok) { const text = await res.text(); throw new Error(`PB list failed (${res.status}): ${text || res.statusText}`); } return res.json(); } export async function pbGet(params: ClientGetParams): Promise { const qs = new URLSearchParams(); qs.set("collection", params.collection); if (params.expand) qs.set("expand", params.expand); const res = await fetch(`/api/pb/records/${params.id}?${qs.toString()}`, { cache: "no-store" }); if (!res.ok) { const text = await res.text(); throw new Error(`PB get failed (${res.status}): ${text || res.statusText}`); } return res.json(); } /** * Single-purpose toggle for `is_client` on a director or company row. * Server enforces a hard allowlist; this client only sends * { collection, id, is_client }, nothing else. * * Used by the directors/companies table rows and the detail-page toggle. */ export async function pbToggleClient(params: { collection: "ysterbaard_directors" | "ysterbaard_companies"; id: string; is_client: boolean; }): Promise<{ ok: true; id: string; is_client: boolean; updated: string }> { const res = await fetch("/api/pb/records/toggle-client", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ collection: params.collection, id: params.id, is_client: params.is_client }) }); if (!res.ok) { const text = await res.text(); throw new Error(`PB toggle failed (${res.status}): ${text || res.statusText}`); } return res.json(); } /** * Creates a new record on any collection. * Only allowlisted fields pass through. */ export async function pbCreate(params: { collection: string; fields: Record; }): Promise { const res = await fetch("/api/pb/records/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), }); if (!res.ok) { const text = await res.text(); throw new Error(`PB create failed (${res.status}): ${text || res.statusText}`); } return res.json(); } /** * Creates a new director or company record with is_client=true. * Only the allowlisted fields pass through — no arbitrary PB writes. * * Used by the Add Director / Add Company modal forms. */ export async function pbCreateClient(params: { collection: "ysterbaard_directors" | "ysterbaard_companies"; fields: Record; }): Promise { const res = await fetch("/api/pb/records/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), }); if (!res.ok) { const text = await res.text(); throw new Error(`PB create failed (${res.status}): ${text || res.statusText}`); } return res.json(); } /** * Updates editable fields on an existing director or company record. * Only allowlisted fields pass through. Use for the in-app edit form. */ export async function pbPatch(params: { collection: string; id: string; fields: Record; }): Promise { const res = await fetch(`/api/pb/records/${params.id}?collection=${params.collection}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params.fields), }); if (!res.ok) { const text = await res.text(); throw new Error(`PB patch failed (${res.status}): ${text || res.statusText}`); } return res.json(); } /** * Deletes a director or company record permanently. */ export async function pbDelete(params: { collection: string; id: string; }): Promise { const res = await fetch(`/api/pb/records/${params.id}?collection=${params.collection}`, { method: "DELETE", }); if (!res.ok) { const text = await res.text(); throw new Error(`PB delete failed (${res.status}): ${text || res.statusText}`); } } // ───────────────────────────────────────────────────────────────────────── // Document management helpers // ───────────────────────────────────────────────────────────────────────── export interface DocType { id: string; name: string; context: "director" | "company"; description: string | null; } export async function fetchDocumentTypes(context?: "director" | "company"): Promise> { const qs = context ? `?context=${context}` : ""; const res = await fetch(`/api/documents/types${qs}`, { cache: "no-store" }); if (!res.ok) throw new Error(`Failed to load document types (${res.status})`); return res.json(); } export async function createDocumentType(data: { name: string; context: "director" | "company"; description?: string; }): Promise { const res = await fetch("/api/documents/types", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as { error?: string }).error ?? "Failed to create document type"); } return res.json(); } export async function updateDocumentType(id: string, data: { name?: string; description?: string }): Promise { const res = await fetch(`/api/documents/types/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as { error?: string }).error ?? "Failed to update document type"); } return res.json(); } export async function deleteDocumentType(id: string): Promise { const res = await fetch(`/api/documents/types/${id}`, { method: "DELETE" }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as { error?: string }).error ?? "Failed to delete document type"); } } export async function uploadDirectorDocument(formData: FormData): Promise { const res = await fetch("/api/documents/director/upload", { method: "POST", body: formData }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as { error?: string }).error ?? "Upload failed"); } return res.json(); } export async function uploadCompanyDocument(formData: FormData): Promise { const res = await fetch("/api/documents/company/upload", { method: "POST", body: formData }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error((err as { error?: string }).error ?? "Upload failed"); } return res.json(); } export async function deleteDirectorDocument(id: string): Promise { const res = await fetch(`/api/documents/director/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error(`Delete failed (${res.status})`); } export async function deleteCompanyDocument(id: string): Promise { const res = await fetch(`/api/documents/company/${id}`, { method: "DELETE" }); if (!res.ok) throw new Error(`Delete failed (${res.status})`); } // ───────────────────────────────────────────────────────────────────────── // SARS submission tick-boxes // ───────────────────────────────────────────────────────────────────────── export async function fetchSarsSubmissionFlags( returnType: SarsSubmissionReturnType, period: string ): Promise> { const qs = new URLSearchParams({ return_type: returnType, period }); const res = await fetch(`/api/sars-submission-flags?${qs.toString()}`, { cache: "no-store" }); if (!res.ok) { const text = await res.text(); throw new Error( `fetchSarsSubmissionFlags failed (${res.status}): ${text || res.statusText}` ); } return res.json(); } export async function upsertSarsSubmissionFlag(params: { company: string; return_type: SarsSubmissionReturnType; period: string; submitted: boolean; }): Promise { const res = await fetch("/api/sars-submission-flags", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params) }); if (!res.ok) { const text = await res.text(); throw new Error( `upsertSarsSubmissionFlag failed (${res.status}): ${text || res.statusText}` ); } return res.json(); } export async function clearSarsSubmissionFlags( returnType: SarsSubmissionReturnType, period: string ): Promise<{ ok: true; deleted: number }> { const qs = new URLSearchParams({ return_type: returnType, period }); const res = await fetch(`/api/sars-submission-flags?${qs.toString()}`, { method: "DELETE" }); if (!res.ok) { const text = await res.text(); throw new Error( `clearSarsSubmissionFlags failed (${res.status}): ${text || res.statusText}` ); } return res.json(); }