All checks were successful
Build And Deploy Main / build-and-deploy (push) Successful in 40s
389 lines
15 KiB
JavaScript
389 lines
15 KiB
JavaScript
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 fontkit = require("@pdf-lib/fontkit");
|
|
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 cwd = process.cwd();
|
|
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),
|
|
path.join(cwd, "app", "views", "upload", fileName),
|
|
path.join(cwd, "client", "build", "upload", fileName),
|
|
path.join(cwd, "client", "public", "upload", fileName),
|
|
path.join("/www/wwwroot/upload", fileName),
|
|
path.join(`/www/wwwroot/worldshine${safeSite}`, "app", "views", "upload", fileName),
|
|
path.join(`/www/wwwroot/worldshine${safeSite}-tspt`, "app", "views", "upload", fileName)
|
|
];
|
|
return candidatePaths.find((candidate) => fs.existsSync(candidate)) || "";
|
|
};
|
|
|
|
const findUnicodeFontPath = (site) => {
|
|
const safeSite = [1, 2, 3].includes(Number(site)) ? Number(site) : 1;
|
|
const primaryFileName = "NotoSansSC-Regular.ttf";
|
|
const secondaryFileName = "NotoSansSC-VF.ttf";
|
|
const fallbackFileName = "NotoSansCJKsc-Regular.otf";
|
|
const cwd = process.cwd();
|
|
const candidatePaths = [
|
|
path.join(ROOT_DIR, "app", "assets", "fonts", primaryFileName),
|
|
path.join(ROOT_DIR, "app", "views", "upload", primaryFileName),
|
|
path.join(ROOT_DIR, "client", "build", "upload", primaryFileName),
|
|
path.join(ROOT_DIR, "client", "public", "upload", primaryFileName),
|
|
path.join(cwd, "app", "assets", "fonts", primaryFileName),
|
|
path.join(cwd, "app", "views", "upload", primaryFileName),
|
|
path.join(cwd, "client", "build", "upload", primaryFileName),
|
|
path.join(cwd, "client", "public", "upload", primaryFileName),
|
|
path.join("/www/wwwroot/upload", primaryFileName),
|
|
path.join(`/www/wwwroot/worldshine${safeSite}`, "app", "assets", "fonts", primaryFileName),
|
|
path.join(`/www/wwwroot/worldshine${safeSite}-tspt`, "app", "assets", "fonts", primaryFileName),
|
|
path.join(ROOT_DIR, "app", "assets", "fonts", secondaryFileName),
|
|
path.join(ROOT_DIR, "app", "views", "upload", secondaryFileName),
|
|
path.join(ROOT_DIR, "client", "build", "upload", secondaryFileName),
|
|
path.join(ROOT_DIR, "client", "public", "upload", secondaryFileName),
|
|
path.join(cwd, "app", "assets", "fonts", secondaryFileName),
|
|
path.join(cwd, "app", "views", "upload", secondaryFileName),
|
|
path.join(cwd, "client", "build", "upload", secondaryFileName),
|
|
path.join(cwd, "client", "public", "upload", secondaryFileName),
|
|
path.join("/www/wwwroot/upload", secondaryFileName),
|
|
path.join(`/www/wwwroot/worldshine${safeSite}`, "app", "assets", "fonts", secondaryFileName),
|
|
path.join(`/www/wwwroot/worldshine${safeSite}-tspt`, "app", "assets", "fonts", secondaryFileName),
|
|
path.join(ROOT_DIR, "app", "assets", "fonts", fallbackFileName),
|
|
path.join(ROOT_DIR, "app", "views", "upload", fallbackFileName),
|
|
path.join(ROOT_DIR, "client", "build", "upload", fallbackFileName),
|
|
path.join(ROOT_DIR, "client", "public", "upload", fallbackFileName),
|
|
path.join(cwd, "app", "assets", "fonts", fallbackFileName),
|
|
path.join(cwd, "app", "views", "upload", fallbackFileName),
|
|
path.join(cwd, "client", "build", "upload", fallbackFileName),
|
|
path.join(cwd, "client", "public", "upload", fallbackFileName),
|
|
path.join("/www/wwwroot/upload", fallbackFileName),
|
|
path.join(`/www/wwwroot/worldshine${safeSite}`, "app", "assets", "fonts", fallbackFileName),
|
|
path.join(`/www/wwwroot/worldshine${safeSite}-tspt`, "app", "assets", "fonts", fallbackFileName)
|
|
];
|
|
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, options = {}) => {
|
|
if (!fieldName || value === undefined || value === null) return;
|
|
const text = `${value}`;
|
|
try {
|
|
const field = form.getTextField(fieldName);
|
|
if (typeof options.fontSize === "number") {
|
|
field.setFontSize(options.fontSize);
|
|
}
|
|
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, unicodeFontBytes) => {
|
|
const pdfDoc = await PDFDocument.load(templateBytes);
|
|
pdfDoc.registerFontkit(fontkit);
|
|
const unicodeFont = await pdfDoc.embedFont(unicodeFontBytes, { subset: true });
|
|
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 || "");
|
|
const customerNoteText = customer?.customer_note || customer?.customer_special_needs || "";
|
|
safeSetField(form, `note_${row}`, customerNoteText);
|
|
|
|
const pickupTime = formatUtcToLocalHm(customer?.customer_pickup_time);
|
|
if (pickupTime) safeSetField(form, `pick_${row}`, pickupTime);
|
|
|
|
const hasEnterCenterTime = customer?.customer_enter_center_time !== undefined && customer?.customer_enter_center_time !== null;
|
|
if (hasEnterCenterTime) {
|
|
const enterCenterTime = formatUtcToLocalHm(customer?.customer_enter_center_time);
|
|
if (enterCenterTime) {
|
|
safeSetField(form, `arrive_${row}`, enterCenterTime);
|
|
}
|
|
safeSetField(form, `y_${row}`, "✓", { fontSize: 11 });
|
|
safeSetField(form, `n_${row}`, "", { fontSize: 11 });
|
|
} else if (customer?.customer_route_status === "inCenter") {
|
|
safeSetField(form, `y_${row}`, "✓", { fontSize: 11 });
|
|
safeSetField(form, `n_${row}`, "", { fontSize: 11 });
|
|
} else {
|
|
safeSetField(form, `y_${row}`, "", { fontSize: 11 });
|
|
safeSetField(form, `n_${row}`, "✕", { fontSize: 11 });
|
|
}
|
|
|
|
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);
|
|
});
|
|
|
|
// Rebuild form appearances with a Unicode font so UTF-8 (e.g., Chinese) renders correctly.
|
|
form.updateFieldAppearances(unicodeFont);
|
|
form.flatten();
|
|
return pdfDoc.save();
|
|
};
|
|
exports.createReport = (req, res) => {
|
|
// Validate request
|
|
if (!req.body.data) {
|
|
res.status(400).send({ message: "Content can not be empty!" });
|
|
return;
|
|
}
|
|
const site = splitSite.findSiteNumber(req);
|
|
// Create a Report
|
|
const report = new Report({
|
|
date: req.body.date || '',
|
|
type: req.body.type || '',
|
|
route_id: req.body.route_id || '',
|
|
driver_name: req.body.driver_name || '',
|
|
route_name: req.body.route_name || '',
|
|
data: req.body.data || [],
|
|
head: req.body.head || [],
|
|
chinese_head: req.body.chinese_head || [],
|
|
checklist_result: req.body.checklist_result || [],
|
|
vehicle_number: req.body.vehicle_number || null,
|
|
site
|
|
});
|
|
|
|
// Save Report in the database
|
|
report
|
|
.save(report)
|
|
.then(data => {
|
|
res.send(data);
|
|
})
|
|
.catch(err => {
|
|
res.status(500).send({
|
|
message:
|
|
err.message || "Some error occurred while creating the Report."
|
|
});
|
|
});
|
|
};
|
|
// Retrieve all Reports from the database.
|
|
exports.getAllReports = (req, res) => {
|
|
var condition = {};
|
|
condition = splitSite.splitSiteGet(req, condition);
|
|
Report.find(condition)
|
|
.then(data => {
|
|
res.send(data);
|
|
})
|
|
.catch(err => {
|
|
res.status(500).send({
|
|
message:
|
|
err.message || "Some error occurred while retrieving reports."
|
|
});
|
|
});
|
|
};
|
|
// Retrieve all Active Reports By Date and Type (Admin Reports).
|
|
exports.getReportsByDateAndType = (req, res) => {
|
|
const params = req.query;
|
|
const date = params?.date;
|
|
const type = params?.type;
|
|
var condition = { date, type };
|
|
condition = splitSite.splitSiteGet(req, condition);
|
|
Report.find(condition)
|
|
.then(data => {
|
|
res.send(data);
|
|
})
|
|
.catch(err => {
|
|
res.status(500).send({
|
|
message:
|
|
err.message || "Some error occurred while retrieving Reports with Date and Type."
|
|
});
|
|
});
|
|
};
|
|
// Retrieve reports By RouteId and Type (Senior Route report)
|
|
exports.getReportsByRouteIdAndType = (req, res) => {
|
|
const params = req.query;
|
|
const route_id = params?.route_id;
|
|
const type = params?.type;
|
|
var condition = { route_id, type };
|
|
condition = splitSite.splitSiteGet(req, condition);
|
|
Report.find(condition)
|
|
.then(data => {
|
|
res.send(data);
|
|
})
|
|
.catch(err => {
|
|
res.status(500).send({
|
|
message:
|
|
err.message || "Some error occurred while retrieving Reports with Date and Type."
|
|
});
|
|
});
|
|
};
|
|
|
|
// Get One Report by Id
|
|
exports.getReport = (req, res) => {
|
|
|
|
};
|
|
// Update a Report by the id in the request
|
|
exports.updateReport = (req, res) => {
|
|
if (!req.body) {
|
|
return res.status(400).send({
|
|
message: "Data to update can not be empty!"
|
|
});
|
|
}
|
|
const id = req.params.id;
|
|
Report.findByIdAndUpdate(id, req.body, { useFindAndModify: false })
|
|
.then(data => {
|
|
if (!data) {
|
|
res.status(404).send({
|
|
message: `Cannot update Report with id=${id}. Maybe Report was not found!`
|
|
});
|
|
} else res.send({ success: true, message: "Report was updated successfully." });
|
|
})
|
|
.catch(err => {
|
|
res.status(500).send({
|
|
success: false,
|
|
message: "Error updating Report with id=" + id
|
|
});
|
|
});
|
|
};
|
|
|
|
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);
|
|
const unicodeFontPath = findUnicodeFontPath(site);
|
|
if (!templatePath) {
|
|
return res.status(500).send({ message: `Missing PDF template for site ${site}.` });
|
|
}
|
|
if (!unicodeFontPath) {
|
|
return res.status(500).send({ message: "Missing Unicode font file: NotoSansSC-Regular.ttf" });
|
|
}
|
|
|
|
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 unicodeFontBytes = fs.readFileSync(unicodeFontPath);
|
|
|
|
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,
|
|
unicodeFontBytes
|
|
);
|
|
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) {
|
|
console.error("Failed to export route report zip:", err);
|
|
res.status(500).send({
|
|
message: err.message || "Failed to export route reports."
|
|
});
|
|
}
|
|
};
|
|
|