import getPocketBase from "./pocketbase"; import type { Customer, Company, Supplier, Quote, QuoteItem, Order, OrderItem, PurchaseOrder, CollectionPool, DeliveryNote, Repair } from "./types"; export async function getCustomers(page = 1, perPage = 50, search = "", sort = "-created") { const pb = getPocketBase(); const filter = search ? `customer ~ "${search}" || email ~ "${search}" || number ~ "${search}" || company ~ "${search}"` : ""; return pb.collection("customers").getList(page, perPage, { filter, sort }); } export async function getCustomer(id: string) { const pb = getPocketBase(); return pb.collection("customers").getOne(id); } export async function getAllCustomers() { const pb = getPocketBase(); return pb.collection("customers").getFullList({ sort: "-created" }); } export async function getCompanies(page = 1, perPage = 50, search = "", sort = "-created") { const pb = getPocketBase(); const filter = search ? `company ~ "${search}" || vendor ~ "${search}"` : ""; return pb.collection("companies").getList(page, perPage, { filter, sort }); } export async function getCompany(id: string) { const pb = getPocketBase(); return pb.collection("companies").getOne(id); } export async function getAllCompanies() { const pb = getPocketBase(); return pb.collection("companies").getFullList({ sort: "-created" }); } export async function getSuppliers(page = 1, perPage = 50, search = "", sort = "-created") { const pb = getPocketBase(); const filter = search ? `suppliername ~ "${search}" || email ~ "${search}" || suppliernumber ~ "${search}" || supply_tags ~ "${search}"` : ""; return pb.collection("suppliers").getList(page, perPage, { filter, sort }); } export async function getSupplier(id: string) { const pb = getPocketBase(); return pb.collection("suppliers").getOne(id); } export async function getAllSuppliers() { const pb = getPocketBase(); return pb.collection("suppliers").getFullList({ sort: "-created" }); } export async function getQuotes(page = 1, perPage = 50, search = "", sort = "-created", status?: string) { const pb = getPocketBase(); const filters: string[] = []; if (search) { filters.push(`quote_number ~ "${search}" || quote_reference ~ "${search}" || rfq_number ~ "${search}"`); } if (status) { filters.push(`status = "${status}"`); } const filter = filters.length > 0 ? filters.join(" && ") : ""; return pb.collection("quotes").getList(page, perPage, { filter, sort, expand: "customer,company", }); } export async function getQuote(id: string) { const pb = getPocketBase(); return pb.collection("quotes").getOne(id, { expand: "customer,company" }); } export async function getQuoteItems(quoteId: string) { const pb = getPocketBase(); return pb.collection("quote_items").getFullList({ filter: `quote = "${quoteId}"`, expand: "supplier", }); } export async function getOrders(page = 1, perPage = 50, search = "", sort = "-created", status?: string) { const pb = getPocketBase(); const filters: string[] = []; if (search) { filters.push(`order_number ~ "${search}"`); } if (status) { filters.push(`status = "${status}"`); } const filter = filters.length > 0 ? filters.join(" && ") : ""; return pb.collection("orders").getList(page, perPage, { filter, sort, expand: "company,customer,quote", }); } export async function getOrder(id: string) { const pb = getPocketBase(); return pb.collection("orders").getOne(id, { expand: "company,customer,quote" }); } export async function getOrderByQuote(quoteId: string) { const pb = getPocketBase(); try { return await pb.collection("orders").getFirstListItem(`quote = "${quoteId}"`, { expand: "company,customer,quote", }); } catch { return null; } } export async function getOrderItems(orderId: string) { const pb = getPocketBase(); return pb.collection("order_items").getFullList({ filter: `order = "${orderId}"`, expand: "supplier", }); } export async function createOrderFromQuote(quoteId: string) { const pb = getPocketBase(); const existingOrder = await getOrderByQuote(quoteId); if (existingOrder) { return { order: existingOrder, alreadyExists: true }; } const quote = await pb.collection("quotes").getOne(quoteId); const quoteItems = await pb.collection("quote_items").getFullList({ filter: `quote = "${quoteId}"`, sort: "created", }); if (quoteItems.length === 0) { throw new Error("Quote has no items to convert."); } const orderNumber = await generateOrderNumber(); const order = await pb.collection("orders").create({ order_number: orderNumber, company: quote.company, customer: quote.customer || "", quote: quote.id, status: "new", notes: quote.notes || "", quote_matching_attempted: true, quote_match_confidence: 1, potential_quote: quote.id, potential_quote_confidence: 1, is_repair_order: false, }); for (const item of quoteItems) { const quantity = Number(item.quantity || 1); await pb.collection("order_items").create({ order: order.id, description: item.description || "", po_description: item.description || "", quantity, order_qty: quantity, selling_price: Number(item.selling_price || 0), supplier: item.supplier || null, cost_price: Number(item.cost_price || 0), markup: Number(item.markup || 0), markup_percent: Number(item.markup || 0), item_status: "pending", drv_status: "unassigned", driver_status: "unassigned", notes: item.notes || "", quote_item: item.id, }); } return { order, alreadyExists: false }; } export async function getCollectionPool(page = 1, perPage = 50, status = "") { const pb = getPocketBase(); const filter = status ? `pool_status = "${status}"` : ""; return pb.collection("collection_pool").getList(page, perPage, { filter, sort: "-created", expand: "order_item,order,supplier,drv_id", }); } export async function queueOrderItemForCollection(item: OrderItem, orderId: string) { const pb = getPocketBase(); const existing = await pb.collection("collection_pool").getList(1, 1, { filter: `order_item = "${item.id}" && pool_status != "completed" && pool_status != "failed"`, }); if (existing.items.length === 0) { await pb.collection("collection_pool").create({ order: orderId, order_item: item.id, supplier: item.supplier || null, pool_status: "queued", priority: "normal", pool_notes: item.notes || "", }); } return pb.collection("order_items").update(item.id, { item_status: "driver_pool", drv_status: "unassigned", }); } export async function updateOrderItemWorkflowStatus( itemId: string, patch: Partial> ) { const pb = getPocketBase(); return pb.collection("order_items").update(itemId, patch); } export async function updateCollectionPoolStatus( poolItem: CollectionPool, poolStatus: CollectionPool["pool_status"] ) { const pb = getPocketBase(); const payload: Record = { pool_status: poolStatus }; if (poolStatus === "assigned") { payload.assigned_at = new Date().toISOString(); } if (poolStatus === "completed") { payload.completed_at = new Date().toISOString(); } const updated = await pb.collection("collection_pool").update(poolItem.id, payload); if (poolStatus === "assigned") { await pb.collection("order_items").update(poolItem.order_item, { item_status: "assigned", drv_status: "assigned", }); } if (poolStatus === "in_progress") { await pb.collection("order_items").update(poolItem.order_item, { drv_status: "en_route", }); } if (poolStatus === "completed") { await pb.collection("order_items").update(poolItem.order_item, { item_status: "collected", drv_status: "collected", }); } if (poolStatus === "failed") { await pb.collection("order_items").update(poolItem.order_item, { drv_status: "failed", problem_flag: true, problem_type: "driver", }); } return updated; } export async function getDeliveryNotes(page = 1, perPage = 50, search = "") { const pb = getPocketBase(); const filter = search ? `delivery_note_number ~ "${search}" || notes ~ "${search}"` : ""; return pb.collection("delivery_notes").getList(page, perPage, { filter, sort: "-created", expand: "order,company,customer", }); } export async function generateDeliveryNoteNumber(): Promise { const pb = getPocketBase(); const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const prefix = `DN-${year}-${month}-`; const existing = await pb.collection("delivery_notes").getList(1, 1, { filter: `delivery_note_number ~ "${prefix}"`, sort: "-delivery_note_number", }); let seq = 1; if (existing.items.length > 0) { const lastNum = parseInt(existing.items[0].delivery_note_number.split("-").pop() || "0", 10); seq = lastNum + 1; } return `${prefix}${String(seq).padStart(3, "0")}`; } export async function createDeliveryNoteFromOrder(order: Order, items: OrderItem[]) { const pb = getPocketBase(); const eligible = items.filter((item) => ["ready_delivery", "delivered"].includes(item.item_status)); if (eligible.length === 0) { throw new Error("No ready or delivered items available for a delivery note."); } const deliveryNoteNumber = await generateDeliveryNoteNumber(); const deliveryNote = await pb.collection("delivery_notes").create({ delivery_note_number: deliveryNoteNumber, order: order.id, company: order.company || null, customer: order.customer || null, status: "draft", notes: order.notes || "", }); for (const item of eligible) { await pb.collection("delivery_note_items").create({ delivery_note: deliveryNote.id, order_item: item.id, description: item.description, quantity: item.quantity, }); } return deliveryNote; } export async function getRepairs(page = 1, perPage = 50, search = "", status = "") { const pb = getPocketBase(); const filters: string[] = []; if (search) { filters.push(`repair_number ~ "${search}" || item_description ~ "${search}" || fault_description ~ "${search}"`); } if (status) { filters.push(`status = "${status}"`); } return pb.collection("repairs").getList(page, perPage, { filter: filters.join(" && "), sort: "-created", expand: "customer,company,order", }); } export async function generateRepairNumber(): Promise { const pb = getPocketBase(); const now = new Date(); 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(1, 1, { filter: `repair_number ~ "${prefix}"`, sort: "-repair_number", }); let seq = 1; if (existing.items.length > 0) { const lastNum = parseInt(existing.items[0].repair_number.split("-").pop() || "0", 10); seq = lastNum + 1; } return `${prefix}${String(seq).padStart(3, "0")}`; } export async function createRepair(data: { customer?: string; company?: string; order?: string; item_description: string; fault_description: string; notes?: string; }) { const pb = getPocketBase(); const repairNumber = await generateRepairNumber(); return pb.collection("repairs").create({ repair_number: repairNumber, customer: data.customer || null, company: data.company || null, order: data.order || null, status: "new", item_description: data.item_description, fault_description: data.fault_description, notes: data.notes || "", }); } export async function generateQuoteNumber(): Promise { const pb = getPocketBase(); const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const prefix = `Q-${year}-${month}-`; const existing = await pb.collection("quotes").getList(1, 1, { filter: `quote_number ~ "${prefix}"`, sort: "-quote_number", }); let seq = 1; if (existing.items.length > 0) { const lastRef = existing.items[0].quote_number; const lastNum = parseInt(lastRef.split("-").pop() || "0", 10); seq = lastNum + 1; } return `${prefix}${String(seq).padStart(3, "0")}`; } export async function generateOrderNumber(): Promise { const pb = getPocketBase(); const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const prefix = `ORD-${year}-${month}-`; const existing = await pb.collection("orders").getList(1, 1, { filter: `order_number ~ "${prefix}"`, sort: "-order_number", }); let seq = 1; if (existing.items.length > 0) { const lastNum = parseInt(existing.items[0].order_number.split("-").pop() || "0", 10); seq = lastNum + 1; } return `${prefix}${String(seq).padStart(3, "0")}`; } export async function generatePONumber(): Promise { const pb = getPocketBase(); const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const prefix = `PO-${year}-${month}-`; const existing = await pb.collection("purchase_orders").getList(1, 1, { filter: `po_number ~ "${prefix}"`, sort: "-po_number", }); let seq = 1; if (existing.items.length > 0) { const lastNum = parseInt(existing.items[0].po_number.split("-").pop() || "0", 10); seq = lastNum + 1; } return `${prefix}${String(seq).padStart(3, "0")}`; } export function calculateTotals(items: { selling_price: number; quantity: number }[]) { const subtotal = Number(items.reduce((sum, item) => sum + item.selling_price * item.quantity, 0).toFixed(2)); const vat_amount = Number((subtotal * 0.15).toFixed(2)); const grand_total = Number((subtotal + vat_amount).toFixed(2)); return { subtotal, vat_amount, grand_total }; } export function formatCurrency(amount: number): string { return new Intl.NumberFormat("en-ZA", { style: "currency", currency: "ZAR" }).format(amount); } export function formatDate(dateStr: string): string { if (!dateStr) return ""; return new Date(dateStr).toLocaleDateString("en-ZA", { year: "numeric", month: "short", day: "numeric", }); } export function exportToCSV(data: Record[], filename: string) { if (data.length === 0) return; const headers = Object.keys(data[0]); const csv = [ headers.join(","), ...data.map((row) => headers .map((h) => { const val = row[h]; const str = val === null || val === undefined ? "" : String(val); return str.includes(",") || str.includes('"') ? `"${str.replace(/"/g, '""')}"` : str; }) .join(",") ), ].join("\n"); const blob = new Blob([csv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${filename}.csv`; a.click(); URL.revokeObjectURL(url); }