+ */
+import PocketBase from "pocketbase";
+import { jsPDF } from "jspdf";
+import fs from "node:fs/promises";
+import path from "node:path";
+
+const PB_URL = "https://pb.jeistk.apphub.co.za";
+const PB_EMAIL = "david@wfsales.co.za";
+const PB_PASSWORD = "1234567890";
+const QUOTE_NUMBER = process.argv[2] || "QT-20260914-001";
+
+const pb = new PocketBase(PB_URL);
+pb.autoCancellation(false);
+await pb.admins.authWithPassword(PB_EMAIL, PB_PASSWORD);
+
+// Mirror the Quotes.tsx hydration logic
+const record = await pb.collection("quotes").getFirstListItem(
+ `quote_number = "${QUOTE_NUMBER}"`,
+ { expand: "customerName" }
+);
+
+const itemIds = Array.isArray(record.items) ? record.items : [];
+const items = itemIds.map((id, i) => ({
+ stockItemId: id,
+ itemName: "",
+ quantity: record.quantities?.[i] ?? 0,
+ sku: "",
+ sellingPrice: record.unitPrices?.[i] ?? 0,
+}));
+
+if (itemIds.length > 0) {
+ const filter = itemIds.map((id) => `id = "${id}"`).join(" || ");
+ const stockRecs = await pb.collection("stock_items").getList(1, 200, { filter });
+ const byId = new Map(stockRecs.items.map((r) => [r.id, r]));
+ for (const it of items) {
+ const s = byId.get(it.stockItemId);
+ if (s) { it.itemName = s.name; it.sku = s.sku; }
+ }
+}
+
+const customer = record.expand?.customerName;
+const quote = {
+ id: record.id,
+ quote_number: record.quote_number,
+ customerName: customer?.comapny_name ?? "",
+ quoteDate: record.quoteDate,
+ validUntil: record.validUntil,
+ status: record.status,
+ quantities: record.quantities ?? [],
+ unitPrices: record.unitPrices ?? [],
+ items,
+ customer: customer ? { name: customer.comapny_name, address: "", email: "", phone: "" } : null,
+ notes: record.notes ?? "",
+ attachments: [],
+};
+
+console.log("Quote for PDF:", JSON.stringify({ n: quote.quote_number, items: quote.items.length, qty: quote.quantities }, null, 2));
+
+// ---- Mirror the PDF generation from QuoteDataTable.tsx ----
+const doc = new jsPDF();
+const brandBlack = "#000000";
+const accentYellow = "#ffc905";
+const darkText = "#1a1a1a";
+const lightGray = "#f5f5f5";
+const mediumGray = "#666666";
+
+doc.setFillColor(brandBlack);
+doc.rect(0, 0, 210, 45, "F");
+doc.setFillColor(accentYellow);
+doc.rect(0, 45, 210, 2, "F");
+doc.setFont("helvetica", "bold"); doc.setFontSize(20); doc.setTextColor(255, 255, 255);
+doc.text("JEI SA", 14, 20);
+doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(220, 220, 220);
+doc.text("47 Station St, Oberholzer, Carletonville, 2499", 14, 26);
+doc.text("Tel: 018 786 2897", 14, 31);
+doc.setFont("helvetica", "bold"); doc.setFontSize(24); doc.setTextColor(255, 255, 255);
+doc.text("QUOTATION", 120, 25);
+
+doc.setFillColor(250, 250, 250);
+doc.roundedRect(125, 55, 71, 35, 2, 2, "FD");
+doc.setFont("helvetica", "bold"); doc.setFontSize(9); doc.setTextColor(mediumGray);
+doc.text("Quote No:", 128, 61);
+doc.text("Date:", 128, 69);
+doc.text("Valid Until:", 128, 77);
+doc.setFont("helvetica", "normal"); doc.setTextColor(darkText);
+doc.text(quote.quote_number || quote.id, 193, 61, { align: "right" });
+doc.text(new Date(quote.quoteDate).toLocaleDateString(), 193, 69, { align: "right" });
+doc.text(quote.validUntil ? new Date(quote.validUntil).toLocaleDateString() : "N/A", 193, 77, { align: "right" });
+
+let y = 105;
+doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(brandBlack);
+doc.text("CUSTOMER INFORMATION", 14, y);
+doc.setDrawColor(accentYellow); doc.setLineWidth(1.5);
+doc.line(14, y + 2, 75, y + 2);
+y += 10;
+doc.setFillColor(lightGray);
+doc.roundedRect(14, y, 182, 32, 2, 2, "F");
+y += 6;
+doc.setFont("helvetica", "bold"); doc.setFontSize(10); doc.setTextColor(darkText);
+doc.text(quote.customer?.name || quote.customerName || "N/A", 18, y);
+
+y += 14;
+doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(brandBlack);
+doc.text("QUOTE ITEMS", 14, y);
+doc.setDrawColor(accentYellow);
+doc.line(14, y + 2, 55, y + 2);
+y += 8;
+
+doc.setFillColor(brandBlack);
+doc.roundedRect(14, y, 182, 10, 1, 1, "F");
+doc.setFont("helvetica", "bold"); doc.setFontSize(9); doc.setTextColor(255, 255, 255);
+doc.text("#", 17, y + 6);
+doc.text("Description", 28, y + 6);
+doc.text("SKU", 100, y + 6);
+doc.text("Qty", 130, y + 6, { align: "center" });
+doc.text("Unit Price", 155, y + 6, { align: "right" });
+doc.text("Total", 188, y + 6, { align: "right" });
+y += 10;
+
+doc.setFont("helvetica", "normal"); doc.setFontSize(8);
+let subtotal = 0;
+const lineCount = Math.max(quote.quantities?.length ?? 0, quote.items?.length ?? 0, quote.unitPrices?.length ?? 0);
+for (let i = 0; i < lineCount; i++) {
+ const item = quote.items?.[i] || { itemName: "(item)", sku: "", sellingPrice: 0 };
+ const qty = quote.quantities?.[i] || 0;
+ const price = quote.unitPrices?.[i] || item.sellingPrice || 0;
+ const total = qty * price;
+ subtotal += total;
+ const rowH = 10;
+ if (i % 2 === 0) {
+ doc.setFillColor(250, 250, 250);
+ doc.rect(14, y, 182, rowH, "F");
+ }
+ doc.setTextColor(darkText);
+ doc.text(String(i + 1), 17, y + 5);
+ doc.text(String(item.itemName || "(item)").substring(0, 40), 28, y + 5);
+ doc.setTextColor(mediumGray); doc.setFontSize(7);
+ doc.text(item.sku || "—", 100, y + 5);
+ doc.setFontSize(8); doc.setTextColor(darkText);
+ doc.text(String(qty), 130, y + 5, { align: "center" });
+ doc.text(`R${price.toFixed(2)}`, 155, y + 5, { align: "right" });
+ doc.text(`R${total.toFixed(2)}`, 188, y + 5, { align: "right" });
+ y += rowH;
+}
+
+const vatAmount = subtotal * 0.15;
+const totalWithVat = subtotal + vatAmount;
+y += 5;
+doc.setFillColor(lightGray);
+doc.roundedRect(130, y, 66, 30, 2, 2, "F");
+y += 7;
+doc.setFont("helvetica", "normal"); doc.setFontSize(9); doc.setTextColor(mediumGray);
+doc.text("Subtotal:", 135, y);
+doc.setTextColor(darkText);
+doc.text(`R${subtotal.toFixed(2)}`, 188, y, { align: "right" });
+y += 8;
+doc.setTextColor(mediumGray);
+doc.text("Tax (VAT 15%):", 135, y);
+doc.setTextColor(darkText);
+doc.text(`R${vatAmount.toFixed(2)}`, 188, y, { align: "right" });
+y += 8;
+doc.setFillColor(brandBlack);
+doc.roundedRect(130, y - 3, 66, 10, 2, 2, "F");
+doc.setFont("helvetica", "bold"); doc.setFontSize(11); doc.setTextColor(255, 255, 255);
+doc.text("TOTAL:", 135, y + 4);
+doc.text(`R${totalWithVat.toFixed(2)}`, 188, y + 4, { align: "right" });
+
+const out = `/tmp/${QUOTE_NUMBER.replace(/[^A-Za-z0-9_-]/g, "_")}.pdf`;
+const bytes = Buffer.from(doc.output("arraybuffer"));
+await fs.writeFile(out, bytes);
+console.log(`\n✅ PDF written: ${out} (${bytes.length} bytes)`);
+console.log(` Subtotal: R ${subtotal.toFixed(2)}`);
+console.log(` VAT: R ${vatAmount.toFixed(2)}`);
+console.log(` Total: R ${totalWithVat.toFixed(2)}`);
+console.log(` Lines rendered: ${lineCount}`);
diff --git a/src/components/purchase-orders/PurchaseOrderForm.tsx b/src/components/purchase-orders/PurchaseOrderForm.tsx
index 387748d..9e3bfcd 100644
--- a/src/components/purchase-orders/PurchaseOrderForm.tsx
+++ b/src/components/purchase-orders/PurchaseOrderForm.tsx
@@ -356,6 +356,15 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
const formData = new FormData();
Object.entries(purchaseOrderData).forEach(([key, value]) => {
if (Array.isArray(value)) {
+ // JSON fields (snapshot_items) hold objects. String(obj) coerces to
+ // "[object Object]" and PocketBase stores that verbatim — the PO then
+ // renders as N rows of "Unknown / £0.00". Send the array as one JSON
+ // string instead. Arrays of primitives (items, quantities) still
+ // append one entry per element, which is what multi-value fields want.
+ if (value.some(v => typeof v === "object" && v !== null)) {
+ formData.append(key, JSON.stringify(value));
+ return;
+ }
value.forEach(v => formData.append(key, String(v)));
} else {
formData.append(key, String(value));
diff --git a/src/components/purchase-orders/pdf.ts b/src/components/purchase-orders/pdf.ts
index e32b010..9975dc6 100644
--- a/src/components/purchase-orders/pdf.ts
+++ b/src/components/purchase-orders/pdf.ts
@@ -273,8 +273,10 @@ export async function generatePurchaseOrderPdfBlob(order: PurchaseOrder): Promis
return doc.output("blob")
}
+/** Download / stored-copy filename for a PO PDF.
+ * Plain order number, no extra prefix: "PO-20261015-001.pdf". */
export function purchaseOrderFilename(order: PurchaseOrder): string {
- const rawName = order.orderNumber || order.id || `PO_${Date.now()}`
+ const rawName = order.orderNumber || order.id
const safeName = String(rawName).replace(/[^A-Za-z0-9_-]/g, "_")
- return `PO_${safeName}.pdf`
+ return `${safeName}.pdf`
}
\ No newline at end of file
diff --git a/src/components/quotes/QuoteColumns.tsx b/src/components/quotes/QuoteColumns.tsx
index 0de6374..19a497f 100644
--- a/src/components/quotes/QuoteColumns.tsx
+++ b/src/components/quotes/QuoteColumns.tsx
@@ -28,6 +28,7 @@ export type Quote = {
status: 'Draft' | 'Sent' | 'Accepted' | 'Declined'
itemCount: number
validUntil?: string
+ // `items` is an array of stock_item ids (JSON in PB, hydrated async in Quotes.tsx)
items: Array<{ stockItemId: string; itemName: string; quantity: number; sku: string; sellingPrice: number }>
quantities: number[]
unitPrices: number[]
diff --git a/src/components/quotes/QuoteDataTable.tsx b/src/components/quotes/QuoteDataTable.tsx
index 5530df4..79e34e7 100644
--- a/src/components/quotes/QuoteDataTable.tsx
+++ b/src/components/quotes/QuoteDataTable.tsx
@@ -123,8 +123,8 @@ export function QuoteDataTable({ columns: columnsProp, data, filterValue, onFilt
Valid Until: {selectedQuote?.validUntil ? new Date(selectedQuote.validUntil).toLocaleDateString() : 'N/A'}
Items:
- {selectedQuote?.items.map((item, i) => (
- - {item.itemName} (SKU: {item.sku}) — Qty: {selectedQuote.quantities[i]} × R{item.sellingPrice?.toFixed(2) || '0.00'}
+ {(selectedQuote?.items || []).map((item, i) => (
+ - {item.itemName || item.sku || '(item)'} (SKU: {item.sku || '—'}) — Qty: {selectedQuote.quantities[i] ?? 0} × R{(selectedQuote.unitPrices[i] ?? item.sellingPrice ?? 0).toFixed(2)}
))}
@@ -291,9 +291,13 @@ async function generateQuotePDF(quote: Quote) {
doc.setFont("helvetica", "normal")
doc.setFontSize(8)
let subtotal = 0
- quote.items.forEach((item, i) => {
- const qty = quote.quantities[i] || 0
- const price = quote.unitPrices[i] || item.sellingPrice || 0
+ // quantities is the source of truth — old quotes have empty items arrays
+ // because the previous schema truncated them, but quantities/unitPrices are intact.
+ const lineCount = Math.max(quote.quantities?.length ?? 0, quote.items?.length ?? 0, quote.unitPrices?.length ?? 0)
+ for (let i = 0; i < lineCount; i++) {
+ const item = quote.items?.[i] || { itemName: '(item)', sku: '', sellingPrice: 0 }
+ const qty = quote.quantities?.[i] || 0
+ const price = quote.unitPrices?.[i] || item.sellingPrice || 0
const total = qty * price
subtotal += total
@@ -305,17 +309,17 @@ async function generateQuotePDF(quote: Quote) {
doc.setTextColor(darkText)
doc.text(String(i + 1), 17, y + 5)
- doc.text(item.itemName.substring(0, 40), 28, y + 5)
+ doc.text(String(item.itemName || '(item)').substring(0, 40), 28, y + 5)
doc.setTextColor(mediumGray)
doc.setFontSize(7)
- doc.text(item.sku, 100, y + 5)
+ doc.text(item.sku || '—', 100, y + 5)
doc.setFontSize(8)
doc.setTextColor(darkText)
doc.text(String(qty), 130, y + 5, { align: "center" })
doc.text(`R${price.toFixed(2)}`, 155, y + 5, { align: "right" })
doc.text(`R${total.toFixed(2)}`, 188, y + 5, { align: "right" })
y += rowH
- })
+ }
// Totals
const vatRate = 0.15
@@ -464,9 +468,13 @@ export async function saveQuotePdfToPocketbase(quote: Quote, pdfFilename: string
doc.setFont("helvetica", "normal")
doc.setFontSize(8)
let subtotal = 0
- quote.items.forEach((item, i) => {
- const qty = quote.quantities[i] || 0
- const price = quote.unitPrices[i] || item.sellingPrice || 0
+ // quantities is the source of truth — old quotes have empty items arrays
+ // because the previous schema truncated them, but quantities/unitPrices are intact.
+ const lineCount = Math.max(quote.quantities?.length ?? 0, quote.items?.length ?? 0, quote.unitPrices?.length ?? 0)
+ for (let i = 0; i < lineCount; i++) {
+ const item = quote.items?.[i] || { itemName: '(item)', sku: '', sellingPrice: 0 }
+ const qty = quote.quantities?.[i] || 0
+ const price = quote.unitPrices?.[i] || item.sellingPrice || 0
const total = qty * price
subtotal += total
@@ -478,17 +486,17 @@ export async function saveQuotePdfToPocketbase(quote: Quote, pdfFilename: string
doc.setTextColor(darkText)
doc.text(String(i + 1), 17, y + 5)
- doc.text(item.itemName.substring(0, 40), 28, y + 5)
+ doc.text(String(item.itemName || '(item)').substring(0, 40), 28, y + 5)
doc.setTextColor(mediumGray)
doc.setFontSize(7)
- doc.text(item.sku, 100, y + 5)
+ doc.text(item.sku || '—', 100, y + 5)
doc.setFontSize(8)
doc.setTextColor(darkText)
doc.text(String(qty), 130, y + 5, { align: "center" })
doc.text(`R${price.toFixed(2)}`, 155, y + 5, { align: "right" })
doc.text(`R${total.toFixed(2)}`, 188, y + 5, { align: "right" })
y += rowH
- })
+ }
// Totals
const vatRate = 0.15
diff --git a/src/components/quotes/QuoteForm.tsx b/src/components/quotes/QuoteForm.tsx
index 6ca815d..c553e08 100644
--- a/src/components/quotes/QuoteForm.tsx
+++ b/src/components/quotes/QuoteForm.tsx
@@ -140,8 +140,11 @@ export function QuoteForm({ setOpen, refreshData }: QuoteFormProps) {
const selectedCustomer = customers.find(c => c.name === values.customerName)
if (!selectedCustomer) { showError("Invalid customer."); return }
- // Process items: for manual entries, create stock items on-the-fly
- const processedItems = await Promise.all(values.items.map(async (item) => {
+ // Process items: for manual entries, create stock items on-the-fly.
+ // For stock entries, the user MUST have actually selected an item from the
+ // dropdown — otherwise the SKU / stockItemId will be empty.
+ const processedItems: Array<{ stockItemId: string; quantity: number; unitPrice: number }> = []
+ for (const item of values.items) {
if (item.entryMode === 'manual') {
// Create a new stock item
const newStockItem = await pb.collection('stock_items').create({
@@ -151,10 +154,16 @@ export function QuoteForm({ setOpen, refreshData }: QuoteFormProps) {
quantity: 0,
category: 'manual',
})
- return { stockItemId: newStockItem.id, quantity: item.quantity, unitPrice: item.manualUnitPrice || 0 }
+ processedItems.push({ stockItemId: newStockItem.id, quantity: item.quantity, unitPrice: item.manualUnitPrice || 0 })
+ continue
}
- return { stockItemId: item.stockItemId, quantity: item.quantity, unitPrice: item.unitPrice }
- }))
+ // Stock mode: validate that the user picked a real item.
+ if (!item.stockItemId) {
+ showError(`Item ${item.itemName || '(unnamed)'} was not selected from the stock list. Pick it from the dropdown or switch to Manual Entry.`)
+ return
+ }
+ processedItems.push({ stockItemId: item.stockItemId, quantity: item.quantity, unitPrice: item.unitPrice })
+ }
const formData = new FormData()
formData.append('quote_number', values.quoteNumber)
@@ -164,6 +173,7 @@ export function QuoteForm({ setOpen, refreshData }: QuoteFormProps) {
formData.append('validUntil', values.validUntil?.toISOString() || "")
formData.append('notes', values.notes || "")
formData.append('status', 'Draft')
+ // items is a JSON array of stock_item ids (schema fix applied to PB)
formData.append('items', JSON.stringify(processedItems.map(i => i.stockItemId)))
formData.append('quantities', JSON.stringify(processedItems.map(i => i.quantity)))
formData.append('unitPrices', JSON.stringify(processedItems.map(i => i.unitPrice)))
@@ -189,14 +199,16 @@ export function QuoteForm({ setOpen, refreshData }: QuoteFormProps) {
// Try to fetch the stock item
try {
const item = await pb.collection('stock_items').getOne(pi.stockItemId)
- return { stockItemId: pi.stockItemId, itemName: item.name, quantity: item.quantity, sku: item.sku, sellingPrice: item.selling_price || 0 }
+ return { stockItemId: pi.stockItemId, itemName: item.name, quantity: Number(item.quantity ?? 0), sku: item.sku, sellingPrice: Number(item.selling_price ?? 0) }
} catch { return { stockItemId: pi.stockItemId, itemName: '', quantity: 0, sku: '', sellingPrice: 0 } }
}
- return { stockItemId: si.id, itemName: si.name, quantity: si.quantity || 0, sku: si.sku, sellingPrice: si.selling_price || 0 }
+ return { stockItemId: si.id, itemName: si.name, quantity: Number(si.quantity ?? 0), sku: si.sku, sellingPrice: Number(si.selling_price ?? 0) }
})),
customer: customer ? { name: customer.name, address: '', email: '', phone: '' } : null,
notes: values.notes || '',
attachments: [] as string[],
+ itemCount: processedItems.length,
+ customerRef: values.customerRef || '',
}
const pdfFilename = `QT_${values.quoteNumber.replace(/[^A-Za-z0-9_-]/g, "_")}.pdf`
await saveQuotePdfToPocketbase(quoteForPdf, pdfFilename)
diff --git a/src/pages/PurchaseOrders.tsx b/src/pages/PurchaseOrders.tsx
index 53716c2..28e0d97 100644
--- a/src/pages/PurchaseOrders.tsx
+++ b/src/pages/PurchaseOrders.tsx
@@ -50,12 +50,19 @@ const PurchaseOrders = () => {
if (!records) throw lastErr;
const formattedData: PurchaseOrder[] = records.items.map((record: any) => {
+ // Only trust the snapshot when it actually carries readable rows.
+ // A snapshot that saved as ["[object Object]"] is non-empty but useless;
+ // falling back to the live relation keeps the PO (and its PDF) correct.
const useSnapshot =
- Array.isArray(record.snapshot_items) && record.snapshot_items.length > 0;
+ Array.isArray(record.snapshot_items) &&
+ record.snapshot_items.length > 0 &&
+ record.snapshot_items.every(
+ (it: any) => it && typeof it === "object" && typeof it.itemName === "string" && it.itemName !== "",
+ );
const items = useSnapshot
? record.snapshot_items.map((item: any) => ({
stockItemId: item.stockItemId || "",
- itemName: item.itemName || "Unknown",
+ itemName: item.itemName,
quantity: item.quantity || 0,
sku: item.sku || "",
costPrice: item.costPrice || 0,
diff --git a/src/pages/Quotes.tsx b/src/pages/Quotes.tsx
index 7b9f6c6..77836ae 100644
--- a/src/pages/Quotes.tsx
+++ b/src/pages/Quotes.tsx
@@ -23,44 +23,62 @@ const Quotes = () => {
try {
const records = await pb.collection('quotes').getList(1, 50, {
sort: '-quoteDate',
- expand: 'customerName,items',
+ expand: 'customerName',
})
const formattedData = records.items.map((record: any) => {
- // quantities is the source of truth for item count
- const qtyArray = record.quantities || []
- const itemCount = qtyArray.length || 1
- // items field may be a single ID string or null when no expand
- const itemId = record.items || ''
+ const qtyArray: number[] = Array.isArray(record.quantities) ? record.quantities : []
+ const priceArray: number[] = Array.isArray(record.unitPrices) ? record.unitPrices : []
+ const itemIds: string[] = Array.isArray(record.items) ? record.items : []
+ const itemCount = qtyArray.length || itemIds.length || 0
+
+ // Hydrate item details from stock_items using the parallel `items` array.
+ const items = itemIds.map((stockItemId: string, i: number) => ({
+ stockItemId,
+ itemName: '',
+ quantity: qtyArray[i] ?? 0,
+ sku: '',
+ sellingPrice: priceArray[i] ?? 0,
+ }))
+
return {
id: record.id,
quote_number: record.quote_number || record.id,
- customerName: record.expand?.customerName?.comapny_name || record.customerName || '',
+ customerName: record.expand?.customerName?.comapny_name || record.expand?.customerName?.customer || record.customerName || '',
customerRef: record.customerRef || '',
quoteDate: record.quoteDate || record.created,
validUntil: record.validUntil || '',
status: record.status || 'Draft',
itemCount,
quantities: qtyArray,
- unitPrices: record.unitPrices || [],
- items: record.expand?.items ? [
- {
- stockItemId: record.expand.items.id,
- itemName: record.expand.items.name,
- quantity: record.expand.items.quantity || 0,
- sku: record.expand.items.sku,
- sellingPrice: record.expand.items.selling_price || 0,
- }
- ] : (itemId ? [{ stockItemId: itemId, itemName: '', quantity: 0, sku: '', sellingPrice: 0 }] : []),
+ unitPrices: priceArray,
+ items,
customer: record.expand?.customerName ? {
- name: record.expand.customerName.comapny_name || '',
- address: record.expand.customerName.address || '',
+ name: record.expand.customerName.comapny_name || record.expand.customerName.customer || '',
+ address: '',
email: record.expand.customerName.email || '',
- phone: record.expand.customerName.phone || '',
+ phone: '',
} : null,
attachments: record.attachments || [],
notes: record.notes || '',
}
})
+
+ // Async-hydrate item names/skus from stock_items for display
+ const stockIds = Array.from(new Set(formattedData.flatMap(q => q.items.map(i => i.stockItemId)).filter(Boolean)))
+ if (stockIds.length > 0) {
+ try {
+ const filter = stockIds.map(id => `id = "${id}"`).join(' || ')
+ const stockRecs = await pb.collection('stock_items').getList(1, 200, { filter })
+ const byId = new Map(stockRecs.items.map((r: any) => [r.id, r]))
+ for (const q of formattedData) {
+ for (const it of q.items) {
+ const s = byId.get(it.stockItemId)
+ if (s) { it.itemName = s.name; it.sku = s.sku; it.quantity = s.quantity ?? 0 }
+ }
+ }
+ } catch (e) { console.warn('stock_items hydrate failed', e) }
+ }
+
setQuotes(formattedData)
} catch (err) {
console.error("Error fetching quotes:", err)