"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Alert, Box, Button, CircularProgress, Grid, MenuItem, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Typography, } from "@mui/material"; import type { ApexOptions } from "apexcharts"; import dayjs from "dayjs"; import * as XLSX from "xlsx-js-style"; import { CompletedDoPickOrderResponse, fetchCompletedDoPickOrdersAll, fetchCompletedDoPickOrdersWorkbenchAll, } from "@/app/api/pickOrder/actions"; import SafeApexCharts from "@/components/charts/SafeApexCharts"; type FloorFilter = "all" | "2/F" | "4/F"; type DailySummaryRow = { date: string; floor2F: number; floor4F: number; truckX: number; total: number; }; type Props = { mode?: "normal" | "workbench"; }; const FinishedGoodCartonDashboardTab: React.FC = ({ mode = "normal" }) => { const [floor, setFloor] = useState("all"); const [date, setDate] = useState(dayjs().format("YYYY-MM-DD")); const [loading, setLoading] = useState(false); const [isExporting, setIsExporting] = useState(false); const [error, setError] = useState(""); const [records, setRecords] = useState([]); 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("載入成品出倉出箱數量失敗,請稍後再試。"); setRecords([]); } finally { setLoading(false); } }, [date, mode]); useEffect(() => { loadData(); }, [loadData]); const rows = useMemo(() => { const filtered = floor === "all" ? records : records.filter((record) => record.storeId === floor); const summary = new Map(); filtered.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 (String(record.truckLanceCode ?? "").trim() === "車線-X") { current.truckX += cartonQty; } current.total += cartonQty; summary.set(day, current); }); return Array.from(summary.values()).sort((a, b) => b.date.localeCompare(a.date)); }, [records, floor]); const chartOptions = useMemo( () => ({ chart: { type: "bar", toolbar: { show: false }, }, colors: ["#1976d2", "#9c27b0", "#ff9800", "#2e7d32"], dataLabels: { enabled: false }, stroke: { show: true, width: 1, colors: ["transparent"] }, plotOptions: { bar: { horizontal: false, borderRadius: 3, columnWidth: "55%", }, }, xaxis: { categories: rows.map((row) => row.date), title: { text: "日期" }, }, yaxis: { title: { text: "箱數" }, labels: { formatter: (val) => Number(val || 0).toLocaleString("zh-HK"), }, }, tooltip: { y: { formatter: (val) => `${Number(val || 0).toLocaleString("zh-HK")} 箱`, }, }, legend: { position: "top", }, noData: { text: "沒有圖表資料", }, }), [rows], ); const chartSeries = useMemo( () => [ { name: "2/F", data: rows.map((row) => row.floor2F) }, { name: "4/F", data: rows.map((row) => row.floor4F) }, { name: "車線-X", data: rows.map((row) => row.truckX) }, { name: "總數", data: rows.map((row) => row.total) }, ], [rows], ); 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 buildDailyRowsFromRecords = useCallback( ( sourceRecords: CompletedDoPickOrderResponse[], startDate: dayjs.Dayjs, endDate: dayjs.Dayjs, selectedFloor: FloorFilter, ): DailySummaryRow[] => { const summaryMap = new Map(); const start = startDate.startOf("day"); const end = endDate.endOf("day"); sourceRecords.forEach((record) => { if (selectedFloor !== "all" && record.storeId !== selectedFloor) { 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 cartonQty = Number(record.numberOfCartons ?? 0); const current = summaryMap.get(dayKey) ?? { date: dayKey, 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 (String(record.truckLanceCode ?? "").trim() === "車線-X") current.truckX += cartonQty; current.total += cartonQty; summaryMap.set(dayKey, current); }); return Array.from(summaryMap.values()).sort((a, b) => a.date.localeCompare(b.date)); }, [], ); 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: 16 }, { wch: 14 }, { wch: 14 }, { wch: 16 }, { wch: 14 }]; worksheet["!merges"] = [ { s: { r: 0, c: 0 }, e: { r: 0, c: 4 } }, { s: { r: summaryTitleRow, c: 0 }, e: { r: summaryTitleRow, c: 4 } }, ]; 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 <= 4; 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) { const addr = XLSX.utils.encode_cell({ r, c }); if (!worksheet[addr]) continue; worksheet[addr].s = c === 0 ? 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; } if (worksheet["A1"]) worksheet["A1"].s = titleStyle; const summaryTitleAddr = XLSX.utils.encode_cell({ r: summaryTitleRow, c: 0 }); if (worksheet[summaryTitleAddr]) worksheet[summaryTitleAddr].s = summaryTitleStyle; }, []); const addReportSheet = useCallback( ( workbook: XLSX.WorkBook, sheetName: string, reportTitle: string, dailyRows: DailySummaryRow[], ) => { const reportSummary = calcSummary(dailyRows); const aoa: (string | number)[][] = [ [reportTitle, "", "", "", ""], ["", "", "", "", ""], ["日期", "2/F 出箱數", "4/F 出箱數", "車線-X 出箱數", "總出箱數"], ...dailyRows.map((row) => [row.date, row.floor2F, row.floor4F, row.truckX, row.total]), ["", "", "", "", ""], ["彙總", "", "", "", ""], ["2/F 出箱數", reportSummary.floor2F, "", "", ""], ["4/F 出箱數", reportSummary.floor4F, "", "", ""], ["車線-X 出箱數", reportSummary.truckX, "", "", ""], ["總出箱數", reportSummary.total, "", "", ""], ]; const worksheet = XLSX.utils.aoa_to_sheet(aoa); styleWorksheet(worksheet, dailyRows.length); XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); }, [calcSummary, styleWorksheet], ); const handleDownloadExcel = useCallback(async () => { setIsExporting(true); try { const allRecords = mode === "workbench" ? await fetchCompletedDoPickOrdersWorkbenchAll() : await fetchCompletedDoPickOrdersAll(); const baseDate = dayjs(date || undefined).isValid() ? dayjs(date) : dayjs(); const floorLabel = floor === "all" ? "全部樓層" : floor; const dateLabel = baseDate.format("YYYY-MM-DD"); const last7Rows = buildDailyRowsFromRecords( allRecords, baseDate.subtract(6, "day"), baseDate, floor, ); const monthRows = buildDailyRowsFromRecords( allRecords, baseDate.startOf("month"), baseDate.endOf("month"), floor, ); const yearRows = buildDailyRowsFromRecords( allRecords, baseDate.startOf("year"), baseDate.endOf("year"), floor, ); const workbook = XLSX.utils.book_new(); addReportSheet( workbook, "最近7天", `成品出倉出箱數量(最近7天)- ${floorLabel} - 基準日 ${dateLabel}`, last7Rows, ); addReportSheet( workbook, "本月", `成品出倉出箱數量(本月)- ${floorLabel} - ${baseDate.format("YYYY年MM月")}`, monthRows, ); addReportSheet( workbook, "本年", `成品出倉出箱數量(本年)- ${floorLabel} - ${baseDate.format("YYYY年")}`, yearRows, ); XLSX.writeFile(workbook, `成品出倉出箱數量_多時段報表_${floorLabel.replace("/", "")}_${dateLabel}.xlsx`); } finally { setIsExporting(false); } }, [mode, date, floor, buildDailyRowsFromRecords, addReportSheet]); return ( 成品出倉出箱數量 {error && ( {error} )} {loading ? ( ) : ( setFloor(event.target.value as FloorFilter)} > 全部 2/F 4/F setDate(event.target.value)} /> 2/F 出箱數 {summary.floor2F.toLocaleString("zh-HK")} 4/F 出箱數 {summary.floor4F.toLocaleString("zh-HK")} 車線-X 出箱數 {summary.truckX.toLocaleString("zh-HK")} 總出箱數 {summary.total.toLocaleString("zh-HK")}
)}
); }; export default FinishedGoodCartonDashboardTab;