This commit is contained in:
@@ -1,6 +1,122 @@
|
|||||||
const { splitSite } = require("../middlewares");
|
const { splitSite } = require("../middlewares");
|
||||||
const db = require("../models");
|
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 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) => {
|
exports.createReport = (req, res) => {
|
||||||
// Validate request
|
// Validate request
|
||||||
if (!req.body.data) {
|
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."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,5 +18,6 @@ module.exports = app => {
|
|||||||
// Get reports by Date and Type
|
// Get reports by Date and Type
|
||||||
router.get("/search", reports.getReportsByDateAndType);
|
router.get("/search", reports.getReportsByDateAndType);
|
||||||
router.get("/search-route", reports.getReportsByRouteIdAndType);
|
router.get("/search-route", reports.getReportsByRouteIdAndType);
|
||||||
|
router.get("/export-route-report", [authJwt.verifyToken], reports.exportRouteReportZip);
|
||||||
app.use('/api/reports', router);
|
app.use('/api/reports', router);
|
||||||
};
|
};
|
||||||
BIN
app/upload/pdf_templete1.pdf
Normal file
BIN
app/upload/pdf_templete1.pdf
Normal file
Binary file not shown.
BIN
app/upload/pdf_templete2.pdf
Normal file
BIN
app/upload/pdf_templete2.pdf
Normal file
Binary file not shown.
BIN
app/upload/pdf_templete3.pdf
Normal file
BIN
app/upload/pdf_templete3.pdf
Normal file
Binary file not shown.
BIN
client/build/.DS_Store
vendored
BIN
client/build/.DS_Store
vendored
Binary file not shown.
@@ -56,7 +56,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build && node scripts/copy-route-report-templates.js",
|
||||||
"test": "react-scripts test",
|
"test": "react-scripts test",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
client/public/.DS_Store
vendored
BIN
client/public/.DS_Store
vendored
Binary file not shown.
BIN
client/public/upload/pdf_templete1.pdf
Normal file
BIN
client/public/upload/pdf_templete1.pdf
Normal file
Binary file not shown.
BIN
client/public/upload/pdf_templete2.pdf
Normal file
BIN
client/public/upload/pdf_templete2.pdf
Normal file
Binary file not shown.
BIN
client/public/upload/pdf_templete3.pdf
Normal file
BIN
client/public/upload/pdf_templete3.pdf
Normal file
Binary file not shown.
22
client/scripts/copy-route-report-templates.js
Normal file
22
client/scripts/copy-route-report-templates.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const rootDir = path.resolve(__dirname, "..");
|
||||||
|
const sourceDir = path.join(rootDir, "public", "upload");
|
||||||
|
const targetDir = path.join(rootDir, "build", "upload");
|
||||||
|
const templates = ["pdf_templete1.pdf", "pdf_templete2.pdf", "pdf_templete3.pdf"];
|
||||||
|
|
||||||
|
if (!fs.existsSync(targetDir)) {
|
||||||
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
templates.forEach((templateName) => {
|
||||||
|
const sourcePath = path.join(sourceDir, templateName);
|
||||||
|
const targetPath = path.join(targetDir, templateName);
|
||||||
|
if (!fs.existsSync(sourcePath)) {
|
||||||
|
throw new Error(`Missing source template: ${sourcePath}`);
|
||||||
|
}
|
||||||
|
fs.copyFileSync(sourcePath, targetPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Route report PDF templates copied to build/upload.");
|
||||||
@@ -8,6 +8,7 @@ import { Spinner, Breadcrumb, BreadcrumbItem, Tabs, Tab, Dropdown, Modal } from
|
|||||||
import { Columns, Download, Filter, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons";
|
import { Columns, Download, Filter, PencilSquare, PersonSquare, Plus } from "react-bootstrap-icons";
|
||||||
|
|
||||||
const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, title = null }) => {
|
const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, title = null }) => {
|
||||||
|
const HIDDEN_CUSTOMER_TYPE_FILTER_VALUES = [CUSTOMER_TYPE.TRANSFERRED, CUSTOMER_TYPE.DECEASED, CUSTOMER_TYPE.DISCHARGED];
|
||||||
const CUSTOMER_LIST_COLUMN_TAB_MAP = {
|
const CUSTOMER_LIST_COLUMN_TAB_MAP = {
|
||||||
name: 'personalInfo',
|
name: 'personalInfo',
|
||||||
chinese_name: 'personalInfo',
|
chinese_name: 'personalInfo',
|
||||||
@@ -414,7 +415,7 @@ const DashboardCustomersList = ({ additionalButtons, showBreadcrumb = false, tit
|
|||||||
<div className="field-label">Customer Type</div>
|
<div className="field-label">Customer Type</div>
|
||||||
<select value={customerTypeFilter} onChange={(e) => setCustomerTypeFilter(e.currentTarget.value)}>
|
<select value={customerTypeFilter} onChange={(e) => setCustomerTypeFilter(e.currentTarget.value)}>
|
||||||
<option value="">All</option>
|
<option value="">All</option>
|
||||||
{getOptionsFromEnum(CUSTOMER_TYPE, CUSTOMER_TYPE_TEXT).map((item) => (
|
{getOptionsFromEnum(CUSTOMER_TYPE, CUSTOMER_TYPE_TEXT).filter((item) => !HIDDEN_CUSTOMER_TYPE_FILTER_VALUES.includes(item.value)).map((item) => (
|
||||||
<option key={item.value} value={item.value}>{item.label}</option>
|
<option key={item.value} value={item.value}>{item.label}</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { useNavigate, useSearchParams } from "react-router-dom";
|
|||||||
import { selectInboundRoutes, selectTomorrowAllRoutes, selectTomorrowInboundRoutes, selectTomorrowOutboundRoutes, selectHistoryInboundRoutes, selectHistoryRoutes, selectHistoryOutboundRoutes, selectOutboundRoutes, selectAllRoutes, selectAllActiveVehicles, selectAllActiveDrivers, transRoutesSlice } from "./../../store";
|
import { selectInboundRoutes, selectTomorrowAllRoutes, selectTomorrowInboundRoutes, selectTomorrowOutboundRoutes, selectHistoryInboundRoutes, selectHistoryRoutes, selectHistoryOutboundRoutes, selectOutboundRoutes, selectAllRoutes, selectAllActiveVehicles, selectAllActiveDrivers, transRoutesSlice } from "./../../store";
|
||||||
import RoutesSection from "./RoutesSection";
|
import RoutesSection from "./RoutesSection";
|
||||||
import PersonnelSection from "./PersonnelSection";
|
import PersonnelSection from "./PersonnelSection";
|
||||||
import { AuthService, CustomerService, TransRoutesService, DriverService, EventsService, DailyRoutesTemplateService } from "../../services";
|
import { AuthService, CustomerService, TransRoutesService, DriverService, EventsService, DailyRoutesTemplateService, ReportService } from "../../services";
|
||||||
import { PERSONAL_ROUTE_STATUS, ROUTE_STATUS, reportRootUrl, CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS_TEXT, PICKUP_STATUS, PICKUP_STATUS_TEXT, REPORT_TYPE } from "../../shared";
|
import { PERSONAL_ROUTE_STATUS, ROUTE_STATUS, CUSTOMER_TYPE_TEXT, PERSONAL_ROUTE_STATUS_TEXT, PICKUP_STATUS, PICKUP_STATUS_TEXT, REPORT_TYPE } from "../../shared";
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import { CalendarWeek, ClockHistory, Copy, Download, Eraser, Plus, Clock, Filter, CalendarCheck, Check } from "react-bootstrap-icons";
|
import { CalendarWeek, ClockHistory, Copy, Download, Eraser, Plus, Clock, Filter, CalendarCheck, Check } from "react-bootstrap-icons";
|
||||||
@@ -14,6 +14,7 @@ import RouteCustomerTable from "./RouteCustomerTable";
|
|||||||
|
|
||||||
|
|
||||||
const RoutesDashboard = () => {
|
const RoutesDashboard = () => {
|
||||||
|
const HIDDEN_CUSTOMER_TYPE_FILTER_VALUES = ['transferred', 'deceased', 'discharged'];
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { fetchAllRoutes, createRoute, fetchAllHistoryRoutes, fetchAllTomorrowRoutes, updateRoute } = transRoutesSlice.actions;
|
const { fetchAllRoutes, createRoute, fetchAllHistoryRoutes, fetchAllTomorrowRoutes, updateRoute } = transRoutesSlice.actions;
|
||||||
@@ -865,9 +866,24 @@ const RoutesDashboard = () => {
|
|||||||
const directToSchedule = () => {
|
const directToSchedule = () => {
|
||||||
setDateSelected(tomorrow);
|
setDateSelected(tomorrow);
|
||||||
}
|
}
|
||||||
const generateRouteReport = () => {
|
const generateRouteReport = async () => {
|
||||||
const targetDate = dateSelected || now;
|
const targetDate = dateSelected || now;
|
||||||
window.open(`${reportRootUrl}?token=${localStorage.getItem('token')}&date=${getDateString(targetDate)}`, '_blank');
|
try {
|
||||||
|
const selectedDateText = getDateString(targetDate);
|
||||||
|
const response = await ReportService.exportRouteReportZip(selectedDateText);
|
||||||
|
const blob = new Blob([response.data], { type: 'application/zip' });
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.setAttribute('download', `route_report_${selectedDateText.replace(/\//g, '-')}.zip`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(downloadUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export route report:', error);
|
||||||
|
window.alert('Failed to export route report.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const goToHistoryPage = () => {
|
const goToHistoryPage = () => {
|
||||||
navigate('/trans-routes/history');
|
navigate('/trans-routes/history');
|
||||||
@@ -1385,7 +1401,7 @@ const RoutesDashboard = () => {
|
|||||||
onChange={(e) => setCustomerTypeFilter(e.target.value)}
|
onChange={(e) => setCustomerTypeFilter(e.target.value)}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
[['', ''], ...Object.entries(CUSTOMER_TYPE_TEXT)].map(([key, text]) => (
|
[['', ''], ...Object.entries(CUSTOMER_TYPE_TEXT).filter(([key]) => !HIDDEN_CUSTOMER_TYPE_FILTER_VALUES.includes(key))].map(([key, text]) => (
|
||||||
<option key={key} value={key}>{text}</option>
|
<option key={key} value={key}>{text}</option>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { transRoutesSlice ,selectHistoryInboundRoutes, selectHistoryOutboundRout
|
|||||||
import RoutesSection from "./RoutesSection";
|
import RoutesSection from "./RoutesSection";
|
||||||
import PersonnelSection from "./PersonnelSection";
|
import PersonnelSection from "./PersonnelSection";
|
||||||
import DatePicker from "react-datepicker";
|
import DatePicker from "react-datepicker";
|
||||||
import { AuthService } from "../../services";
|
import { AuthService, ReportService } from "../../services";
|
||||||
import {reportRootUrl} from "../../shared";
|
|
||||||
|
|
||||||
const RoutesHistory = () => {
|
const RoutesHistory = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -29,8 +28,27 @@ const RoutesHistory = () => {
|
|||||||
dispatch(fetchAllHistoryRoutes({dateText: getDateString(selectedDate)}));
|
dispatch(fetchAllHistoryRoutes({dateText: getDateString(selectedDate)}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const generateRouteReport = () => {
|
const generateRouteReport = async () => {
|
||||||
window.open(`${reportRootUrl}?token=${localStorage.getItem('token')}&date=${getDateString(selectedDate)}`, '_blank');
|
if (!selectedDate) {
|
||||||
|
window.alert('Please select a date first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const selectedDateText = getDateString(selectedDate);
|
||||||
|
const response = await ReportService.exportRouteReportZip(selectedDateText);
|
||||||
|
const blob = new Blob([response.data], { type: 'application/zip' });
|
||||||
|
const downloadUrl = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = downloadUrl;
|
||||||
|
link.setAttribute('download', `route_report_${selectedDateText.replace(/\//g, '-')}.zip`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
window.URL.revokeObjectURL(downloadUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export route report:', error);
|
||||||
|
window.alert('Failed to export route report.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -33,11 +33,19 @@ const updateReport = (id, data) => {
|
|||||||
return http.put(`/reports/${id}`, data);
|
return http.put(`/reports/${id}`, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exportRouteReportZip = (date) => {
|
||||||
|
return http.get('/reports/export-route-report', {
|
||||||
|
params: { date },
|
||||||
|
responseType: 'blob'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const ReportService = {
|
export const ReportService = {
|
||||||
getReportsByDateAndType,
|
getReportsByDateAndType,
|
||||||
createReport,
|
createReport,
|
||||||
updateReport,
|
updateReport,
|
||||||
getReportsByRouteIdAndType
|
getReportsByRouteIdAndType,
|
||||||
|
exportRouteReportZip
|
||||||
};
|
};
|
||||||
|
|||||||
1172
package-lock.json
generated
1172
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
|||||||
"author": "yangli",
|
"author": "yangli",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"archiver": "^7.0.1",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"body-parser": "^1.20.0",
|
"body-parser": "^1.20.0",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"multer": "^1.4.4",
|
"multer": "^1.4.4",
|
||||||
"multer-gridfs-storage": "^5.0.2",
|
"multer-gridfs-storage": "^5.0.2",
|
||||||
"node-cron": "^4.1.0",
|
"node-cron": "^4.1.0",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
"pizzip": "^3.1.7",
|
"pizzip": "^3.1.7",
|
||||||
"xlsx-template": "^1.4.4"
|
"xlsx-template": "^1.4.4"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user