"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Alert, Autocomplete, Box, Button, CircularProgress, Grid, MenuItem, Paper, Stack, TextField, Tooltip, Typography, } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs from "dayjs"; import "dayjs/locale/zh-hk"; import * as XLSX from "xlsx-js-style"; import { CompletedDoPickOrderResponse, fetchCompletedDoPickOrdersAll, fetchCompletedDoPickOrdersWorkbenchAll, } from "@/app/api/pickOrder/actions"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { useTranslation } from "react-i18next"; type FloorFilter = "all" | "2/F" | "4/F"; type BreakdownDimension = "lane" | "shop" | "floor"; type DailySummaryRow = { date: string; floor2F: number; floor4F: number; truckX: number; total: number; }; type ShopDailyRow = DailySummaryRow & { shopCode: string; shopName: string; }; type ShopQtyRow = { code: string; name: string; qty: number; }; type BreakdownRow = { key: string; label: string; qty: number; }; type Props = { mode?: "normal" | "workbench"; }; const TRUCK_X_LANE = "車線-X"; const ALL = "all"; const CHART_ROW_H = 38; /** Jasper / report standard used across FPSMS Excel output. */ const EXCEL_FONT_NAME = "微軟正黑體"; const DAILY_SHEET_LAST_COL = 6; type FilterOption = { value: string; label: string }; function normalizeTruckLane(raw: string | null | undefined): string { const value = String(raw ?? "").trim(); return value || TRUCK_X_LANE; } function shortLaneLabel(raw: string | null | undefined): string { const value = normalizeTruckLane(raw); const stripped = value .replace(/^(車線)[-–—]?\s*/, "") .replace(/^(truck)\s*[-–—]?\s*/i, "") .trim(); return stripped || value; } function normalizeShopCode(raw: string | null | undefined): string { return String(raw ?? "").trim(); } function shopGroup(raw: string | null | undefined): string { const code = normalizeShopCode(raw).toUpperCase(); if (!code) return ""; return code.slice(0, 2); } function digitsWithoutLeadingZeros(digits: string): string { const stripped = digits.replace(/^0+/, ""); return stripped || (digits ? "0" : ""); } /** Shop names often already start with the code, sometimes zero-padded (`HP23` vs `HP023`). */ function shopNameWithoutCode(code: string, name: string | null | undefined): string { const shopName = String(name ?? "").trim(); if (!shopName) return ""; const normalized = normalizeShopCode(code).toUpperCase(); const codeParts = normalized.match(/^([A-Z]+)(\d*)$/); const nameParts = shopName.match(/^([A-Za-z]+)(\d*)(?:\s*[-–—]\s*|\s+)([\s\S]*)$/); if (codeParts && nameParts) { const sameLetters = nameParts[1].toUpperCase() === codeParts[1]; const sameDigits = !nameParts[2] || digitsWithoutLeadingZeros(nameParts[2]) === digitsWithoutLeadingZeros(codeParts[2]); if (sameLetters && sameDigits) return nameParts[3].trim(); } if (normalized && shopName.toUpperCase().startsWith(normalized)) { return shopName.slice(normalized.length).replace(/^[\s\-–—]+/, "").trim(); } return shopName; } function formatShopLabel(code: string, name: string | null | undefined): string { const normalized = normalizeShopCode(code).toUpperCase(); const displayName = shopNameWithoutCode(normalized, name); if (!normalized) return displayName; if (!displayName || displayName.toUpperCase() === normalized) return normalized; return `${normalized} ${displayName}`; } function applyProjectExcelFont(worksheet: XLSX.WorkSheet) { if (!worksheet["!ref"]) return; const range = XLSX.utils.decode_range(worksheet["!ref"]); for (let r = range.s.r; r <= range.e.r; r += 1) { for (let c = range.s.c; c <= range.e.c; c += 1) { const addr = XLSX.utils.encode_cell({ r, c }); const cell = worksheet[addr]; if (!cell) continue; const style = (cell.s ?? {}) as XLSX.CellStyle; const font = style.font ?? {}; cell.s = { ...style, font: { ...font, name: EXCEL_FONT_NAME, sz: font.sz ?? 11, }, }; } } } function buildShopQtyRows(source: CompletedDoPickOrderResponse[]): ShopQtyRow[] { const grouped = new Map(); source.forEach((record) => { const code = normalizeShopCode(record.shopCode).toUpperCase(); if (!code) return; const name = shopNameWithoutCode(code, record.shopName); const current = grouped.get(code) ?? { code, name, qty: 0 }; if (!current.name && name) current.name = name; current.qty += Number(record.numberOfCartons ?? 0); grouped.set(code, current); }); return Array.from(grouped.values()).sort( (a, b) => b.qty - a.qty || a.code.localeCompare(b.code, "zh-Hant"), ); } function isShopGroupValue(shop: string): boolean { if (shop === ALL) return false; const code = normalizeShopCode(shop).toUpperCase(); return code.length > 0 && code === shopGroup(code); } function recordMatchesShop( recordShop: string | null | undefined, shop: string, ): boolean { if (shop === ALL) return true; const code = normalizeShopCode(recordShop).toUpperCase(); if (!code) return false; const selected = normalizeShopCode(shop).toUpperCase(); if (isShopGroupValue(selected)) { return shopGroup(code) === selected; } return code === selected; } function recordMatchesFilters( record: CompletedDoPickOrderResponse, floor: FloorFilter, lane: string, shop: string, ): boolean { if (floor !== ALL && record.storeId !== floor) return false; if (lane !== ALL && normalizeTruckLane(record.truckLanceCode) !== lane) return false; if (!recordMatchesShop(record.shopCode, shop)) return false; return true; } function resolveBreakdownDimension(lane: string, shop: string): BreakdownDimension { if (shop !== ALL) return "shop"; if (lane !== ALL) return "shop"; return "lane"; } type SearchableFilterProps = { label: string; options: FilterOption[]; value: string; onChange: (next: string) => void; }; function SearchableFilterSelect({ label, options, value, onChange }: SearchableFilterProps) { const selectable = options.filter((option) => option.value !== ALL); const selected = selectable.find((option) => option.value === value) ?? null; const placeholder = options.find((option) => option.value === ALL)?.label ?? ""; return ( onChange(option?.value ?? ALL)} getOptionLabel={(option) => option?.label ?? ""} isOptionEqualToValue={(a, b) => a?.value === b?.value} selectOnFocus autoHighlight handleHomeEndKeys autoComplete includeInputInList size="small" sx={{ width: "100%" }} renderInput={(params) => ( )} /> ); } const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => { const { t, i18n } = useTranslation(); const numberLocale = i18n.language?.startsWith("zh") ? "zh-HK" : "en-US"; const [floor, setFloor] = useState(ALL); const [lane, setLane] = useState(ALL); const [shop, setShop] = useState(ALL); const [date, setDate] = useState(dayjs().format("YYYY-MM-DD")); const [loading, setLoading] = useState(false); const [isExporting, setIsExporting] = useState(false); const [isExportingFiltered, setIsExportingFiltered] = useState(false); const exportInFlightRef = useRef(false); const filteredExportInFlightRef = useRef(false); const [error, setError] = useState(""); const [records, setRecords] = useState([]); const todayDate = dayjs().format("YYYY-MM-DD"); const filtersAreActive = floor !== ALL || lane !== ALL || shop !== ALL || date !== todayDate; const resetFilters = useCallback(() => { setFloor(ALL); setLane(ALL); setShop(ALL); setDate(dayjs().format("YYYY-MM-DD")); }, []); const loadData = useCallback(async () => { setLoading(true); setError(""); try { const data = mode === "workbench" ? await fetchCompletedDoPickOrdersWorkbenchAll( date ? { targetDate: date } : undefined, ) : await fetchCompletedDoPickOrdersAll( date ? { targetDate: date } : undefined, ); setRecords(data); } catch (err) { console.error("Failed to load finished good carton dashboard data", err); setError(t("Failed to load FG carton quantity. Please try again later.")); setRecords([]); } finally { setLoading(false); } }, [date, mode, t]); useEffect(() => { loadData(); }, [loadData]); const laneOptions = useMemo(() => { const byRaw = new Map(); records.forEach((record) => { if (floor !== ALL && record.storeId !== floor) return; if (!recordMatchesShop(record.shopCode, shop)) return; const raw = normalizeTruckLane(record.truckLanceCode); if (!byRaw.has(raw)) byRaw.set(raw, shortLaneLabel(raw)); }); return Array.from(byRaw.entries()) .map(([value, label]) => ({ value, label })) .sort((a, b) => { if (a.value === TRUCK_X_LANE) return 1; if (b.value === TRUCK_X_LANE) return -1; return a.label.localeCompare(b.label, "zh-Hant"); }); }, [records, floor, shop]); const shopOptions = useMemo(() => { const groups = new Set(); records.forEach((record) => { if (floor !== ALL && record.storeId !== floor) return; if (lane !== ALL && normalizeTruckLane(record.truckLanceCode) !== lane) return; const group = shopGroup(record.shopCode); if (group) groups.add(group); }); return Array.from(groups) .sort((a, b) => a.localeCompare(b, "zh-Hant")) .map((group) => ({ value: group, label: group })); }, [records, floor, lane]); const laneFilterOptions = useMemo(() => { const options: FilterOption[] = [{ value: ALL, label: t("All lanes") }, ...laneOptions]; if (lane !== ALL && !options.some((option) => option.value === lane)) { options.splice(1, 0, { value: lane, label: shortLaneLabel(lane) }); } return options; }, [laneOptions, lane, t]); const shopFilterValue = shop === ALL ? ALL : shopGroup(shop); const shopFilterOptions = useMemo(() => { const options: FilterOption[] = [{ value: ALL, label: t("All shops") }, ...shopOptions]; if ( shopFilterValue !== ALL && !options.some((option) => option.value === shopFilterValue) ) { options.splice(1, 0, { value: shopFilterValue, label: shopFilterValue }); } return options; }, [shopOptions, shopFilterValue, t]); const filteredRecords = useMemo( () => records.filter((record) => recordMatchesFilters(record, floor, lane, shop)), [records, floor, lane, shop], ); const rows = useMemo(() => { const summary = new Map(); filteredRecords.forEach((record) => { const day = dayjs(record.deliveryDate).isValid() ? dayjs(record.deliveryDate).format("YYYY-MM-DD") : "-"; const cartonQty = Number(record.numberOfCartons ?? 0); const current = summary.get(day) ?? { date: day, floor2F: 0, floor4F: 0, truckX: 0, total: 0, }; if (record.storeId === "2/F") { current.floor2F += cartonQty; } if (record.storeId === "4/F") { current.floor4F += cartonQty; } if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) { current.truckX += cartonQty; } current.total += cartonQty; summary.set(day, current); }); return Array.from(summary.values()).sort((a, b) => b.date.localeCompare(a.date)); }, [filteredRecords]); const breakdownDimension = resolveBreakdownDimension(lane, shop); const breakdownRows = useMemo(() => { const toSorted = (slices: BreakdownRow[]) => slices.sort((a, b) => b.qty - a.qty || a.label.localeCompare(b.label, "zh-Hant")); if (breakdownDimension === "floor") { let floor2F = 0; let floor4F = 0; let truckX = 0; filteredRecords.forEach((record) => { const cartonQty = Number(record.numberOfCartons ?? 0); if (record.storeId === "2/F") floor2F += cartonQty; if (record.storeId === "4/F") floor4F += cartonQty; if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) truckX += cartonQty; }); return toSorted( [ { key: "2/F", label: "2/F", qty: floor2F }, { key: "4/F", label: "4/F", qty: floor4F }, { key: TRUCK_X_LANE, label: shortLaneLabel(TRUCK_X_LANE), qty: truckX }, ].filter((row) => { if (row.qty > 0) return true; if (row.key === "2/F" || row.key === "4/F") return floor === row.key; return lane === TRUCK_X_LANE && row.key === TRUCK_X_LANE; }), ); } const grouped = new Map(); filteredRecords.forEach((record) => { const cartonQty = Number(record.numberOfCartons ?? 0); if (breakdownDimension === "lane") { const key = normalizeTruckLane(record.truckLanceCode); const current = grouped.get(key) ?? { key, label: shortLaneLabel(key), qty: 0, }; current.qty += cartonQty; grouped.set(key, current); return; } const code = normalizeShopCode(record.shopCode).toUpperCase(); const name = String(record.shopName ?? "").trim(); const key = shop !== ALL ? code : shopGroup(record.shopCode); if (!key) return; const label = shop !== ALL ? formatShopLabel(code, name) : key; const current = grouped.get(key) ?? { key, label, qty: 0 }; if (shop !== ALL && name) current.label = formatShopLabel(code, name); current.qty += cartonQty; grouped.set(key, current); }); return toSorted( Array.from(grouped.values()).filter((row) => { if (row.qty > 0) return true; if (breakdownDimension === "shop") return shop !== ALL && row.key === shop; return lane !== ALL && row.key === lane; }), ); }, [breakdownDimension, filteredRecords, floor, lane, shop]); const breakdownCaption = breakdownDimension === "shop" ? shop !== ALL ? t("Cartons by shop") : t("Cartons by shop group") : breakdownDimension === "floor" ? t("Cartons by floor") : t("Cartons by lane"); const applyBreakdownClick = useCallback( (row: BreakdownRow) => { if (breakdownDimension === "lane") { setLane((prev) => (prev === row.key ? ALL : row.key)); return; } if (breakdownDimension === "shop") { setShop((prev) => { if (prev === row.key) { const group = shopGroup(row.key); return group && group !== row.key ? group : ALL; } return row.key; }); } }, [breakdownDimension], ); const viewingSpecificShop = shop !== ALL && !isShopGroupValue(shop); const chartClicksEnabled = !viewingSpecificShop; const chartCanGoBack = lane !== ALL || shop !== ALL; const goBackChartLevel = useCallback(() => { if (shop !== ALL && !isShopGroupValue(shop)) { setShop(shopGroup(shop) || ALL); return; } if (shop !== ALL) { setShop(ALL); return; } if (lane !== ALL) setLane(ALL); }, [lane, shop]); const isBreakdownRowActive = useCallback( (row: BreakdownRow) => { if (breakdownDimension === "lane") return lane === row.key; if (breakdownDimension === "shop") return shop === row.key; if (row.key === "2/F" || row.key === "4/F") return floor === row.key; return lane === TRUCK_X_LANE && row.key === TRUCK_X_LANE; }, [breakdownDimension, floor, lane, shop], ); const summary = useMemo(() => { return rows.reduce( (acc, row) => { acc.floor2F += row.floor2F; acc.floor4F += row.floor4F; acc.truckX += row.truckX; acc.total += row.total; return acc; }, { floor2F: 0, floor4F: 0, truckX: 0, total: 0 }, ); }, [rows]); const chartMaxQty = Math.max(1, ...breakdownRows.map((row) => row.qty)); const buildDailyRowsFromRecords = useCallback( ( sourceRecords: CompletedDoPickOrderResponse[], startDate: dayjs.Dayjs, endDate: dayjs.Dayjs, selectedFloor: FloorFilter, selectedLane: string, selectedShop: string, ): ShopDailyRow[] => { const summaryMap = new Map(); const start = startDate.startOf("day"); const end = endDate.endOf("day"); sourceRecords.forEach((record) => { if (!recordMatchesFilters(record, selectedFloor, selectedLane, selectedShop)) { return; } const deliveryDay = dayjs(record.deliveryDate, ["YYYY-MM-DD", "YYYYMMDD"], true); if (!deliveryDay.isValid() || deliveryDay.isBefore(start) || deliveryDay.isAfter(end)) { return; } const dayKey = deliveryDay.format("YYYY-MM-DD"); const shopCode = normalizeShopCode(record.shopCode).toUpperCase() || "-"; const shopName = shopNameWithoutCode(shopCode, record.shopName); const mapKey = `${dayKey}|${shopCode}`; const cartonQty = Number(record.numberOfCartons ?? 0); const current = summaryMap.get(mapKey) ?? { date: dayKey, shopCode, shopName, floor2F: 0, floor4F: 0, truckX: 0, total: 0, }; if (!current.shopName && shopName) current.shopName = shopName; if (record.storeId === "2/F") current.floor2F += cartonQty; if (record.storeId === "4/F") current.floor4F += cartonQty; if (normalizeTruckLane(record.truckLanceCode) === TRUCK_X_LANE) current.truckX += cartonQty; current.total += cartonQty; summaryMap.set(mapKey, current); }); return Array.from(summaryMap.values()).sort( (a, b) => a.date.localeCompare(b.date) || a.shopCode.localeCompare(b.shopCode, "zh-Hant"), ); }, [], ); const calcSummary = useCallback((dailyRows: DailySummaryRow[]) => { return dailyRows.reduce( (acc, row) => { acc.floor2F += row.floor2F; acc.floor4F += row.floor4F; acc.truckX += row.truckX; acc.total += row.total; return acc; }, { floor2F: 0, floor4F: 0, truckX: 0, total: 0 }, ); }, []); const styleWorksheet = useCallback((worksheet: XLSX.WorkSheet, dataRowsCount: number) => { const summaryTitleRow = 4 + dataRowsCount; const summaryStartRow = 5 + dataRowsCount; worksheet["!cols"] = [ { wch: 14 }, { wch: 14 }, { wch: 28 }, { wch: 16 }, { wch: 16 }, { wch: 18 }, { wch: 14 }, ]; worksheet["!merges"] = [ { s: { r: 0, c: 0 }, e: { r: 0, c: DAILY_SHEET_LAST_COL } }, { s: { r: summaryTitleRow, c: 0 }, e: { r: summaryTitleRow, c: DAILY_SHEET_LAST_COL } }, ]; const titleStyle = { font: { bold: true, sz: 14, color: { rgb: "1F2D3D" } }, alignment: { horizontal: "center", vertical: "center" }, fill: { fgColor: { rgb: "EAF3FF" } }, }; const headerStyle = { font: { bold: true, color: { rgb: "FFFFFF" } }, fill: { fgColor: { rgb: "1976D2" } }, alignment: { horizontal: "center", vertical: "center" }, border: { top: { style: "thin", color: { rgb: "B0BEC5" } }, bottom: { style: "thin", color: { rgb: "B0BEC5" } }, left: { style: "thin", color: { rgb: "B0BEC5" } }, right: { style: "thin", color: { rgb: "B0BEC5" } }, }, }; const cellStyle = { alignment: { vertical: "center" }, border: { top: { style: "thin", color: { rgb: "D0D7DE" } }, bottom: { style: "thin", color: { rgb: "D0D7DE" } }, left: { style: "thin", color: { rgb: "D0D7DE" } }, right: { style: "thin", color: { rgb: "D0D7DE" } }, }, }; const numberStyle = { ...cellStyle, alignment: { horizontal: "right", vertical: "center" }, numFmt: "#,##0", }; const summaryTitleStyle = { font: { bold: true, color: { rgb: "1F2D3D" } }, fill: { fgColor: { rgb: "F1F8E9" } }, alignment: { horizontal: "left", vertical: "center" }, }; for (let c = 0; c <= DAILY_SHEET_LAST_COL; c += 1) { const headerCell = XLSX.utils.encode_cell({ r: 2, c }); if (worksheet[headerCell]) worksheet[headerCell].s = headerStyle; } for (let r = 3; r < 3 + dataRowsCount; r += 1) { for (let c = 0; c <= DAILY_SHEET_LAST_COL; c += 1) { const addr = XLSX.utils.encode_cell({ r, c }); if (!worksheet[addr]) continue; worksheet[addr].s = c < 3 ? cellStyle : numberStyle; } } for (let r = summaryStartRow; r <= summaryStartRow + 3; r += 1) { for (let c = 0; c <= DAILY_SHEET_LAST_COL; c += 1) { const addr = XLSX.utils.encode_cell({ r, c }); const cell = worksheet[addr]; if (!cell) continue; cell.s = typeof cell.v === "number" ? numberStyle : cellStyle; } } if (worksheet["A1"]) worksheet["A1"].s = titleStyle; const summaryTitleAddr = XLSX.utils.encode_cell({ r: summaryTitleRow, c: 0 }); if (worksheet[summaryTitleAddr]) worksheet[summaryTitleAddr].s = summaryTitleStyle; applyProjectExcelFont(worksheet); }, []); const addReportSheet = useCallback( ( workbook: XLSX.WorkBook, sheetName: string, reportTitle: string, dailyRows: ShopDailyRow[], ) => { const reportSummary = calcSummary(dailyRows); const blank = ["", "", "", "", "", "", ""]; const aoa: (string | number)[][] = [ [reportTitle, ...blank.slice(1)], [...blank], [ t("Date"), t("Shop code"), t("Shop Name"), t("2/F carton qty"), t("4/F carton qty"), t("Truck X carton qty"), t("Total carton qty"), ], ...dailyRows.map((row) => [ row.date, row.shopCode, row.shopName, row.floor2F, row.floor4F, row.truckX, row.total, ]), [...blank], [t("Summary"), ...blank.slice(1)], [t("2/F carton qty"), "", "", reportSummary.floor2F, "", "", ""], [t("4/F carton qty"), "", "", "", reportSummary.floor4F, "", ""], [t("Truck X carton qty"), "", "", "", "", reportSummary.truckX, ""], [t("Total carton qty"), "", "", "", "", "", reportSummary.total], ]; const worksheet = XLSX.utils.aoa_to_sheet(aoa); styleWorksheet(worksheet, dailyRows.length); XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); }, [calcSummary, styleWorksheet, t], ); const addBreakdownSheet = useCallback( ( workbook: XLSX.WorkBook, sheetName: string, reportTitle: string, shops: ShopQtyRow[], totalQty: number, ) => { const aoa: (string | number)[][] = [ [reportTitle, "", "", ""], ["", "", "", ""], [t("Shop code"), t("Shop Name"), t("Cartons"), t("Share")], ...shops.map((row) => { const share = totalQty > 0 ? (row.qty / totalQty) * 100 : 0; return [row.code, row.name, row.qty, `${share.toFixed(1)}%`]; }), ["", "", "", ""], [t("Total carton qty"), "", totalQty, ""], ]; const worksheet = XLSX.utils.aoa_to_sheet(aoa); worksheet["!cols"] = [{ wch: 14 }, { wch: 28 }, { wch: 14 }, { wch: 12 }]; worksheet["!merges"] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 3 } }]; const titleStyle = { font: { bold: true, sz: 14, color: { rgb: "1F2D3D" } }, alignment: { horizontal: "center", vertical: "center" }, fill: { fgColor: { rgb: "EAF3FF" } }, }; const headerStyle = { font: { bold: true, color: { rgb: "FFFFFF" } }, fill: { fgColor: { rgb: "1976D2" } }, alignment: { horizontal: "center", vertical: "center" }, }; const cellStyle = { alignment: { vertical: "center" }, border: { top: { style: "thin", color: { rgb: "D0D7DE" } }, bottom: { style: "thin", color: { rgb: "D0D7DE" } }, left: { style: "thin", color: { rgb: "D0D7DE" } }, right: { style: "thin", color: { rgb: "D0D7DE" } }, }, }; const numberStyle = { ...cellStyle, alignment: { horizontal: "right", vertical: "center" }, numFmt: "#,##0", }; if (worksheet["A1"]) worksheet["A1"].s = titleStyle; for (let c = 0; c <= 3; c += 1) { const headerCell = XLSX.utils.encode_cell({ r: 2, c }); if (worksheet[headerCell]) worksheet[headerCell].s = headerStyle; } shops.forEach((_, index) => { const r = 3 + index; const codeAddr = XLSX.utils.encode_cell({ r, c: 0 }); const nameAddr = XLSX.utils.encode_cell({ r, c: 1 }); const qtyAddr = XLSX.utils.encode_cell({ r, c: 2 }); const shareAddr = XLSX.utils.encode_cell({ r, c: 3 }); if (worksheet[codeAddr]) worksheet[codeAddr].s = cellStyle; if (worksheet[nameAddr]) worksheet[nameAddr].s = cellStyle; if (worksheet[qtyAddr]) worksheet[qtyAddr].s = numberStyle; if (worksheet[shareAddr]) worksheet[shareAddr].s = cellStyle; }); const totalRow = 4 + shops.length; const totalLabelAddr = XLSX.utils.encode_cell({ r: totalRow, c: 0 }); const totalValueAddr = XLSX.utils.encode_cell({ r: totalRow, c: 2 }); if (worksheet[totalLabelAddr]) worksheet[totalLabelAddr].s = cellStyle; if (worksheet[totalValueAddr]) worksheet[totalValueAddr].s = numberStyle; applyProjectExcelFont(worksheet); XLSX.utils.book_append_sheet(workbook, worksheet, sheetName.slice(0, 31)); }, [t], ); const handleDownloadExcel = useCallback(async () => { if (exportInFlightRef.current || filteredExportInFlightRef.current) return; exportInFlightRef.current = true; setIsExporting(true); try { const allRecords = mode === "workbench" ? await fetchCompletedDoPickOrdersWorkbenchAll() : await fetchCompletedDoPickOrdersAll(); const baseDate = dayjs(date || undefined).isValid() ? dayjs(date) : dayjs(); const floorLabel = floor === ALL ? t("All floors") : floor; const laneLabel = lane === ALL ? t("All lanes") : lane; const shopLabel = shop === ALL ? t("All shops") : shop; const dateLabel = baseDate.format("YYYY-MM-DD"); const monthPeriod = i18n.language?.startsWith("zh") ? baseDate.format("YYYY年MM月") : baseDate.format("YYYY-MM"); const yearPeriod = i18n.language?.startsWith("zh") ? baseDate.format("YYYY年") : baseDate.format("YYYY"); const last7Rows = buildDailyRowsFromRecords( allRecords, baseDate.subtract(6, "day"), baseDate, floor, lane, shop, ); const monthRows = buildDailyRowsFromRecords( allRecords, baseDate.startOf("month"), baseDate.endOf("month"), floor, lane, shop, ); const yearRows = buildDailyRowsFromRecords( allRecords, baseDate.startOf("year"), baseDate.endOf("year"), floor, lane, shop, ); const workbook = XLSX.utils.book_new(); addReportSheet( workbook, t("Last 7 days"), t("FG carton qty last 7 days title", { floor: floorLabel, date: dateLabel }), last7Rows, ); addReportSheet( workbook, t("This month"), t("FG carton qty this month title", { floor: floorLabel, period: monthPeriod }), monthRows, ); addReportSheet( workbook, t("This year"), t("FG carton qty this year title", { floor: floorLabel, period: yearPeriod }), yearRows, ); const fileBits = [ "FG_carton_qty", floorLabel.replace("/", ""), lane === ALL ? "" : laneLabel, shop === ALL ? "" : shopLabel, dateLabel, ].filter(Boolean); XLSX.writeFile(workbook, `${fileBits.join("_")}.xlsx`); } finally { setIsExporting(false); exportInFlightRef.current = false; } }, [mode, date, floor, lane, shop, buildDailyRowsFromRecords, addReportSheet, t, i18n.language]); const handleDownloadFilteredExcel = useCallback(async () => { if (filteredExportInFlightRef.current || exportInFlightRef.current) return; filteredExportInFlightRef.current = true; setIsExportingFiltered(true); try { const baseDate = dayjs(date || undefined).isValid() ? dayjs(date) : dayjs(); const floorLabel = floor === ALL ? t("All floors") : floor; const laneLabel = lane === ALL ? t("All lanes") : shortLaneLabel(lane); const shopLabel = shop === ALL ? t("All shops") : isShopGroupValue(shop) ? shop : formatShopLabel(shop, ""); const dateLabel = baseDate.format("YYYY-MM-DD"); const reportTitle = t("FG carton qty filtered title", { floor: floorLabel, lane: laneLabel, shop: shopLabel, date: dateLabel, }); const dailyRows = buildDailyRowsFromRecords( records, baseDate.startOf("day"), baseDate.endOf("day"), floor, lane, shop, ); const filteredTotal = calcSummary(dailyRows).total; const shopRows = buildShopQtyRows( records.filter((record) => recordMatchesFilters(record, floor, lane, shop)), ); const workbook = XLSX.utils.book_new(); addReportSheet(workbook, t("Filtered"), reportTitle, dailyRows); addBreakdownSheet( workbook, t("Breakdown"), t("Cartons by shop"), shopRows, filteredTotal, ); const fileBits = [ "FG_carton_qty_filtered", floorLabel.replace("/", ""), lane === ALL ? "" : laneLabel, shop === ALL ? "" : shopLabel, dateLabel, ].filter(Boolean); XLSX.writeFile(workbook, `${fileBits.join("_")}.xlsx`); } finally { setIsExportingFiltered(false); filteredExportInFlightRef.current = false; } }, [ date, floor, lane, shop, records, buildDailyRowsFromRecords, addReportSheet, addBreakdownSheet, calcSummary, t, ]); const isAnyExporting = isExporting || isExportingFiltered; return ( {t("FG Carton Qty")} {error && ( {error} )} setFloor(event.target.value as FloorFilter)} > {t("All")} 2/F 4/F { if (newValue && dayjs(newValue).isValid()) { setDate(dayjs(newValue).format(OUTPUT_DATE_FORMAT)); } }} slotProps={{ textField: { size: "small", fullWidth: true }, }} /> {loading ? ( ) : ( {[ { label: t("2/F carton qty"), value: summary.floor2F }, { label: t("4/F carton qty"), value: summary.floor4F }, { label: t("Truck X carton qty"), value: summary.truckX }, { label: t("Total carton qty"), value: summary.total }, ].map((kpi, index) => ( {kpi.label} {kpi.value.toLocaleString(numberLocale)} ))} {breakdownCaption} {chartClicksEnabled && ( {t("Click a row to filter")} )} {breakdownRows.length === 0 ? ( {t("No data available")} ) : ( {breakdownRows.map((row) => { const active = isBreakdownRowActive(row); const share = summary.total > 0 ? (row.qty / summary.total) * 100 : 0; const barWidth = row.qty <= 0 ? "0%" : `${(row.qty / chartMaxQty) * 100}%`; return ( { if (chartClicksEnabled) applyBreakdownClick(row); }} onKeyDown={(event) => { if (!chartClicksEnabled) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); applyBreakdownClick(row); } }} title={`${row.label}: ${row.qty.toLocaleString(numberLocale)} (${share.toFixed(1)}%)`} sx={{ height: CHART_ROW_H, display: "grid", gridTemplateColumns: { xs: "132px minmax(0, 1fr) 84px", sm: "200px minmax(0, 1fr) 84px", md: "280px minmax(0, 1fr) 84px", }, columnGap: 1, alignItems: "center", minWidth: 0, cursor: chartClicksEnabled ? "pointer" : "default", borderRadius: 1, px: 0.5, bgcolor: active ? "action.selected" : "transparent", "&:hover": chartClicksEnabled ? { bgcolor: "action.hover" } : undefined, }} > {row.label} 0 ? 4 : 0, bgcolor: "#1976d2", borderRadius: "4px", }} /> {row.qty.toLocaleString(numberLocale)} ); })} )} )} ); }; export default FinishedGoodCartonDashboardTab;