Просмотр исходного кода

成品出倉出箱數量 Update

stable1
B.E.N.S.O.N 2 недель назад
Родитель
Сommit
510d3fd831
2 измененных файлов: 262 добавлений и 2 удалений
  1. +255
    -0
      src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx
  2. +7
    -2
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx

+ 255
- 0
src/components/FinishedGoodSearch/FinishedGoodCartonDashboardTab.tsx Просмотреть файл

@@ -0,0 +1,255 @@
"use client";

import { useCallback, useEffect, useMemo, useState } from "react";
import {
Alert,
Box,
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 {
CompletedDoPickOrderResponse,
fetchCompletedDoPickOrdersAll,
} 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;
};

const FinishedGoodCartonDashboardTab: React.FC = () => {
const [floor, setFloor] = useState<FloorFilter>("all");
const [date, setDate] = useState<string>(dayjs().format("YYYY-MM-DD"));
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>("");
const [records, setRecords] = useState<CompletedDoPickOrderResponse[]>([]);

const loadData = useCallback(async () => {
setLoading(true);
setError("");
try {
const data = 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]);

useEffect(() => {
loadData();
}, [loadData]);

const rows = useMemo<DailySummaryRow[]>(() => {
const filtered =
floor === "all" ? records : records.filter((record) => record.storeId === floor);

const summary = new Map<string, DailySummaryRow>();

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<ApexOptions>(
() => ({
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]);

return (
<Box sx={{ width: "100%" }}>
<Typography variant="h6" sx={{ mb: 2 }}>
成品出倉出箱數量
</Typography>

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

{loading ? (
<Box sx={{ py: 6, display: "flex", justifyContent: "center" }}>
<CircularProgress />
</Box>
) : (
<Stack spacing={2}>
<Grid container spacing={1.5}>
<Grid item xs={12} md={6}>
<TextField
select
fullWidth
label="樓層"
value={floor}
onChange={(event) => setFloor(event.target.value as FloorFilter)}
>
<MenuItem value="all">全部</MenuItem>
<MenuItem value="2/F">2/F</MenuItem>
<MenuItem value="4/F">4/F</MenuItem>
</TextField>
</Grid>
<Grid item xs={12} md={6}>
<TextField
fullWidth
label="日期"
type="date"
value={date}
InputLabelProps={{ shrink: true }}
onChange={(event) => setDate(event.target.value)}
/>
</Grid>
</Grid>

<Grid container spacing={1.5} alignItems="stretch">
<Grid item xs={12} md={4}>
<TableContainer component={Paper} sx={{ height: "100%" }}>
<Table size="small">
<TableBody>
<TableRow>
<TableCell>2/F 出箱數</TableCell>
<TableCell align="right">{summary.floor2F.toLocaleString("zh-HK")}</TableCell>
</TableRow>
<TableRow>
<TableCell>4/F 出箱數</TableCell>
<TableCell align="right">{summary.floor4F.toLocaleString("zh-HK")}</TableCell>
</TableRow>
<TableRow>
<TableCell>車線-X 出箱數</TableCell>
<TableCell align="right">{summary.truckX.toLocaleString("zh-HK")}</TableCell>
</TableRow>
<TableRow>
<TableCell>總出箱數</TableCell>
<TableCell align="right">{summary.total.toLocaleString("zh-HK")}</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Grid>
<Grid item xs={12} md={8}>
<Paper sx={{ p: 1.5, height: "100%" }}>
<SafeApexCharts
type="bar"
height={240}
options={chartOptions}
series={chartSeries}
chartRevision={`${floor}-${date}-${rows.length}`}
/>
</Paper>
</Grid>
</Grid>
</Stack>
)}
</Box>
);
};

export default FinishedGoodCartonDashboardTab;

+ 7
- 2
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Просмотреть файл

@@ -42,6 +42,7 @@ import { PrinterCombo } from "@/app/api/settings/printer";
import { Autocomplete } from "@mui/material";
import FGPickOrderTicketReleaseTable from "./FGPickOrderTicketReleaseTable";
import TruckRoutingSummaryTab, { TruckRoutingSummaryFilters } from "./TruckRoutingSummaryTab";
import FinishedGoodCartonDashboardTab from "./FinishedGoodCartonDashboardTab";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { fetchTruckRoutingSummaryPrecheck } from "@/app/(main)/report/truckRoutingSummaryApi";
@@ -378,7 +379,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo
}
}, [truckRoutingFilters, selectedPrinterForAllDraft, t]);

const isTruckRoutingTab = tabIndex === 5;
const isTruckRoutingTab = tabIndex === 6;
const canPrintTruckRoutingSummary = Boolean(
truckRoutingFilters.storeId &&
truckRoutingFilters.truckLanceCode &&
@@ -393,7 +394,7 @@ const [selectedPrinterForDraft, setSelectedPrinterForDraft] = useState<PrinterCo
}, [fetchReleasedOrderCount]);

useEffect(() => {
if (tabIndex === 5) {
if (tabIndex === 6) {
logFeatureUsage(FEATURE_USAGE.TRUCK_ROUTING_SUMMARY, FEATURE_USAGE_ACTION.PAGE_VIEW);
}
}, [tabIndex]);
@@ -831,6 +832,7 @@ const handleAssignByLane = useCallback(async (
<Tab label={t("Finished Good Record")} iconPosition="end" />
<Tab label={t("Ticket Release Table")} iconPosition="end" />
<Tab label={t("Finished Good Record (All)")} iconPosition="end" />
<Tab label="成品出倉出箱數量" iconPosition="end" />
<Tab label="送貨路線摘要" iconPosition="end" />
</Tabs>
@@ -887,6 +889,9 @@ const handleAssignByLane = useCallback(async (
/>
)}
{tabIndex === 5 && (
<FinishedGoodCartonDashboardTab />
)}
{tabIndex === 6 && (
<TruckRoutingSummaryTab onFiltersChange={setTruckRoutingFilters} />
)}
</Box>


Загрузка…
Отмена
Сохранить