WORKFLOW-APP/lib/api.ts

244 lines
8.2 KiB
TypeScript
Raw Permalink Normal View History

// 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 } 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<T>(params: ClientListParams): Promise<PBList<T>> {
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<T>(params: ClientGetParams): Promise<T> {
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<string, string | number | string[]>;
}): Promise<unknown> {
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<string, string>;
}): Promise<unknown> {
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<string, string | number | string[]>;
}): Promise<unknown> {
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<void> {
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<PBList<DocType>> {
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<DocType> {
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<DocType> {
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<void> {
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<unknown> {
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<unknown> {
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<void> {
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<void> {
const res = await fetch(`/api/documents/company/${id}`, { method: "DELETE" });
if (!res.ok) throw new Error(`Delete failed (${res.status})`);
}