|
|
|
@@ -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; |