// Generates the same "Picklist" PDF that /generate-shipments produces, but // from an arbitrary set of m_inout document numbers (e.g. the currently-open // "Zu kommissionieren" shipments on the dashboard). // // Mirrors the data fetch + aggregation + sort + pdfmake layout in // pages/generate-shipments.vue so both code paths produce identical output. interface PicklistOptions { docNos: string[] sortBy?: 'locator' | 'value' | 'sku' | 'mpn' | 'upc' | 'name' | 'qty' | '' userName?: string title?: string } const loadPdfMake = async () => { const pdfMakeModule: any = await import('pdfmake/build/pdfmake.js') const pdfFontsModule: any = await import('pdfmake/build/vfs_fonts.js') const pdfMake = pdfMakeModule.default || pdfMakeModule let vfs: any = null if (pdfFontsModule.pdfMake?.vfs) vfs = pdfFontsModule.pdfMake.vfs else if (pdfFontsModule.default?.pdfMake?.vfs) vfs = pdfFontsModule.default.pdfMake.vfs else if (pdfFontsModule.default?.vfs) vfs = pdfFontsModule.default.vfs else if (pdfFontsModule.default) { const keys = Object.keys(pdfFontsModule.default) if (keys.some(k => k.endsWith('.ttf'))) vfs = pdfFontsModule.default } pdfMake.vfs = vfs return pdfMake } // Shared comparator: organization first, then the chosen field. Used both to sort // the aggregated picklist rows AND to derive the shipment print order, so the // shipments print in exactly the same order the picklist is sorted in. const lineComparator = (sortBy: string) => (a: any, b: any) => { const orgCompare = (a.AD_Org_ID?.identifier || '').localeCompare(b.AD_Org_ID?.identifier || '') if (orgCompare !== 0) return orgCompare if (!sortBy) return 0 switch (sortBy) { case 'value': return (a.M_Product_ID?.Value || '').localeCompare(b.M_Product_ID?.Value || '') case 'sku': return (a.M_Product_ID?.SKU || '').localeCompare(b.M_Product_ID?.SKU || '') case 'mpn': return (a.M_Product_ID?.mpn || '').localeCompare(b.M_Product_ID?.mpn || '') case 'upc': return (a.M_Product_ID?.UPC || '').localeCompare(b.M_Product_ID?.UPC || '') case 'name': return (a.M_Product_ID?.Name || '').localeCompare(b.M_Product_ID?.Name || '') case 'qty': return Number(b.QtyEntered) - Number(a.QtyEntered) case 'locator': { const xCompare = (a.M_Locator_ID?.X || '').toString().localeCompare((b.M_Locator_ID?.X || '').toString(), undefined, { numeric: true }) if (xCompare !== 0) return xCompare const yCompare = (a.M_Locator_ID?.Y || '').toString().localeCompare((b.M_Locator_ID?.Y || '').toString(), undefined, { numeric: true }) if (yCompare !== 0) return yCompare return (a.M_Locator_ID?.Z || '').toString().localeCompare((b.M_Locator_ID?.Z || '').toString(), undefined, { numeric: true }) } default: return 0 } } const sortPicklistData = (data: any[], sortBy: string) => [...data].sort(lineComparator(sortBy)) export const useGenerateOpenPicklistPdf = () => { const generate = async ({ docNos, sortBy = 'locator', userName = 'SUPERUSER', title = 'Picklist' }: PicklistOptions) => { if (!docNos?.length) return { ok: false, reason: 'no-docs' as const } const aggregated: any[] = [] const relatedDocNos: string[] = [] // Flat product lines tagged with their shipment + a docNo→shipment map, used to // derive the shipment print order (same comparator as the picklist). Does not // affect the picklist PDF itself. const orderLines: any[] = [] const shipmentByDocNo = new Map() for (const docNo of docNos) { if (!docNo) continue let res: any try { res = await $fetch('/api/processes/inouts/' + docNo) } catch (e) { continue } if (!res?.records?.[0]) continue if (!relatedDocNos.includes(docNo)) relatedDocNos.push(docNo) const docRes = res.records[0] if (!shipmentByDocNo.has(docNo)) shipmentByDocNo.set(docNo, { id: docRes.id, documentNo: docNo }) const productLines = (docRes.m_inoutline || []).filter((l: any) => l.M_Product_ID && l.M_Locator_ID && !l.C_Charge_ID) for (const drl of productLines) { orderLines.push({ ...drl, _docNo: docNo }) const existingIdx = aggregated.findIndex(i => i.AD_Org_ID?.id == drl.AD_Org_ID?.id && i.M_Product_ID?.Value == drl.M_Product_ID?.Value && i.M_Locator_ID?.X == drl.M_Locator_ID?.X && i.M_Locator_ID?.Y == drl.M_Locator_ID?.Y && i.M_Locator_ID?.Z == drl.M_Locator_ID?.Z ) if (existingIdx >= 0) { aggregated[existingIdx].QtyEntered = Number(aggregated[existingIdx].QtyEntered) + Number(drl.QtyEntered) } else { aggregated.push({ ...drl }) } } } if (!aggregated.length) return { ok: false, reason: 'no-lines' as const } const sorted = sortPicklistData(aggregated, sortBy) // Shipment print order — same comparator the picklist uses, first occurrence per // shipment, so the referenced shipments print in the picklist's order. const orderedShipments: { id: any, documentNo: string }[] = [] const seenDoc = new Set() for (const line of [...orderLines].sort(lineComparator(sortBy))) { if (seenDoc.has(line._docNo)) continue seenDoc.add(line._docNo) const sh = shipmentByDocNo.get(line._docNo) if (sh) orderedShipments.push(sh) } // Shipments without pickable product lines still belong in the batch — append them. for (const [doc, sh] of shipmentByDocNo) { if (!seenDoc.has(doc)) { seenDoc.add(doc); orderedShipments.push(sh) } } const today = new Date().toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }) const pdfMake = await loadPdfMake() const tableBody: any[] = [ [ { text: 'Org', style: 'tableHeader' }, { text: 'Loc', style: 'tableHeader' }, { text: 'Name', style: 'tableHeader' }, { text: 'Value', style: 'tableHeader' }, { text: 'SKU', style: 'tableHeader' }, { text: 'MPN', style: 'tableHeader' }, { text: 'UPC', style: 'tableHeader' }, { text: 'Qty', style: 'tableHeader', alignment: 'center' }, ], ...sorted.map((item: any) => { return [ { text: item.AD_Org_ID?.identifier || '-', style: 'tableCell' }, { text: `${item.M_Locator_ID?.X ?? ''},${item.M_Locator_ID?.Y ?? ''},${item.M_Locator_ID?.Z ?? ''}`, style: 'tableCell' }, { text: item.M_Product_ID?.Name || '-', style: 'tableCell' }, { text: item.M_Product_ID?.Value || '-', style: 'tableCell' }, { text: item.M_Product_ID?.SKU || '-', style: 'tableCell' }, { text: item.M_Product_ID?.mpn || '-', style: 'tableCell' }, { text: item.M_Product_ID?.UPC || '-', style: 'tableCell' }, { text: String(item.QtyEntered ?? ''), style: 'tableCell', alignment: 'center' }, ] }), ] const content: any[] = [ { text: title, style: 'title', alignment: 'center' }, { text: today, style: 'subtitle', alignment: 'center', margin: [0, 2, 0, 8] }, { table: { headerRows: 1, widths: [40, 48, '*', 66, 66, 66, 66, 26], body: tableBody, dontBreakRows: true, }, layout: { hLineWidth: () => 0.5, vLineWidth: () => 0.5, hLineColor: () => '#9f9f9f', vLineColor: () => '#9f9f9f', fillColor: (rowIndex: number) => rowIndex === 0 ? '#f2f2f2' : null, paddingLeft: () => 3, paddingRight: () => 3, paddingTop: () => 2, paddingBottom: () => 2, }, }, ] if (relatedDocNos.length > 0) { content.push({ margin: [0, 8, 0, 0], table: { widths: ['*'], body: [[{ stack: [ { text: `Related Shipments (${relatedDocNos.length})`, fontSize: 7, bold: true, margin: [0, 0, 0, 3] }, { text: relatedDocNos.join(', '), fontSize: 6.5, lineHeight: 1.4 }, ], margin: [4, 4, 4, 4], }]], }, layout: { hLineWidth: () => 0.5, vLineWidth: () => 0.5, hLineColor: () => '#9f9f9f', vLineColor: () => '#9f9f9f', }, }) } const docDefinition: any = { pageSize: 'A4', pageOrientation: 'portrait', pageMargins: [15, 15, 15, 25], content, footer: (currentPage: number, pageCount: number) => pageCount > 1 ? { text: `${currentPage} / ${pageCount}`, alignment: 'center', fontSize: 7, margin: [0, 8, 0, 0] } : null, styles: { title: { fontSize: 12, bold: true }, subtitle: { fontSize: 7 }, tableHeader: { fontSize: 6.5, bold: true }, tableCell: { fontSize: 6.5 }, }, defaultStyle: { fontSize: 6.5 }, } const stamp = new Date().toISOString().replace(/[-:T]/g, '_').slice(0, 19) const fileName = `${stamp}-PICKLIST-${userName}.pdf` // Return the pdfmake document handle instead of triggering a download here, so // the caller can preview it, download it (pdf.download), or push it to a server // printer (pdf.getBase64 → /api/print/picklist). See PicklistGenerateModal.vue. const pdf = pdfMake.createPdf(docDefinition) return { ok: true, pdf, fileName, count: aggregated.length, docs: relatedDocNos.length, orderedShipments } } return { generate } }