Uncommitted work in progress at the point of starting the Kanban project. Captures: - Registration wizard refactor + multi-step routing - structured physical/postal address fields on companies and directors - appointments flow (sign + verify) - document type manager + upload/panel for companies and directors - status actions, approve button, action/dropdown buttons - step2 review page tsconfig.tsbuildinfo is now ignored (build artefact).
197 lines
7.1 KiB
TypeScript
197 lines
7.1 KiB
TypeScript
// lib/pb.ts
|
|
// SERVER-ONLY PocketBase client.
|
|
import "server-only";
|
|
|
|
export const PB_URL = process.env.PB_URL;
|
|
export const BASE_URL = PB_URL;
|
|
export const PB_TOKEN = process.env.PB_TOKEN;
|
|
const TOKEN = PB_TOKEN;
|
|
|
|
if (!BASE_URL) throw new Error("PB_URL is not set.");
|
|
if (!TOKEN || TOKEN === "PASTE_SUPERUSER_JWT_HERE") throw new Error("PB_TOKEN missing");
|
|
|
|
export interface PBList<T> { page: number; perPage: number; totalItems: number; totalPages: number; items: T[]; }
|
|
export interface PBRecord { id: string; collectionId: string; collectionName: string; [key: string]: unknown; }
|
|
|
|
function buildUrl(path: string, query: Record<string, string | number | undefined> = {}): string {
|
|
const params = new URLSearchParams();
|
|
for (const [k, v] of Object.entries(query)) {
|
|
if (v !== undefined && v !== null && v !== "") params.set(k, String(v));
|
|
}
|
|
const qs = params.toString();
|
|
return `${BASE_URL}${path}${qs ? `?${qs}` : ""}`;
|
|
}
|
|
|
|
export async function pbFetch<T>(path: string, query: Record<string, string | number | undefined> = {}): Promise<T> {
|
|
const url = buildUrl(path, query);
|
|
const res = await fetch(url, {
|
|
headers: { Authorization: TOKEN as string },
|
|
cache: "no-store",
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on ${path}: ${body.slice(0, 200)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export async function createRecordMultipart<T extends PBRecord>(collection: string, fields: Record<string, string>, files: { field: string; file: File }[] = []): Promise<T> {
|
|
const form = new FormData();
|
|
for (const [k, v] of Object.entries(fields)) form.set(k, v);
|
|
for (const { field, file } of files) form.append(field, file, file.name);
|
|
const url = `${BASE_URL}/api/collections/${collection}/records`;
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { Authorization: TOKEN as string },
|
|
body: form,
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on POST ${collection}: ${body.slice(0, 300)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export async function updateRecord<T extends PBRecord>(collection: string, id: string, fields: Record<string, unknown>): Promise<T> {
|
|
const url = `${BASE_URL}/api/collections/${collection}/records/${encodeURIComponent(id)}`;
|
|
const res = await fetch(url, {
|
|
method: "PATCH",
|
|
headers: { Authorization: TOKEN as string, "Content-Type": "application/json" },
|
|
body: JSON.stringify(fields),
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on PATCH ${collection}/${id}: ${body.slice(0, 300)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export async function fetchCollection<T extends PBRecord>(collection: string, params: { page?: number; perPage?: number; sort?: string; filter?: string; expand?: string; fields?: string } = {}): Promise<PBList<T>> {
|
|
return pbFetch<PBList<T>>(`/api/collections/${collection}/records`, {
|
|
page: params.page ?? 1,
|
|
perPage: params.perPage ?? 25,
|
|
sort: params.sort,
|
|
filter: params.filter,
|
|
expand: params.expand,
|
|
fields: params.fields,
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* JSON create — for collections without file fields.
|
|
* Explicit IDs are preserved (some collections have required system-id fields
|
|
* with no auto-generate pattern).
|
|
*/
|
|
export async function createRecord<T extends PBRecord>(
|
|
collection: string,
|
|
fields: Record<string, unknown>,
|
|
): Promise<T> {
|
|
const url = `${BASE_URL}/api/collections/${collection}/records`;
|
|
const res = await fetch(url, {
|
|
method: "POST",
|
|
headers: { Authorization: TOKEN as string, "Content-Type": "application/json" },
|
|
body: JSON.stringify(fields),
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on POST ${collection}: ${body.slice(0, 300)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export async function fetchRecord<T = unknown>(collection: string, id: string, expand?: string): Promise<T> {
|
|
return pbFetch<T>(`/api/collections/${collection}/records/${encodeURIComponent(id)}`, { expand });
|
|
}
|
|
|
|
/**
|
|
* Junction table helpers — ysterbaard_company_directors (pbc_2497518006)
|
|
*/
|
|
|
|
export async function fetchJunctionRecords(params: {
|
|
filter?: string;
|
|
expand?: string;
|
|
perPage?: number;
|
|
sort?: string;
|
|
}): Promise<PBList<PBRecord>> {
|
|
return pbFetch<PBList<PBRecord>>("/api/collections/pbc_2497518006/records", {
|
|
page: 1,
|
|
perPage: params.perPage ?? 500,
|
|
sort: params.sort,
|
|
filter: params.filter,
|
|
expand: params.expand,
|
|
});
|
|
}
|
|
|
|
/** All directors linked to a company, via the junction table */
|
|
export async function fetchDirectorsForCompany(companyId: string): Promise<PBRecord[]> {
|
|
const safe = companyId.replace(/'/g, "''");
|
|
const list = await fetchJunctionRecords({
|
|
filter: `company = '${safe}'`,
|
|
expand: "director",
|
|
perPage: 500,
|
|
});
|
|
return list.items.filter((j) => (j.expand as Record<string, unknown>)?.director);
|
|
}
|
|
|
|
/** All companies linked to a director, via the junction table — returns junction records */
|
|
export async function fetchJunctionEntriesForDirector(directorId: string): Promise<PBRecord[]> {
|
|
const safe = directorId.replace(/'/g, "''");
|
|
const list = await fetchJunctionRecords({
|
|
filter: `director = '${safe}'`,
|
|
expand: "company",
|
|
perPage: 500,
|
|
});
|
|
return list.items.filter((j) => (j.expand as Record<string, unknown>)?.company);
|
|
}
|
|
|
|
/** All directors linked to a company, via the junction table — returns junction records */
|
|
export async function fetchJunctionEntriesForCompany(companyId: string): Promise<PBRecord[]> {
|
|
const safe = companyId.replace(/'/g, "''");
|
|
const list = await fetchJunctionRecords({
|
|
filter: `company = '${safe}'`,
|
|
expand: "director",
|
|
perPage: 500,
|
|
});
|
|
return list.items.filter((j) => (j.expand as Record<string, unknown>)?.director);
|
|
}
|
|
|
|
export async function deleteRecord(collection: string, id: string): Promise<void> {
|
|
const url = `${BASE_URL}/api/collections/${collection}/records/${encodeURIComponent(id)}`;
|
|
const res = await fetch(url, {
|
|
method: "DELETE",
|
|
headers: { Authorization: TOKEN as string },
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok && res.status !== 204) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on DELETE ${collection}/${id}: ${body.slice(0, 300)}`);
|
|
}
|
|
}
|
|
|
|
export async function updateRecordMultipart<T extends PBRecord>(
|
|
collection: string,
|
|
id: string,
|
|
fields: Record<string, string>,
|
|
files: { field: string; file: File }[] = []
|
|
): Promise<T> {
|
|
const form = new FormData();
|
|
for (const [k, v] of Object.entries(fields)) form.set(k, v);
|
|
for (const { field, file } of files) form.append(field, file, file.name);
|
|
const url = `${BASE_URL}/api/collections/${collection}/records/${encodeURIComponent(id)}`;
|
|
const res = await fetch(url, {
|
|
method: "PATCH",
|
|
headers: { Authorization: TOKEN as string },
|
|
body: form,
|
|
cache: "no-store",
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PocketBase ${res.status} on PATCH ${collection}/${id}: ${body.slice(0, 300)}`);
|
|
}
|
|
return (await res.json()) as T;
|
|
}
|