244 lines
8.2 KiB
TypeScript
244 lines
8.2 KiB
TypeScript
|
|
// 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 | boolean | 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})`);
|
||
|
|
}
|