import * as XLSX from "xlsx"; export type ParsedImportLine = { name: string; unit: string; quantity: number; unitPrice: number; gstRate: number; }; export type ParsedImport = { companyName: string; /** Extracted from structured PO number (COMPANY/VESSEL/ID/FY). Null for legacy formats. */ companyCode: string | null; costCentreCode: string | null; poSequenceId: number | null; poNumber: string; piQuotationNo: string; placeOfDelivery: string; tcDelivery: string; tcDispatch: string; tcInspection: string; tcTransitInsurance: string; tcPaymentTerms: string; tcOthers: string; vendorName: string; vendorAddress: string; vendorContact: string; lineItems: ParsedImportLine[]; }; export function cellStr(sheet: XLSX.WorkSheet, row: number, col: number): string { const addr = XLSX.utils.encode_cell({ r: row, c: col }); const cell = sheet[addr]; if (!cell) return ""; return String(cell.v ?? "").trim(); } export function cellNum(sheet: XLSX.WorkSheet, row: number, col: number): number { const addr = XLSX.utils.encode_cell({ r: row, c: col }); const cell = sheet[addr]; if (!cell) return 0; const v = parseFloat(String(cell.v)); return isNaN(v) ? 0 : v; } /** Parse a structured PO number (COMPANY/VESSEL/ID/FY) into its parts. */ function parsePoNumberParts(poNumber: string): { companyCode: string | null; costCentreCode: string | null; poSequenceId: number | null; } { const parts = poNumber.split("/"); if (parts.length !== 4) return { companyCode: null, costCentreCode: null, poSequenceId: null }; const poSequenceId = parseInt(parts[2], 10); return { companyCode: parts[0] || null, costCentreCode: parts[1] || null, poSequenceId: isNaN(poSequenceId) ? null : poSequenceId, }; } export function parseSheet(sheet: XLSX.WorkSheet): ParsedImport { // Row 1 (index 0) = company name, spanning the full header (col 0) const companyName = cellStr(sheet, 0, 0); const poNumber = cellStr(sheet, 4, 2); const { companyCode, costCentreCode, poSequenceId } = parsePoNumberParts(poNumber); const piQuotationNo = cellStr(sheet, 5, 2); const placeOfDelivery = cellStr(sheet, 8, 2); const vendorName = cellStr(sheet, 12, 2); const vendorAddress = cellStr(sheet, 12, 3); const vendorContact = cellStr(sheet, 13, 2); // T&C from instruction rows 28–33 (col 1) const tcDelivery = cellStr(sheet, 28, 1).replace(/^DELIVERY\s*:\s*/i, "").trim(); const tcDispatch = cellStr(sheet, 29, 1).replace(/^DISPATCH INSTRUCTIONS:\s*/i, "").trim(); const tcInspection = cellStr(sheet, 30, 1).replace(/^INSPECTION\s*:\s*/i, "").trim(); const tcTransitInsurance = cellStr(sheet, 31, 1).replace(/^TRANSIT INSURANCE:\s*/i, "").trim(); const tcPaymentTerms = cellStr(sheet, 32, 1).replace(/^PAYMENT TERMS:\s*/i, "").trim(); const tcOthers = cellStr(sheet, 33, 1).trim(); const lineItems: ParsedImportLine[] = []; for (let r = 15; r <= 100; r++) { const sn = cellStr(sheet, r, 0); const desc = cellStr(sheet, r, 1); // "INSTRUCTIONS TO VENDORS" in col 0 signals the T&C section — stop here if (sn.toUpperCase().includes("INSTRUCTION")) break; if (!desc && !sn) continue; if (!desc) continue; if (desc.toLowerCase().includes("total") || desc.toLowerCase().includes("grand")) break; const unitRaw = cellStr(sheet, r, 3); const qty = cellNum(sheet, r, 4); const unitPrice = cellNum(sheet, r, 5); // Skip rows with no quantity and no unit price (T&C text rows, etc.) if (qty === 0 && unitPrice === 0) continue; const gstRaw = cellNum(sheet, r, 7); const gstRate = gstRaw > 1 ? gstRaw / 100 : gstRaw; lineItems.push({ name: desc, unit: unitRaw || "pc", quantity: qty || 1, unitPrice, gstRate: gstRate || 0.18, }); } return { companyName, companyCode, costCentreCode, poSequenceId, poNumber, piQuotationNo, placeOfDelivery, tcDelivery, tcDispatch, tcInspection, tcTransitInsurance, tcPaymentTerms, tcOthers, vendorName, vendorAddress, vendorContact, lineItems, }; } export function parseWorkbook(buffer: Buffer): ParsedImport[] { const workbook = XLSX.read(buffer, { type: "buffer" }); const results: ParsedImport[] = []; for (const sheetName of workbook.SheetNames) { const sheet = workbook.Sheets[sheetName]; try { const parsed = parseSheet(sheet); if (parsed.lineItems.length > 0) results.push(parsed); } catch { // skip unparseable sheets } } return results; }