Kaynağa Gözat

[Fix] Amend few visual issues

do_workbench_fix
Harry Groves 7 saat önce
ebeveyn
işleme
759df0b379
6 değiştirilmiş dosya ile 356 ekleme ve 276 silme
  1. +6
    -6
      src/components/DoWorkbench/DoWorkbenchTabs.tsx
  2. +338
    -266
      src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx
  3. +3
    -1
      src/i18n/en/doWorkbench.json
  4. +3
    -1
      src/i18n/en/pickOrder.json
  5. +3
    -1
      src/i18n/zh/doWorkbench.json
  6. +3
    -1
      src/i18n/zh/pickOrder.json

+ 6
- 6
src/components/DoWorkbench/DoWorkbenchTabs.tsx Dosyayı Görüntüle

@@ -282,7 +282,7 @@ const DoWorkbenchTabsInner: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ defaultTabIndex = 0, printerCom
variant="inherit"
sx={{
pr: etraIncompleteDopoCount > 0 ? 1 : 0,
whiteSpace: "normal",
whiteSpace: "nowrap",
lineHeight: 1.2,
textAlign: "center",
}}


+ 338
- 266
src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx Dosyayı Görüntüle

@@ -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<string, ShopQtyRow>();
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<Props> = ({ mode = "normal" }) =>
return options;
}, [laneOptions, lane, t]);

const shopFilterValue = shop === ALL ? ALL : shopGroup(shop);

const shopFilterOptions = useMemo<FilterOption[]>(() => {
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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<ApexOptions>(
() => ({
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<Props> = ({ mode = "normal" }) =>
selectedFloor: FloorFilter,
selectedLane: string,
selectedShop: string,
): DailySummaryRow[] => {
const summaryMap = new Map<string, DailySummaryRow>();
): ShopDailyRow[] => {
const summaryMap = new Map<string, ShopDailyRow>();
const start = startDate.startOf("day");
const end = endDate.endOf("day");

@@ -539,24 +534,33 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ 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<Props> = ({ mode = "normal" }) =>
lane,
shop,
records,
breakdownDimension,
breakdownCaption,
breakdownRows,
buildDailyRowsFromRecords,
addReportSheet,
addBreakdownSheet,
@@ -982,20 +1004,29 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
<SearchableFilterSelect
label={t("Shop Code")}
options={shopFilterOptions}
value={shop}
value={shopFilterValue}
onChange={setShop}
/>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<TextField
fullWidth
size="small"
label={t("Date")}
type="date"
value={date}
InputLabelProps={{ shrink: true }}
onChange={(event) => setDate(event.target.value)}
/>
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={i18n.language?.startsWith("zh") ? "zh-hk" : "en"}
>
<DatePicker
label={t("Date")}
format={OUTPUT_DATE_FORMAT}
value={dayjs(date).isValid() ? dayjs(date) : null}
onChange={(newValue) => {
if (newValue && dayjs(newValue).isValid()) {
setDate(dayjs(newValue).format(OUTPUT_DATE_FORMAT));
}
}}
slotProps={{
textField: { size: "small", fullWidth: true },
}}
/>
</LocalizationProvider>
</Grid>
</Grid>

@@ -1038,93 +1069,134 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
</Paper>

<Paper sx={{ p: 1.5 }}>
<Typography variant="subtitle2" color="text.secondary">
{breakdownCaption}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: "block", mb: 1 }}>
{t("Click a row to filter")}
</Typography>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
spacing={1}
sx={{ mb: 1 }}
>
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle2" color="text.secondary">
{breakdownCaption}
</Typography>
{chartClicksEnabled && (
<Typography variant="caption" color="text.secondary" sx={{ display: "block" }}>
{t("Click a row to filter")}
</Typography>
)}
</Box>
<Button
size="small"
variant="outlined"
startIcon={<ArrowBackIcon />}
onClick={goBackChartLevel}
disabled={!chartCanGoBack}
>
{t("Back to previous level")}
</Button>
</Stack>
{breakdownRows.length === 0 ? (
<Typography sx={{ py: 3 }} color="text.secondary">
{t("No data available")}
</Typography>
) : (
<Box sx={{ display: "flex", alignItems: "stretch" }}>
<Box
sx={{
width: { xs: 112, sm: 168, md: 220 },
flexShrink: 0,
pr: 1.5,
}}
>
{breakdownRows.map((row) => {
const active = isBreakdownRowActive(row);
return (
<Box
key={row.key}
role="button"
tabIndex={0}
onClick={() => applyBreakdownClick(row)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
applyBreakdownClick(row);
}
<Box>
{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 (
<Box
key={row.key}
role={chartClicksEnabled ? "button" : undefined}
tabIndex={chartClicksEnabled ? 0 : undefined}
onClick={() => {
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,
}}
>
<Typography
component="span"
noWrap
title={row.label}
sx={{
minWidth: 0,
width: "100%",
fontWeight: 700,
fontSize: 13,
lineHeight: `${CHART_ROW_H}px`,
color: "text.primary",
textAlign: "left",
}}
>
{row.label}
</Typography>
<Box
sx={{
height: CHART_ROW_H,
height: "100%",
display: "flex",
alignItems: "center",
minWidth: 0,
cursor: "pointer",
borderRadius: 1,
px: 0.5,
bgcolor: active ? "action.selected" : "transparent",
"&:hover": { bgcolor: "action.hover" },
}}
>
<Typography
component="span"
title={row.label}
noWrap
<Box
sx={{
fontWeight: 700,
fontSize: 13,
color: "text.primary",
minWidth: 0,
height: 22,
width: barWidth,
minWidth: row.qty > 0 ? 4 : 0,
bgcolor: "#1976d2",
borderRadius: "4px",
}}
>
{row.label}
</Typography>
/>
</Box>
);
})}
</Box>
<Box
sx={{
flex: 1,
minWidth: 0,
cursor: "pointer",
"& .apexcharts-bar-area, & .apexcharts-series path": {
cursor: "pointer",
},
}}
onClick={(event) => {
const rect = event.currentTarget.getBoundingClientRect();
const index = Math.floor((event.clientY - rect.top) / CHART_ROW_H);
const row = breakdownRows[index];
if (row) applyBreakdownClick(row);
}}
>
<SafeApexCharts
type="bar"
height={chartHeight}
options={chartOptions}
series={chartSeries}
allowZeroSeries
chartRevision={`${floor}-${lane}-${shop}-${date}-${breakdownDimension}-${breakdownRows.length}`}
/>
</Box>
<Box
sx={{
height: 24,
display: "flex",
alignItems: "center",
justifyContent: "center",
px: 0.75,
borderRadius: 0.5,
bgcolor: "#fff",
border: "1px solid #90a4ae",
color: "#102a43",
fontWeight: 800,
fontSize: 15,
lineHeight: 1,
boxShadow: "0 1px 3px rgba(0,0,0,0.22)",
}}
>
{row.qty.toLocaleString(numberLocale)}
</Box>
</Box>
);
})}
</Box>
)}
</Paper>


+ 3
- 1
src/i18n/en/doWorkbench.json Dosyayı Görüntüle

@@ -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",


+ 3
- 1
src/i18n/en/pickOrder.json Dosyayı Görüntüle

@@ -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",


+ 3
- 1
src/i18n/zh/doWorkbench.json Dosyayı Görüntüle

@@ -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": "篩選",


+ 3
- 1
src/i18n/zh/pickOrder.json Dosyayı Görüntüle

@@ -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": "按車線統計箱數",


Yükleniyor…
İptal
Kaydet