fix
All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 34s

This commit is contained in:
2026-03-12 12:50:45 -04:00
parent 9a6e503fb3
commit 1f0c6938b9
18 changed files with 1449 additions and 12 deletions

View File

@@ -1,6 +1,122 @@
const { splitSite } = require("../middlewares");
const db = require("../models");
const fs = require("fs");
const path = require("path");
const moment = require("moment-timezone");
const archiver = require("archiver");
const { PDFDocument } = require("pdf-lib");
const Report = db.report;
const RoutePath = db.route_path;
const Employee = db.employee;
const Vehicle = db.vehicle;
const ROOT_DIR = path.resolve(__dirname, "../..");
const TARGET_TIMEZONE = "America/New_York";
const sanitizeFileName = (name) =>
(name || "route")
.toString()
.replace(/[\\/:*?"<>|]/g, "_")
.trim();
const toObjectIdText = (value) => {
if (!value) return "";
if (typeof value === "string") return value;
if (typeof value === "object" && value._id) return `${value._id}`;
return `${value}`;
};
const findTemplatePathBySite = (site) => {
const safeSite = [1, 2, 3].includes(Number(site)) ? Number(site) : 1;
const fileName = `pdf_templete${safeSite}.pdf`;
const candidatePaths = [
path.join(ROOT_DIR, "app", "views", "upload", fileName),
path.join(ROOT_DIR, "client", "build", "upload", fileName),
path.join(ROOT_DIR, "client", "public", "upload", fileName)
];
return candidatePaths.find((candidate) => fs.existsSync(candidate)) || "";
};
const formatUtcToLocalHm = (dateLike) => {
if (!dateLike) return "";
const parsed = moment.utc(dateLike);
if (!parsed.isValid()) return "";
return parsed.tz(TARGET_TIMEZONE).format("HH:mm");
};
const safeSetField = (form, fieldName, value) => {
if (!fieldName || value === undefined || value === null) return;
const text = `${value}`;
try {
const field = form.getTextField(fieldName);
field.setText(text);
return;
} catch (_textErr) {}
try {
const checkbox = form.getCheckBox(fieldName);
if (text.trim()) {
checkbox.check();
} else {
checkbox.uncheck();
}
} catch (_checkboxErr) {}
};
const findOutboundStatusByCustomerId = (outboundCustomerStatuses, customerId) => {
const targetId = toObjectIdText(customerId);
return outboundCustomerStatuses.find((item) => toObjectIdText(item?.customer_id) === targetId) || {};
};
const buildRoutePdfBuffer = async (templateBytes, route, seqNum, driversMap, vehiclesMap, outboundCustomerStatuses) => {
const pdfDoc = await PDFDocument.load(templateBytes);
const form = pdfDoc.getForm();
const routeName = route?.name || "";
const scheduleDate = route?.schedule_date || "";
const driverId = toObjectIdText(route?.driver);
const vehicleId = toObjectIdText(route?.vehicle);
const driver = driversMap.get(driverId);
const vehicle = vehiclesMap.get(vehicleId);
const driverName = driver
? `${driver.name || ""}${driver.name_cn ? driver.name_cn : ""}`
: "";
safeSetField(form, "route", routeName);
safeSetField(form, "driver", driverName);
safeSetField(form, "date", scheduleDate);
safeSetField(form, "vehicle", vehicle?.vehicle_number || "");
safeSetField(form, "seq_num", seqNum);
const customers = Array.isArray(route?.route_customer_list) ? route.route_customer_list : [];
customers.forEach((customer, index) => {
const row = index + 1;
safeSetField(form, `name_${row}`, customer?.customer_name || "");
safeSetField(form, `addr_${row}`, customer?.customer_address || "");
safeSetField(form, `phone_${row}`, customer?.customer_phone || "");
safeSetField(form, `note_${row}`, customer?.customer_note || "");
const pickupTime = formatUtcToLocalHm(customer?.customer_pickup_time);
if (pickupTime) safeSetField(form, `pick_${row}`, pickupTime);
const enterCenterTime = formatUtcToLocalHm(customer?.customer_enter_center_time);
if (enterCenterTime) {
safeSetField(form, `arrive_${row}`, enterCenterTime);
safeSetField(form, `y_${row}`, "✓");
} else if (customer?.customer_route_status === "inCenter") {
safeSetField(form, `y_${row}`, "✓");
} else {
safeSetField(form, `n_${row}`, "✕");
}
const outboundStatus = findOutboundStatusByCustomerId(outboundCustomerStatuses, customer?.customer_id);
const leaveCenterTime = formatUtcToLocalHm(outboundStatus?.customer_leave_center_time);
const dropoffTime = formatUtcToLocalHm(outboundStatus?.customer_dropoff_time);
if (leaveCenterTime) safeSetField(form, `departure_${row}`, leaveCenterTime);
if (dropoffTime) safeSetField(form, `drop_${row}`, dropoffTime);
});
form.flatten();
return pdfDoc.save();
};
exports.createReport = (req, res) => {
// Validate request
if (!req.body.data) {
@@ -116,3 +232,84 @@ exports.updateReport = (req, res) => {
});
};
exports.exportRouteReportZip = async (req, res) => {
try {
const date = req.query?.date;
if (!date) {
return res.status(400).send({ message: "date query is required." });
}
const site = splitSite.findSiteNumber(req);
const templatePath = findTemplatePathBySite(site);
if (!templatePath) {
return res.status(500).send({ message: `Missing PDF template for site ${site}.` });
}
const [routes, drivers, vehicles] = await Promise.all([
RoutePath.find(splitSite.splitSiteGet(req, { schedule_date: date, status: { $ne: "disabled" } })),
Employee.find(splitSite.splitSiteGet(req, {})),
Vehicle.find(splitSite.splitSiteGet(req, { status: "active" }))
]);
const inboundRoutes = (routes || []).filter((route) => route?.type === "inbound");
const outboundRoutes = (routes || []).filter((route) => route?.type === "outbound");
const outboundCustomerStatuses = outboundRoutes.reduce((acc, route) => {
const list = Array.isArray(route?.route_customer_list) ? route.route_customer_list : [];
return acc.concat(list);
}, []);
const driversMap = new Map(
(drivers || []).map((driver) => [
toObjectIdText(driver?._id || driver?.id),
driver
])
);
const vehiclesMap = new Map(
(vehicles || []).map((vehicle) => [
toObjectIdText(vehicle?._id || vehicle?.id),
vehicle
])
);
const templateBytes = fs.readFileSync(templatePath);
const filenameDate = (date || "").replace(/\//g, "-");
const zipName = `route_report_${filenameDate || "date"}.zip`;
res.setHeader("Content-Type", "application/zip");
res.setHeader("Content-Disposition", `attachment; filename="${zipName}"`);
const archive = archiver("zip", { zlib: { level: 9 } });
archive.on("error", (err) => {
if (!res.headersSent) {
res.status(500).send({ message: err.message || "Failed to generate route report zip." });
} else {
res.end();
}
});
archive.pipe(res);
const filenameCounter = new Map();
for (let i = 0; i < inboundRoutes.length; i += 1) {
const route = inboundRoutes[i];
const pdfBytes = await buildRoutePdfBuffer(
templateBytes,
route,
i + 1,
driversMap,
vehiclesMap,
outboundCustomerStatuses
);
const base = sanitizeFileName(route?.name || `route_${i + 1}`) || `route_${i + 1}`;
const existingCount = filenameCounter.get(base) || 0;
filenameCounter.set(base, existingCount + 1);
const finalName = existingCount ? `${base}_${existingCount + 1}.pdf` : `${base}.pdf`;
archive.append(Buffer.from(pdfBytes), { name: finalName });
}
archive.finalize();
} catch (err) {
res.status(500).send({
message: err.message || "Failed to export route reports."
});
}
};

View File

@@ -18,5 +18,6 @@ module.exports = app => {
// Get reports by Date and Type
router.get("/search", reports.getReportsByDateAndType);
router.get("/search-route", reports.getReportsByRouteIdAndType);
router.get("/export-route-report", [authJwt.verifyToken], reports.exportRouteReportZip);
app.use('/api/reports', router);
};

Binary file not shown.

Binary file not shown.

Binary file not shown.