From 759df0b37966b39853bd5c61db9e5b3a24fcae4a Mon Sep 17 00:00:00 2001 From: Harry Groves Date: Tue, 22 Sep 2026 17:40:00 +0800 Subject: [PATCH] [Fix] Amend few visual issues --- .../DoWorkbench/DoWorkbenchTabs.tsx | 12 +- .../FinishedGoodCartonDashboardTab.tsx | 604 ++++++++++-------- src/i18n/en/doWorkbench.json | 4 +- src/i18n/en/pickOrder.json | 4 +- src/i18n/zh/doWorkbench.json | 4 +- src/i18n/zh/pickOrder.json | 4 +- 6 files changed, 356 insertions(+), 276 deletions(-) diff --git a/src/components/DoWorkbench/DoWorkbenchTabs.tsx b/src/components/DoWorkbench/DoWorkbenchTabs.tsx index c247d87b..87a1eb2a 100644 --- a/src/components/DoWorkbench/DoWorkbenchTabs.tsx +++ b/src/components/DoWorkbench/DoWorkbenchTabs.tsx @@ -282,7 +282,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom sx={{ width: "100%", maxWidth: "100%", - minHeight: 56, + minHeight: 48, borderBottom: 1, borderColor: "divider", "& .MuiTabs-flexContainer": { @@ -291,12 +291,12 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom /* 否則 Tab 內 overflow:hidden 會把 Badge 數字裁成紅點 */ "& .MuiTab-root": { overflow: "visible", - minHeight: 56, + minHeight: 48, minWidth: 72, - maxWidth: 120, - px: 1, + maxWidth: "none", + px: 1.5, py: 0.75, - whiteSpace: "normal", + whiteSpace: "nowrap", lineHeight: 1.2, textAlign: "center", }, @@ -342,7 +342,7 @@ const DoWorkbenchTabsInner: React.FC = ({ defaultTabIndex = 0, printerCom variant="inherit" sx={{ pr: etraIncompleteDopoCount > 0 ? 1 : 0, - whiteSpace: "normal", + whiteSpace: "nowrap", lineHeight: 1.2, textAlign: "center", }} diff --git a/src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx b/src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx index ba895bdb..5f74c65a 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx @@ -15,15 +15,18 @@ import { Tooltip, Typography, } from "@mui/material"; -import type { ApexOptions } from "apexcharts"; +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 SafeApexCharts from "@/components/charts/SafeApexCharts"; +import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { useTranslation } from "react-i18next"; type FloorFilter = "all" | "2/F" | "4/F"; @@ -37,6 +40,17 @@ type DailySummaryRow = { total: number; }; +type ShopDailyRow = DailySummaryRow & { + shopCode: string; + shopName: string; +}; + +type ShopQtyRow = { + code: string; + name: string; + qty: number; +}; + type BreakdownRow = { key: string; label: string; @@ -50,6 +64,9 @@ type Props = { 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 }; @@ -61,7 +78,7 @@ function normalizeTruckLane(raw: string | null | undefined): string { function shortLaneLabel(raw: string | null | undefined): string { const value = normalizeTruckLane(raw); const stripped = value - .replace(/^(車線)[-–—]?\s*/u, "") + .replace(/^(車線)[-–—]?\s*/, "") .replace(/^(truck)\s*[-–—]?\s*/i, "") .trim(); return stripped || value; @@ -77,6 +94,77 @@ function shopGroup(raw: string | null | undefined): string { 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(); @@ -110,10 +198,8 @@ function recordMatchesFilters( } function resolveBreakdownDimension(lane: string, shop: string): BreakdownDimension { - if (isShopGroupValue(shop)) return "shop"; - if (shop !== ALL && lane !== ALL) return "floor"; + if (shop !== ALL) return "shop"; if (lane !== ALL) return "shop"; - if (shop !== ALL) return "lane"; return "lane"; } @@ -239,13 +325,18 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => 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 (shop !== ALL && !options.some((option) => option.value === shop)) { - options.splice(1, 0, { value: shop, label: shop }); + if ( + shopFilterValue !== ALL && + !options.some((option) => option.value === shopFilterValue) + ) { + options.splice(1, 0, { value: shopFilterValue, label: shopFilterValue }); } return options; - }, [shopOptions, shop, t]); + }, [shopOptions, shopFilterValue, t]); const filteredRecords = useMemo( () => records.filter((record) => recordMatchesFilters(record, floor, lane, shop)), @@ -330,12 +421,13 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => return; } - const key = - shop !== ALL - ? normalizeShopCode(record.shopCode).toUpperCase() - : shopGroup(record.shopCode); + const code = normalizeShopCode(record.shopCode).toUpperCase(); + const name = String(record.shopName ?? "").trim(); + const key = shop !== ALL ? code : shopGroup(record.shopCode); if (!key) return; - const current = grouped.get(key) ?? { key, label: key, qty: 0 }; + 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); }); @@ -372,19 +464,27 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => } return row.key; }); - return; - } - if (row.key === "2/F" || row.key === "4/F") { - setFloor((prev) => (prev === row.key ? ALL : row.key)); - return; - } - if (row.key === TRUCK_X_LANE) { - setLane((prev) => (prev === TRUCK_X_LANE ? ALL : TRUCK_X_LANE)); } }, [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; @@ -409,111 +509,6 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => }, [rows]); const chartMaxQty = Math.max(1, ...breakdownRows.map((row) => row.qty)); - const chartHeight = Math.max(CHART_ROW_H, breakdownRows.length * CHART_ROW_H); - - const chartOptions = useMemo( - () => ({ - chart: { - type: "bar", - toolbar: { show: false }, - sparkline: { enabled: false }, - parentHeightOffset: 0, - offsetY: 0, - animations: { - enabled: true, - easing: "easeinout", - speed: 450, - animateGradually: { enabled: true, delay: 40 }, - }, - selection: { enabled: false }, - zoom: { enabled: false }, - }, - states: { - hover: { filter: { type: "none" } }, - active: { filter: { type: "none" } }, - }, - colors: ["#1976d2"], - dataLabels: { - enabled: true, - formatter: (val) => Number(val || 0).toLocaleString(numberLocale), - offsetX: 12, - textAnchor: "start", - style: { - fontSize: "15px", - fontWeight: 800, - colors: ["#ffffff"], - }, - background: { - enabled: true, - foreColor: "#102a43", - padding: 6, - borderRadius: 4, - opacity: 1, - borderWidth: 1, - borderColor: "#90a4ae", - dropShadow: { - enabled: true, - color: "#000000", - top: 1, - left: 0, - blur: 3, - opacity: 0.22, - }, - }, - }, - stroke: { show: true, width: 1, colors: ["transparent"] }, - plotOptions: { - bar: { - horizontal: true, - borderRadius: 4, - barHeight: 22, - dataLabels: { - position: "top", - }, - }, - }, - grid: { - show: true, - borderColor: "#eceff1", - padding: { top: -8, bottom: -8, left: 4, right: 64 }, - xaxis: { lines: { show: true } }, - yaxis: { lines: { show: false } }, - }, - xaxis: { - categories: breakdownRows.map((row) => row.label), - labels: { show: false }, - axisBorder: { show: false }, - axisTicks: { show: false }, - min: 0, - max: Math.max(chartMaxQty * 1.4, chartMaxQty + 12), - }, - yaxis: { - labels: { show: false }, - }, - tooltip: { - x: { - formatter: (_val, opts) => - breakdownRows[opts?.dataPointIndex ?? -1]?.label ?? String(_val ?? ""), - }, - y: { - formatter: (val, opts) => { - const qty = Number(val || 0); - const share = summary.total > 0 ? (qty / summary.total) * 100 : 0; - const label = breakdownRows[opts?.dataPointIndex ?? -1]?.label ?? ""; - return `${label}: ${qty.toLocaleString(numberLocale)} (${share.toFixed(1)}%)`; - }, - }, - }, - legend: { show: false }, - noData: { text: t("No chart data") }, - }), - [breakdownRows, chartMaxQty, numberLocale, summary.total, t], - ); - - const chartSeries = useMemo( - () => [{ name: t("Cartons"), data: breakdownRows.map((row) => row.qty) }], - [breakdownRows, t], - ); const buildDailyRowsFromRecords = useCallback( ( @@ -523,8 +518,8 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => selectedFloor: FloorFilter, selectedLane: string, selectedShop: string, - ): DailySummaryRow[] => { - const summaryMap = new Map(); + ): ShopDailyRow[] => { + const summaryMap = new Map(); const start = startDate.startOf("day"); const end = endDate.endOf("day"); @@ -539,24 +534,33 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => } 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(dayKey) ?? { + 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(dayKey, current); + summaryMap.set(mapKey, current); }); - return Array.from(summaryMap.values()).sort((a, b) => a.date.localeCompare(b.date)); + return Array.from(summaryMap.values()).sort( + (a, b) => + a.date.localeCompare(b.date) || a.shopCode.localeCompare(b.shopCode, "zh-Hant"), + ); }, [], ); @@ -578,10 +582,18 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => const summaryTitleRow = 4 + dataRowsCount; const summaryStartRow = 5 + dataRowsCount; - worksheet["!cols"] = [{ wch: 16 }, { wch: 14 }, { wch: 14 }, { wch: 16 }, { wch: 14 }]; + 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: 4 } }, - { s: { r: summaryTitleRow, c: 0 }, e: { r: summaryTitleRow, c: 4 } }, + { 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 = { @@ -620,29 +632,32 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => alignment: { horizontal: "left", vertical: "center" }, }; - for (let c = 0; c <= 4; c += 1) { + 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 <= 4; c += 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 === 0 ? cellStyle : numberStyle; + worksheet[addr].s = c < 3 ? cellStyle : numberStyle; } } for (let r = summaryStartRow; r <= summaryStartRow + 3; r += 1) { - const labelAddr = XLSX.utils.encode_cell({ r, c: 0 }); - const valueAddr = XLSX.utils.encode_cell({ r, c: 1 }); - if (worksheet[labelAddr]) worksheet[labelAddr].s = cellStyle; - if (worksheet[valueAddr]) worksheet[valueAddr].s = numberStyle; + 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( @@ -650,26 +665,37 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => workbook: XLSX.WorkBook, sheetName: string, reportTitle: string, - dailyRows: DailySummaryRow[], + dailyRows: ShopDailyRow[], ) => { const reportSummary = calcSummary(dailyRows); + const blank = ["", "", "", "", "", "", ""]; const aoa: (string | number)[][] = [ - [reportTitle, "", "", "", ""], - ["", "", "", "", ""], + [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.floor2F, row.floor4F, row.truckX, row.total]), - ["", "", "", "", ""], - [t("Summary"), "", "", "", ""], - [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, "", "", ""], + ...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); @@ -684,25 +710,24 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => workbook: XLSX.WorkBook, sheetName: string, reportTitle: string, - categoryLabel: string, - slices: BreakdownRow[], + shops: ShopQtyRow[], totalQty: number, ) => { const aoa: (string | number)[][] = [ - [reportTitle, "", ""], - ["", "", ""], - [categoryLabel, t("Cartons"), t("Share")], - ...slices.map((row) => { + [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.label, row.qty, `${share.toFixed(1)}%`]; + return [row.code, row.name, row.qty, `${share.toFixed(1)}%`]; }), - ["", "", ""], - [t("Total carton qty"), totalQty, ""], + ["", "", "", ""], + [t("Total carton qty"), "", totalQty, ""], ]; const worksheet = XLSX.utils.aoa_to_sheet(aoa); - worksheet["!cols"] = [{ wch: 28 }, { wch: 14 }, { wch: 12 }]; - worksheet["!merges"] = [{ s: { r: 0, c: 0 }, e: { r: 0, c: 2 } }]; + 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" } }, @@ -730,24 +755,27 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => }; if (worksheet["A1"]) worksheet["A1"].s = titleStyle; - for (let c = 0; c <= 2; c += 1) { + for (let c = 0; c <= 3; c += 1) { const headerCell = XLSX.utils.encode_cell({ r: 2, c }); if (worksheet[headerCell]) worksheet[headerCell].s = headerStyle; } - slices.forEach((_, index) => { + shops.forEach((_, index) => { const r = 3 + index; - const labelAddr = XLSX.utils.encode_cell({ r, c: 0 }); - const qtyAddr = XLSX.utils.encode_cell({ r, c: 1 }); - const shareAddr = XLSX.utils.encode_cell({ r, c: 2 }); - if (worksheet[labelAddr]) worksheet[labelAddr].s = cellStyle; + 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 + slices.length; + const totalRow = 4 + shops.length; const totalLabelAddr = XLSX.utils.encode_cell({ r: totalRow, c: 0 }); - const totalValueAddr = XLSX.utils.encode_cell({ r: totalRow, c: 1 }); + 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)); }, @@ -844,7 +872,8 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => 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") : shop; + 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, @@ -852,12 +881,6 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => shop: shopLabel, date: dateLabel, }); - const categoryLabel = - breakdownDimension === "shop" - ? t("Shop Code") - : breakdownDimension === "floor" - ? t("Floor") - : t("Lane"); const dailyRows = buildDailyRowsFromRecords( records, @@ -868,15 +891,17 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => 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"), - breakdownCaption, - categoryLabel, - breakdownRows, + t("Cartons by shop"), + shopRows, filteredTotal, ); @@ -899,9 +924,6 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => lane, shop, records, - breakdownDimension, - breakdownCaption, - breakdownRows, buildDailyRowsFromRecords, addReportSheet, addBreakdownSheet, @@ -982,20 +1004,29 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => - setDate(event.target.value)} - /> + + { + if (newValue && dayjs(newValue).isValid()) { + setDate(dayjs(newValue).format(OUTPUT_DATE_FORMAT)); + } + }} + slotProps={{ + textField: { size: "small", fullWidth: true }, + }} + /> + @@ -1038,93 +1069,134 @@ const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => - - {breakdownCaption} - - - {t("Click a row to filter")} - + + + + {breakdownCaption} + + {chartClicksEnabled && ( + + {t("Click a row to filter")} + + )} + + + {breakdownRows.length === 0 ? ( {t("No data available")} ) : ( - - - {breakdownRows.map((row) => { - const active = isBreakdownRowActive(row); - return ( - applyBreakdownClick(row)} - onKeyDown={(event) => { - if (event.key === "Enter" || event.key === " ") { - event.preventDefault(); - applyBreakdownClick(row); - } + + {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.label} - + /> - ); - })} - - { - const rect = event.currentTarget.getBoundingClientRect(); - const index = Math.floor((event.clientY - rect.top) / CHART_ROW_H); - const row = breakdownRows[index]; - if (row) applyBreakdownClick(row); - }} - > - - + + {row.qty.toLocaleString(numberLocale)} + + + ); + })} )} diff --git a/src/i18n/en/doWorkbench.json b/src/i18n/en/doWorkbench.json index 5031d223..a74b25be 100644 --- a/src/i18n/en/doWorkbench.json +++ b/src/i18n/en/doWorkbench.json @@ -61,6 +61,8 @@ "2/F or 4/F": "2/F or 4/F", "Lane": "Lane", "Shop Code": "Shop group", + "Shop code": "Shop code", + "Back to previous level": "Back", "All lanes": "All lanes", "All shops": "All shop groups", "Cartons by lane": "Cartons by lane", @@ -75,7 +77,7 @@ "Date": "Date", "Download Excel": "Download Excel", "Download this view Excel": "Download Excel (Selected)", - "Download period Excel": "Download Excel (All)", + "Download period Excel": "Download Excel (Complete)", "Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.", "Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.", "Filtered": "Filtered", diff --git a/src/i18n/en/pickOrder.json b/src/i18n/en/pickOrder.json index 6622011a..9bda0fff 100644 --- a/src/i18n/en/pickOrder.json +++ b/src/i18n/en/pickOrder.json @@ -625,7 +625,7 @@ "Exporting...": "Exporting...", "Download Excel": "Download Excel", "Download this view Excel": "Download Excel (Selected)", - "Download period Excel": "Download Excel (All)", + "Download period Excel": "Download Excel (Complete)", "Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.", "Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.", "Filtered": "Filtered", @@ -635,6 +635,8 @@ "2/F or 4/F": "2/F or 4/F", "Lane": "Lane", "Shop Code": "Shop group", + "Shop code": "Shop code", + "Back to previous level": "Back", "All lanes": "All lanes", "All shops": "All shop groups", "Cartons by lane": "Cartons by lane", diff --git a/src/i18n/zh/doWorkbench.json b/src/i18n/zh/doWorkbench.json index 6fed28a9..2d446a83 100644 --- a/src/i18n/zh/doWorkbench.json +++ b/src/i18n/zh/doWorkbench.json @@ -56,6 +56,8 @@ "2/F or 4/F": "2/F 或 4/F", "Lane": "車線", "Shop Code": "店鋪組別", + "Shop code": "店鋪編號", + "Back to previous level": "返回上一層", "All lanes": "全部車線", "All shops": "全部店鋪組別", "Cartons by lane": "按車線統計箱數", @@ -70,7 +72,7 @@ "Date": "日期", "Download Excel": "下載 Excel", "Download this view Excel": "下載 Excel(已選)", - "Download period Excel": "下載 Excel(全部)", + "Download period Excel": "下載 Excel(完整)", "Download this view Excel hint": "匯出目前樓層、車線、店鋪組別、日期的箱數,以及圖表分項。", "Download period Excel hint": "匯出近7天、本月、本年的箱數,不限目前篩選畫面。", "Filtered": "篩選", diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index 5d596eb8..bdde2cf7 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -637,7 +637,7 @@ "Exporting...": "匯出中...", "Download Excel": "下載 Excel", "Download this view Excel": "下載 Excel(已選)", - "Download period Excel": "下載 Excel(全部)", + "Download period Excel": "下載 Excel(完整)", "Download this view Excel hint": "匯出目前樓層、車線、店鋪組別、日期的箱數,以及圖表分項。", "Download period Excel hint": "匯出近7天、本月、本年的箱數,不限目前篩選畫面。", "Filtered": "篩選", @@ -647,6 +647,8 @@ "2/F or 4/F": "2/F 或 4/F", "Lane": "車線", "Shop Code": "店鋪組別", + "Shop code": "店鋪編號", + "Back to previous level": "返回上一層", "All lanes": "全部車線", "All shops": "全部店鋪組別", "Cartons by lane": "按車線統計箱數",