diff --git a/app/controllers/report.controller.js b/app/controllers/report.controller.js index 9ca7ab0..564981d 100644 --- a/app/controllers/report.controller.js +++ b/app/controllers/report.controller.js @@ -28,6 +28,7 @@ const toObjectIdText = (value) => { if (typeof value === "object" && value._id) return `${value._id}`; return `${value}`; }; +const normalizeCustomerName = (name) => (name || "").toString().trim().toLowerCase().replace(/\s+/g, " "); const findTemplatePathBySite = (site) => { const safeSite = [1, 2, 3].includes(Number(site)) ? Number(site) : 1; @@ -180,11 +181,22 @@ const appendNoticeLine = (parts, text) => { } }; -const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, vehiclesMap, outboundCustomerStatuses, unicodeFontBytes, allRoutes, customerNotesById, debugRows) => { +const collectFieldRects = (form, fieldName) => { + try { + const field = form.getTextField(fieldName); + const widgets = field?.acroField?.getWidgets?.() || []; + return widgets.map((widget) => widget.getRectangle()); + } catch (_err) { + return []; + } +}; + +const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, vehiclesMap, outboundCustomerStatuses, unicodeFontBytes, allRoutes, customerNotesById, customerNotesByName, debugRows) => { const pdfDoc = await PDFDocument.load(templateBytes); pdfDoc.registerFontkit(fontkit); const unicodeFont = await pdfDoc.embedFont(unicodeFontBytes, { subset: true }); const form = pdfDoc.getForm(); + const markDrawQueue = []; const routeName = route?.name || ""; const scheduleDate = route?.schedule_date || ""; const driverId = toObjectIdText(route?.driver); @@ -209,7 +221,11 @@ const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, veh safeSetField(form, `addr_${row}`, customer?.customer_address || ""); safeSetField(form, `phone_${row}`, customer?.customer_phone || ""); const customerId = toObjectIdText(customer?.customer_id); - const profileNoteToDriver = (customerNotesById?.get(customerId) || "").toString().trim(); + const profileNoteToDriver = ( + customerNotesById?.get(customerId) || + customerNotesByName?.get(normalizeCustomerName(customer?.customer_name || "")) || + "" + ).toString().trim(); const customerNoteText = profileNoteToDriver; safeSetField(form, `note_${row}`, customerNoteText); @@ -236,16 +252,22 @@ const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, veh nValue = ""; safeSetField(form, `y_${row}`, yValue, { fontSize: 12, center: true, blackText: true }); safeSetField(form, `n_${row}`, nValue, { fontSize: 12, center: true, blackText: true }); + const yRects = collectFieldRects(form, `y_${row}`); + yRects.forEach((rect) => markDrawQueue.push({ rect, text: "Y" })); } else if (hasInCenterStatusByCandidate(customer, relativeRouteCustomer, customerInOtherRoute)) { yValue = "Y"; nValue = ""; safeSetField(form, `y_${row}`, yValue, { fontSize: 12, center: true, blackText: true }); safeSetField(form, `n_${row}`, nValue, { fontSize: 12, center: true, blackText: true }); + const yRects = collectFieldRects(form, `y_${row}`); + yRects.forEach((rect) => markDrawQueue.push({ rect, text: "Y" })); } else { yValue = ""; nValue = "N"; safeSetField(form, `y_${row}`, yValue, { fontSize: 12, center: true, blackText: true }); safeSetField(form, `n_${row}`, nValue, { fontSize: 12, center: true, blackText: true }); + const nRects = collectFieldRects(form, `n_${row}`); + nRects.forEach((rect) => markDrawQueue.push({ rect, text: "N" })); } const outboundStatus = findOutboundStatusByCustomerId(outboundCustomerStatuses, customer?.customer_id); @@ -328,6 +350,14 @@ const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, veh // Rebuild form appearances with a Unicode font so UTF-8 (e.g., Chinese) renders correctly. form.updateFieldAppearances(unicodeFont); form.flatten(); + const firstPage = pdfDoc.getPage(0); + markDrawQueue.forEach(({ rect, text }) => { + const fontSize = Math.min(Math.max(rect.height * 0.72, 9), 14); + const textWidth = unicodeFont.widthOfTextAtSize(text, fontSize); + const x = rect.x + Math.max((rect.width - textWidth) / 2, 1); + const y = rect.y + Math.max((rect.height - fontSize) / 2, 1); + firstPage.drawText(text, { x, y, size: fontSize, font: unicodeFont, color: rgb(0, 0, 0) }); + }); return pdfDoc.save(); }; exports.createReport = (req, res) => { @@ -494,6 +524,17 @@ exports.exportRouteReportZip = async (req, res) => { (customer?.notes_for_driver || "").toString().trim() ]) ); + const customerNotesByName = new Map(); + (customers || []).forEach((customer) => { + const note = (customer?.notes_for_driver || "").toString().trim(); + if (!note) return; + const key = normalizeCustomerName(customer?.name || customer?.name_cn || `${customer?.firstname || ""} ${customer?.lastname || ""}`); + if (!key) return; + const previous = customerNotesByName.get(key); + if (!previous || (customer?.status || "").toString().toLowerCase() === "active") { + customerNotesByName.set(key, note); + } + }); const getRouteVehicleNumber = (route) => { const vehicleId = toObjectIdText(route?.vehicle); const vehicleNumberRaw = vehiclesMap.get(vehicleId)?.vehicle_number; @@ -544,6 +585,7 @@ exports.exportRouteReportZip = async (req, res) => { unicodeFontBytes, routes, customerNotesById, + customerNotesByName, debugMode ? debugRows : null ); const base = sanitizeFileName(route?.name || `route_${i + 1}`) || `route_${i + 1}`; diff --git a/client/src/components/trans-routes/RouteReportWithSignature.js b/client/src/components/trans-routes/RouteReportWithSignature.js index 605c8aa..8706df5 100644 --- a/client/src/components/trans-routes/RouteReportWithSignature.js +++ b/client/src/components/trans-routes/RouteReportWithSignature.js @@ -24,6 +24,7 @@ const RouteReportWithSignature = () => { const [directorSignature, setDirectorSignature] = useState(undefined); const [customerMetaById, setCustomerMetaById] = useState(new Map()); + const [customerMetaByName, setCustomerMetaByName] = useState(new Map()); const site = EventsService.site; @@ -82,17 +83,28 @@ const RouteReportWithSignature = () => { CustomerService.getAllCustomers() .then((res) => { const nextMap = new Map(); + const nextByNameMap = new Map(); (res?.data || []).forEach((customer) => { - nextMap.set(customer?.id, { + const meta = { program_type: customer?.program_type || '', pay_source: customer?.pay_source || '', notes_for_driver: customer?.notes_for_driver || '' - }); + }; + nextMap.set(customer?.id, meta); + const nameKey = (customer?.name || '').toString().trim().toLowerCase(); + if (nameKey) { + const previous = nextByNameMap.get(nameKey); + if (!previous || (customer?.status || '').toString().toLowerCase() === 'active') { + nextByNameMap.set(nameKey, meta); + } + } }); setCustomerMetaById(nextMap); + setCustomerMetaByName(nextByNameMap); }) .catch(() => { setCustomerMetaById(new Map()); + setCustomerMetaByName(new Map()); }); }, []); const normalizeChecklistText = (value = '') => @@ -187,6 +199,9 @@ const RouteReportWithSignature = () => { const getRouteNoteToDriverText = (customerId, ...customerCandidates) => { const profileNote = (customerMetaById.get(customerId)?.notes_for_driver || '').toString().trim(); if (profileNote) return profileNote; + const firstCustomerName = (customerCandidates?.[0]?.customer_name || '').toString().trim().toLowerCase(); + const profileNoteByName = (customerMetaByName.get(firstCustomerName)?.notes_for_driver || '').toString().trim(); + if (profileNoteByName) return profileNoteByName; return ''; };