- Modified the file handling in the repair details page to use the correct repair ID and simplified the key assignment for photo files. - Updated the API route to cast the status parameter to the new RepairStatus type for improved type safety. - Expanded the RepairFileField type to include "delivery_note" for better file management. - Removed unused helper functions to clean up the codebase and improve maintainability.
628 lines
21 KiB
TypeScript
628 lines
21 KiB
TypeScript
/**
|
||
* Server-side repairs service.
|
||
*
|
||
* Owns the write surface for the `repairs` collection (the legacy
|
||
* `pb-helpers.ts` only has the bare `getRepairs` / `createRepair`
|
||
* client-side helpers — they're fine for the list page, but every
|
||
* write that needs admin credentials + cross-collection writes
|
||
* belongs here).
|
||
*
|
||
* The repair lifecycle:
|
||
*
|
||
* new → assessing → quoted → approved → in_progress → completed
|
||
* └────► cancelled (from any pre-completed state)
|
||
*
|
||
* ‑ "new" : booked in, machine physically received
|
||
* ‑ "assessing" : office sent the machine to a supplier for analysis
|
||
* (a delivery-note PDF is on file under
|
||
* `repairs.delivery_note`)
|
||
* ‑ "quoted" : supplier returned a quote (uploaded to
|
||
* `repairs.quote`)
|
||
* ‑ "approved" : customer approved the supplier's quote (and the
|
||
* office cut a supplier PO — uploaded to
|
||
* `repairs.purchase_order`)
|
||
* ‑ "in_progress": supplier is working on the repair
|
||
* ‑ "completed" : machine is back with the customer
|
||
*
|
||
* Side-channel data lives in the file fields (`photos`, `quote`,
|
||
* `purchase_order`, `delivery_note`) — the service exposes thin
|
||
* `upload*` helpers that re-PATCH the row with the new file list,
|
||
* mirroring the multi-file pattern PocketBase uses everywhere else.
|
||
*/
|
||
|
||
import { getAdminPocketBase } from "./pocketbase-admin";
|
||
import { renderDeliveryNotePdf } from "@/lib/pdf/render";
|
||
import {
|
||
deliveryNoteLetterheadCode,
|
||
fetchLetterhead,
|
||
loadLetterheadLogoDataUri,
|
||
} from "@/lib/letterhead";
|
||
import type {
|
||
Customer,
|
||
Repair,
|
||
Supplier,
|
||
User,
|
||
} from "@/lib/types";
|
||
|
||
const REPAIRS = "repairs";
|
||
const CUSTOMERS = "customers";
|
||
const SUPPLIERS = "suppliers";
|
||
const USERS = "users";
|
||
const DELIVERY_NOTES = "delivery_notes";
|
||
const ORDER_ITEMS = "order_items";
|
||
|
||
export type RepairStatus =
|
||
| "new"
|
||
| "assessing"
|
||
| "quoted"
|
||
| "approved"
|
||
| "in_progress"
|
||
| "completed"
|
||
| "cancelled";
|
||
|
||
/** Allowed manual transitions. Centralised so the route + UI agree. */
|
||
const ALLOWED_TRANSITIONS: Record<RepairStatus, RepairStatus[]> = {
|
||
new: ["assessing", "quoted", "cancelled"],
|
||
assessing: ["quoted", "cancelled"],
|
||
quoted: ["approved", "in_progress", "cancelled"],
|
||
approved: ["in_progress", "cancelled"],
|
||
in_progress: ["completed", "cancelled"],
|
||
completed: [],
|
||
cancelled: [],
|
||
};
|
||
|
||
function rejectTransition(from: RepairStatus, to: RepairStatus): never {
|
||
const allowed = ALLOWED_TRANSITIONS[from] ?? [];
|
||
const list = allowed.length > 0 ? allowed.join(", ") : "none";
|
||
throw new Error(
|
||
`Cannot transition repair from "${from}" to "${to}" (allowed: ${list}).`,
|
||
);
|
||
}
|
||
|
||
function assertTransition(from: RepairStatus, to: RepairStatus): void {
|
||
if (!ALLOWED_TRANSITIONS[from].includes(to)) rejectTransition(from, to);
|
||
}
|
||
|
||
/* ============================================================================
|
||
* List + read
|
||
* ========================================================================== */
|
||
|
||
export interface ListRepairsOptions {
|
||
page?: number;
|
||
perPage?: number;
|
||
search?: string;
|
||
status?: RepairStatus | "";
|
||
sort?: string;
|
||
}
|
||
|
||
export interface ListRepairsResult {
|
||
items: Repair[];
|
||
totalItems: number;
|
||
totalPages: number;
|
||
page: number;
|
||
perPage: number;
|
||
}
|
||
|
||
/**
|
||
* Searchable columns on the `repairs` collection. Mirrors the
|
||
* legacy `getRepairs` filter in `pb-helpers.ts:1485` but expanded
|
||
* with the new fields (serial_no, notes) so the operator can find
|
||
* a repair by typing any of the human-readable bits. PB's `~`
|
||
* operator is case-insensitive substring.
|
||
*/
|
||
const SEARCHABLE_COLUMNS = [
|
||
"repair_number",
|
||
"item_description",
|
||
"fault_description",
|
||
"notes",
|
||
"serial_no",
|
||
];
|
||
|
||
function buildSearchFilter(search: string): string | null {
|
||
const trimmed = search.trim();
|
||
if (!trimmed) return null;
|
||
const escaped = trimmed.replace(/"/g, '\\"');
|
||
return SEARCHABLE_COLUMNS.map((c) => `${c} ~ "${escaped}"`).join(" || ");
|
||
}
|
||
|
||
export async function listRepairs(
|
||
options: ListRepairsOptions = {},
|
||
): Promise<ListRepairsResult> {
|
||
const pb = await getAdminPocketBase();
|
||
const page = options.page ?? 1;
|
||
const perPage = options.perPage ?? 20;
|
||
const sort = options.sort ?? "-created";
|
||
|
||
const clauses: string[] = [];
|
||
const searchClause = buildSearchFilter(options.search ?? "");
|
||
if (searchClause) clauses.push(searchClause);
|
||
if (options.status) {
|
||
clauses.push(`status = "${options.status}"`);
|
||
}
|
||
const filter = clauses.join(" && ");
|
||
|
||
const result = await pb.collection(REPAIRS).getList<Repair>(page, perPage, {
|
||
filter,
|
||
sort,
|
||
expand: "customer,company,order,supplier",
|
||
});
|
||
return {
|
||
items: result.items,
|
||
totalItems: result.totalItems,
|
||
totalPages: result.totalPages,
|
||
page: result.page,
|
||
perPage: result.perPage,
|
||
};
|
||
}
|
||
|
||
export async function getRepair(id: string): Promise<Repair> {
|
||
const pb = await getAdminPocketBase();
|
||
return pb.collection(REPAIRS).getOne<Repair>(id, {
|
||
expand: "customer,company,order,supplier",
|
||
});
|
||
}
|
||
|
||
/* ============================================================================
|
||
* Create / update
|
||
* ========================================================================== */
|
||
|
||
export interface CreateRepairInput {
|
||
customer?: string | null;
|
||
company?: string | null;
|
||
order?: string | null;
|
||
supplier?: string | null;
|
||
item_description: string;
|
||
fault_description: string;
|
||
notes?: string | null;
|
||
serial_no?: string | null;
|
||
photos?: string[];
|
||
}
|
||
|
||
export async function createRepair(
|
||
input: CreateRepairInput,
|
||
): Promise<Repair> {
|
||
const pb = await getAdminPocketBase();
|
||
|
||
if (!input.item_description?.trim()) {
|
||
throw new Error("item_description is required.");
|
||
}
|
||
if (!input.fault_description?.trim()) {
|
||
throw new Error("fault_description is required.");
|
||
}
|
||
|
||
const repairNumber = await generateRepairNumberServer();
|
||
|
||
const create: Record<string, unknown> = {
|
||
repair_number: repairNumber,
|
||
status: "new",
|
||
item_description: input.item_description.trim(),
|
||
fault_description: input.fault_description.trim(),
|
||
notes: input.notes ?? "",
|
||
customer: input.customer || null,
|
||
company: input.company || null,
|
||
order: input.order || null,
|
||
supplier: input.supplier || null,
|
||
serial_no: input.serial_no ?? "",
|
||
};
|
||
if (input.photos && input.photos.length > 0) {
|
||
create.photos = input.photos;
|
||
}
|
||
return pb.collection(REPAIRS).create<Repair>(create);
|
||
}
|
||
|
||
export interface UpdateRepairInput {
|
||
status?: RepairStatus;
|
||
notes?: string | null;
|
||
fault_description?: string;
|
||
item_description?: string;
|
||
serial_no?: string | null;
|
||
customer?: string | null;
|
||
company?: string | null;
|
||
order?: string | null;
|
||
supplier?: string | null;
|
||
}
|
||
|
||
export async function updateRepair(
|
||
id: string,
|
||
input: UpdateRepairInput,
|
||
): Promise<Repair> {
|
||
const pb = await getAdminPocketBase();
|
||
const existing = await pb.collection(REPAIRS).getOne<Repair>(id);
|
||
const patch: Record<string, unknown> = {};
|
||
|
||
if (input.status !== undefined) {
|
||
assertTransition(existing.status, input.status);
|
||
patch.status = input.status;
|
||
}
|
||
if (input.notes !== undefined) patch.notes = input.notes ?? "";
|
||
if (input.item_description !== undefined)
|
||
patch.item_description = input.item_description;
|
||
if (input.fault_description !== undefined)
|
||
patch.fault_description = input.fault_description;
|
||
if (input.serial_no !== undefined) patch.serial_no = input.serial_no ?? "";
|
||
if (input.customer !== undefined) patch.customer = input.customer || null;
|
||
if (input.company !== undefined) patch.company = input.company || null;
|
||
if (input.order !== undefined) patch.order = input.order || null;
|
||
if (input.supplier !== undefined) patch.supplier = input.supplier || null;
|
||
|
||
return pb.collection(REPAIRS).update<Repair>(id, patch);
|
||
}
|
||
|
||
/* ============================================================================
|
||
* Repair number generator (server-side, atomic-ish)
|
||
* ========================================================================== */
|
||
|
||
export async function generateRepairNumberServer(
|
||
now: Date = new Date(),
|
||
): Promise<string> {
|
||
const pb = await getAdminPocketBase();
|
||
const year = now.getFullYear();
|
||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||
const prefix = `REP-${year}-${month}-`;
|
||
|
||
const existing = await pb.collection(REPAIRS).getList<Repair>(1, 1, {
|
||
filter: `repair_number ~ "${prefix}"`,
|
||
sort: "-repair_number",
|
||
});
|
||
let seq = 1;
|
||
if (existing.items.length > 0) {
|
||
const last = existing.items[0].repair_number ?? "";
|
||
const lastSeq = parseInt(last.split("-").pop() || "0", 10);
|
||
if (Number.isFinite(lastSeq) && lastSeq > 0) seq = lastSeq + 1;
|
||
}
|
||
return `${prefix}${String(seq).padStart(3, "0")}`;
|
||
}
|
||
|
||
/* ============================================================================
|
||
* "Send to supplier" — generate the DN PDF and attach to repair
|
||
* ========================================================================== */
|
||
|
||
const REPAIR_DN_PREFIX = "RDN";
|
||
|
||
/**
|
||
* Generate a server-side daily sequence for repair DNs. Distinct
|
||
* from the customer-DN sequence so a repair DN never shadows a
|
||
* customer-DN number (operator can tell them apart at a glance).
|
||
*/
|
||
async function generateRepairDeliveryNoteNumber(now: Date = new Date()): Promise<string> {
|
||
const pb = await getAdminPocketBase();
|
||
const yy = String(now.getFullYear()).slice(-2);
|
||
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
||
const dd = String(now.getDate()).padStart(2, "0");
|
||
const prefix = `${REPAIR_DN_PREFIX}${yy}${mm}${dd}`;
|
||
// We use the created order's `delivery_note_number` to find the
|
||
// last sequence. The same `delivery_note_number` field is shared
|
||
// with customer DNs, so we constrain to the repair prefix.
|
||
const existing = await pb
|
||
.collection(ORDER_ITEMS)
|
||
.getList(1, 1, {
|
||
filter: `delivery_note_number ~ "${prefix}"`,
|
||
sort: "-delivery_note_number",
|
||
})
|
||
.catch(() => null);
|
||
let seq = 1;
|
||
if (existing && existing.items.length > 0) {
|
||
const last = (existing.items[0] as { delivery_note_number?: string }).delivery_note_number ?? "";
|
||
const lastSeq = parseInt(last.slice(prefix.length), 10);
|
||
if (Number.isFinite(lastSeq) && lastSeq > 0) seq = lastSeq + 1;
|
||
}
|
||
return `${prefix}${String(seq).padStart(2, "0")}`;
|
||
}
|
||
|
||
export interface GenerateRepairDeliveryNoteResult {
|
||
deliveryNoteId: string;
|
||
deliveryNoteNumber: string;
|
||
filename: string;
|
||
}
|
||
|
||
/**
|
||
* Generate a delivery note for sending the machine to the supplier
|
||
* for analysis. Three steps:
|
||
*
|
||
* 1. Resolve the supplier name + address (for the PDF's "DELIVER TO"
|
||
* block) and the customer name (for the "FROM" reference line).
|
||
* 2. Create a `delivery_notes` header row + one `order_items` row
|
||
* with the repair's item description. The order_items row is
|
||
* what the driver app and the order-completion flows look at;
|
||
* one row is enough for "send this machine to supplier X".
|
||
* 3. Render the DN PDF and stamp it on the repair's
|
||
* `delivery_note` file field so the operator can download it
|
||
* from the repair detail page.
|
||
*
|
||
* The repair's status is bumped to `assessing` (with the standard
|
||
* transition guard). The supplier must already be set on the repair
|
||
* — we don't auto-resolve it.
|
||
*/
|
||
export async function generateRepairDeliveryNote(
|
||
repairId: string,
|
||
): Promise<GenerateRepairDeliveryNoteResult> {
|
||
const pb = await getAdminPocketBase();
|
||
|
||
const repair = await pb.collection(REPAIRS).getOne<Repair>(repairId, {
|
||
expand: "supplier,customer,company",
|
||
});
|
||
|
||
if (!repair.supplier) {
|
||
throw new Error(
|
||
"Repair has no supplier set. Set a supplier on the repair before generating a delivery note.",
|
||
);
|
||
}
|
||
const supplier = await pb.collection(SUPPLIERS).getOne<Supplier>(repair.supplier);
|
||
|
||
// Resolve the customer name (for the PDF reference line) — the
|
||
// "DELIVER TO" block uses the supplier as destination.
|
||
let customerName = "";
|
||
if (repair.customer) {
|
||
try {
|
||
const c = await pb.collection(CUSTOMERS).getOne<Customer>(repair.customer);
|
||
customerName = c.customer ?? "";
|
||
} catch {
|
||
// Customer lookup is best-effort; the PDF falls back to the
|
||
// repair.customer relation name if the lookup fails.
|
||
customerName = repair.expand?.customer?.customer ?? "";
|
||
}
|
||
}
|
||
|
||
const deliveryNoteNumber = await generateRepairDeliveryNoteNumber();
|
||
const now = new Date().toISOString();
|
||
|
||
// ── 1. delivery_notes header ─────────────────────────────────
|
||
// We use the same `delivery_notes` collection as customer DNs;
|
||
// the kind is distinguished by the `notes` field (which we
|
||
// stamp with a leading "[Repair send-to-supplier]" tag) and by
|
||
// the `RDN` number prefix. Customer-DN queries don't filter
|
||
// by number, so this doesn't pollute the customer-DN list.
|
||
const header = await pb.collection(DELIVERY_NOTES).create({
|
||
delivery_note_number: deliveryNoteNumber,
|
||
customer: repair.customer || null,
|
||
company: repair.company || null,
|
||
order: null,
|
||
status: "draft",
|
||
notes:
|
||
`[Repair send-to-supplier] ${repair.repair_number} — ${repair.item_description}` +
|
||
(repair.notes ? `\n\n${repair.notes}` : ""),
|
||
created_by: null,
|
||
});
|
||
|
||
// ── 2. order_items row ───────────────────────────────────────
|
||
const itemDescription = `[Repair ${repair.repair_number}] ${repair.item_description}${repair.serial_no ? ` (S/N: ${repair.serial_no})` : ""}`;
|
||
const item = await pb.collection(ORDER_ITEMS).create({
|
||
order: null,
|
||
delivery_note_number: deliveryNoteNumber,
|
||
description: itemDescription,
|
||
quantity: 1,
|
||
order_qty: 1,
|
||
supplier: repair.supplier,
|
||
cost_price: 0,
|
||
selling_price: 0,
|
||
sku: "",
|
||
notes: repair.fault_description,
|
||
item_status: "delivery_note",
|
||
});
|
||
|
||
// ── 3. Render + upload the DN PDF ─────────────────────────────
|
||
const letterheadCode = deliveryNoteLetterheadCode();
|
||
const letterhead = await fetchLetterhead(letterheadCode);
|
||
const logoDataUri = await loadLetterheadLogoDataUri(letterheadCode);
|
||
|
||
const pdfBuffer = await renderDeliveryNotePdf({
|
||
header: {
|
||
deliveryNoteNumber,
|
||
dateIso: now,
|
||
orderNumber: repair.repair_number,
|
||
companyName: supplier.suppliername ?? "",
|
||
customerName: customerName,
|
||
},
|
||
lines: [
|
||
{
|
||
id: item.id,
|
||
description: itemDescription,
|
||
qty: 1,
|
||
},
|
||
],
|
||
letterhead,
|
||
logoDataUri,
|
||
});
|
||
|
||
const filename = `${deliveryNoteNumber}.pdf`;
|
||
const blob = new Blob([new Uint8Array(pdfBuffer)], { type: "application/pdf" });
|
||
|
||
// Attach to the item's multi-file field (so the rest of the
|
||
// existing DN-download flow works) AND to the repair's
|
||
// `delivery_note` field (so the operator finds it on the
|
||
// repair detail page).
|
||
const itemForm = new FormData();
|
||
itemForm.append("order_item_attachements", blob, filename);
|
||
await pb.collection(ORDER_ITEMS).update(item.id, itemForm);
|
||
|
||
// For the repair, we need to preserve the existing file list
|
||
// (field is multi-file, maxSelect 10) and append the new file.
|
||
const previousFiles = Array.isArray(repair.delivery_note)
|
||
? repair.delivery_note
|
||
: [];
|
||
const repairForm = new FormData();
|
||
for (const f of previousFiles) repairForm.append("delivery_note", f);
|
||
repairForm.append("delivery_note", blob, filename);
|
||
await pb.collection(REPAIRS).update(repairId, repairForm);
|
||
|
||
// ── 4. Status flip: new → assessing ──────────────────────────
|
||
if (repair.status === "new") {
|
||
assertTransition("new", "assessing");
|
||
await pb.collection(REPAIRS).update(repairId, { status: "assessing" });
|
||
}
|
||
|
||
return {
|
||
deliveryNoteId: header.id,
|
||
deliveryNoteNumber,
|
||
filename,
|
||
};
|
||
}
|
||
|
||
/* ============================================================================
|
||
* File upload helpers (photos / quote / purchase_order)
|
||
* ========================================================================== */
|
||
|
||
export type RepairFileField = "photos" | "quote" | "purchase_order" | "delivery_note";
|
||
|
||
export async function uploadRepairFile(
|
||
repairId: string,
|
||
field: RepairFileField,
|
||
file: File | Blob,
|
||
filename: string,
|
||
): Promise<Repair> {
|
||
const pb = await getAdminPocketBase();
|
||
const existing = await pb.collection(REPAIRS).getOne<Repair>(repairId);
|
||
const previousFiles = Array.isArray(existing[field])
|
||
? (existing[field] as string[])
|
||
: [];
|
||
|
||
const form = new FormData();
|
||
for (const f of previousFiles) form.append(field, f);
|
||
form.append(field, file, filename);
|
||
|
||
const updated = await pb.collection(REPAIRS).update<Repair>(repairId, form);
|
||
|
||
// Auto-bump status: uploading a quote moves new/assessing → quoted.
|
||
if (field === "quote" && (existing.status === "new" || existing.status === "assessing")) {
|
||
assertTransition(existing.status, "quoted");
|
||
await pb.collection(REPAIRS).update(repairId, { status: "quoted" });
|
||
}
|
||
|
||
return updated;
|
||
}
|
||
|
||
/**
|
||
* Remove a single file from a multi-file field (PB stores the
|
||
* field as a string[] of filenames). Used by the detail page's
|
||
* "delete" button on each attachment thumbnail.
|
||
*/
|
||
export async function removeRepairFile(
|
||
repairId: string,
|
||
field: RepairFileField,
|
||
filename: string,
|
||
): Promise<Repair> {
|
||
const pb = await getAdminPocketBase();
|
||
const existing = await pb.collection(REPAIRS).getOne<Repair>(repairId);
|
||
const files = Array.isArray(existing[field]) ? (existing[field] as string[]) : [];
|
||
const filtered = files.filter((f) => f !== filename);
|
||
if (filtered.length === files.length) {
|
||
// Nothing to delete — return the row unchanged.
|
||
return existing;
|
||
}
|
||
const form = new FormData();
|
||
for (const f of filtered) form.append(field, f);
|
||
return pb.collection(REPAIRS).update<Repair>(repairId, form);
|
||
}
|
||
|
||
/* ============================================================================
|
||
* Read helpers for the UI (small)
|
||
* ========================================================================== */
|
||
|
||
/**
|
||
* Lightweight list of customers for the "New Repair" dropdown.
|
||
* Distinct from the full customer lookup — just enough to show the
|
||
* name + company in the combobox. Pulled via the admin client; the
|
||
* where-used UI is the office-side repairs page (server-authenticated).
|
||
*
|
||
* The `customer.expand.company` expand is required so the
|
||
* `<CustomerSearch>` combobox can render its "{Company} · {Customer}"
|
||
* triplet. Without this expand the company column falls back to
|
||
* "(no company)" for every row and the picker becomes useless on
|
||
* mobile (the operator can't disambiguate "John Smith at Acme" from
|
||
* "John Smith at Globex").
|
||
*/
|
||
export async function listCustomersForRepairForm(): Promise<Customer[]> {
|
||
const pb = await getAdminPocketBase();
|
||
const result = await pb.collection(CUSTOMERS).getList<Customer>(1, 500, {
|
||
fields: "id,customer,email,number,company,expand",
|
||
sort: "customer",
|
||
expand: "company",
|
||
});
|
||
return result.items;
|
||
}
|
||
|
||
export async function listSuppliersForRepairForm(): Promise<
|
||
Array<{ id: string; suppliername: string }>
|
||
> {
|
||
const pb = await getAdminPocketBase();
|
||
const result = await pb.collection(SUPPLIERS).getList<Supplier>(1, 500, {
|
||
fields: "id,suppliername",
|
||
sort: "suppliername",
|
||
});
|
||
return result.items.map((s) => ({
|
||
id: s.id,
|
||
suppliername: s.suppliername,
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Order lookup used by the OrderSearch combobox. Mirrors the
|
||
* /api/v1/quotes/lookup shape — search by order_number / customer /
|
||
* company. We only return the fields the picker renders, with the
|
||
* customer + company expanded so the dropdown shows
|
||
* "{Company} · {Customer} · {order #}" (matches the sprint modal's
|
||
* QuoteSearch shape).
|
||
*/
|
||
export async function searchOrdersForRepairForm(
|
||
q: string,
|
||
page = 1,
|
||
perPage = 25,
|
||
): Promise<{
|
||
items: Array<{
|
||
id: string;
|
||
order_number: string;
|
||
customer_name: string;
|
||
company_name: string;
|
||
}>;
|
||
totalItems: number;
|
||
totalPages: number;
|
||
page: number;
|
||
perPage: number;
|
||
}> {
|
||
const pb = await getAdminPocketBase();
|
||
const trimmed = q.trim();
|
||
const filters: string[] = [];
|
||
if (trimmed.length > 0) {
|
||
const safe = trimmed.replace(/["()*]/g, "").slice(0, 64);
|
||
if (safe.length > 0) {
|
||
const escaped = safe.replace(/\\/g, "\\\\");
|
||
const ors = [
|
||
`order_number ~ "${escaped}"`,
|
||
`expand.customer.customer ~ "${escaped}"`,
|
||
`expand.company.company ~ "${escaped}"`,
|
||
];
|
||
filters.push(`(${ors.join(" || ")})`);
|
||
}
|
||
}
|
||
const result = await pb
|
||
.collection("orders")
|
||
.getList(page, perPage, {
|
||
filter: filters.join(" && "),
|
||
sort: "-created",
|
||
fields: "id,order_number,customer,company,expand",
|
||
expand: "customer,company",
|
||
})
|
||
.catch((err) => {
|
||
console.error("[searchOrdersForRepairForm] failed:", err);
|
||
return { items: [], page: 1, perPage, totalItems: 0, totalPages: 0 };
|
||
});
|
||
|
||
const items = result.items.map((o) => {
|
||
const expand = (o as { expand?: { customer?: { customer?: string }; company?: { company?: string } } }).expand;
|
||
return {
|
||
id: o.id,
|
||
order_number: (o as { order_number?: string }).order_number ?? "",
|
||
customer_name: expand?.customer?.customer ?? "",
|
||
company_name: expand?.company?.company ?? "",
|
||
};
|
||
});
|
||
|
||
return {
|
||
items,
|
||
totalItems: result.totalItems,
|
||
totalPages: result.totalPages,
|
||
page: result.page,
|
||
perPage: result.perPage,
|
||
};
|
||
}
|
||
|