Kaynağa Gözat

DO CARTON QTY EXCEL VER DOWN

production
B.E.N.S.O.N 3 gün önce
ebeveyn
işleme
7e936d2fe5
1 değiştirilmiş dosya ile 226 ekleme ve 3 silme
  1. +226
    -3
      src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx

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

@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import {
Alert,
Box,
Button,
CircularProgress,
Grid,
MenuItem,
@@ -20,6 +21,7 @@ import {
} from "@mui/material";
import type { ApexOptions } from "apexcharts";
import dayjs from "dayjs";
import * as XLSX from "xlsx-js-style";
import {
CompletedDoPickOrderResponse,
fetchCompletedDoPickOrdersAll,
@@ -45,6 +47,7 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
const [floor, setFloor] = useState<FloorFilter>("all");
const [date, setDate] = useState<string>(dayjs().format("YYYY-MM-DD"));
const [loading, setLoading] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const [error, setError] = useState<string>("");
const [records, setRecords] = useState<CompletedDoPickOrderResponse[]>([]);

@@ -175,11 +178,231 @@ const FinishedGoodCartonDashboardTab: React.FC<Props> = ({ mode = "normal" }) =>
);
}, [rows]);

const buildDailyRowsFromRecords = useCallback(
(
sourceRecords: CompletedDoPickOrderResponse[],
startDate: dayjs.Dayjs,
endDate: dayjs.Dayjs,
selectedFloor: FloorFilter,
): DailySummaryRow[] => {
const summaryMap = new Map<string, DailySummaryRow>();
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 (
<Box sx={{ width: "100%" }}>
<Typography variant="h6" sx={{ mb: 2 }}>
成品出倉出箱數量
</Typography>
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}>
<Typography variant="h6">成品出倉出箱數量</Typography>
<Button
variant="contained"
onClick={handleDownloadExcel}
disabled={loading || isExporting}
>
{isExporting ? "匯出中..." : "下載 Excel"}
</Button>
</Stack>

{error && (
<Alert severity="error" sx={{ mb: 2 }}>


Yükleniyor…
İptal
Kaydet