"use client"; import React, { useCallback, useMemo, useState } from "react"; import { Box, Typography, Skeleton, Alert, TextField, FormControl, InputLabel, Select, MenuItem, Autocomplete, Chip, } from "@mui/material"; import dynamic from "next/dynamic"; import LocalShipping from "@mui/icons-material/LocalShipping"; import { fetchDeliveryOrderByDate, fetchTopDeliveryItems, fetchTopDeliveryItemsItemOptions, fetchStaffDeliveryPerformance, fetchStaffDeliveryPerformanceHandlers, type StaffOption, type TopDeliveryItemOption, } from "@/app/api/chart/client"; import ChartCard from "../_components/ChartCard"; import DateRangeSelect from "../_components/DateRangeSelect"; import { toDateRange, DEFAULT_RANGE_DAYS, TOP_ITEMS_LIMIT_OPTIONS } from "../_components/constants"; const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); const PAGE_TITLE = "發貨與配送"; type Criteria = { delivery: { rangeDays: number }; topItems: { rangeDays: number; limit: number }; staffPerf: { rangeDays: number }; }; const defaultCriteria: Criteria = { delivery: { rangeDays: DEFAULT_RANGE_DAYS }, topItems: { rangeDays: DEFAULT_RANGE_DAYS, limit: 10 }, staffPerf: { rangeDays: DEFAULT_RANGE_DAYS }, }; export default function DeliveryChartPage() { const [criteria, setCriteria] = useState(defaultCriteria); const [topItemsSelected, setTopItemsSelected] = useState([]); const [topItemOptions, setTopItemOptions] = useState([]); const [staffSelected, setStaffSelected] = useState([]); const [staffOptions, setStaffOptions] = useState([]); const [error, setError] = useState(null); const [chartData, setChartData] = useState<{ delivery: { date: string; orderCount: number; totalQty: number }[]; topItems: { itemCode: string; itemName: string; totalQty: number }[]; staffPerf: { date: string; staffName: string; orderCount: number; totalMinutes: number }[]; }>({ delivery: [], topItems: [], staffPerf: [] }); const [loadingCharts, setLoadingCharts] = useState>({}); const updateCriteria = useCallback( (key: K, updater: (prev: Criteria[K]) => Criteria[K]) => { setCriteria((prev) => ({ ...prev, [key]: updater(prev[key]) })); }, [] ); const setChartLoading = useCallback((key: string, value: boolean) => { setLoadingCharts((prev) => (prev[key] === value ? prev : { ...prev, [key]: value })); }, []); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.delivery.rangeDays); setChartLoading("delivery", true); fetchDeliveryOrderByDate(s, e) .then((data) => setChartData((prev) => ({ ...prev, delivery: data as { date: string; orderCount: number; totalQty: number }[], })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("delivery", false)); }, [criteria.delivery, setChartLoading]); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.topItems.rangeDays); setChartLoading("topItems", true); fetchTopDeliveryItems( s, e, criteria.topItems.limit, topItemsSelected.length > 0 ? topItemsSelected.map((o) => o.itemCode) : undefined ) .then((data) => setChartData((prev) => ({ ...prev, topItems: data as { itemCode: string; itemName: string; totalQty: number }[], })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("topItems", false)); }, [criteria.topItems, topItemsSelected, setChartLoading]); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.staffPerf.rangeDays); const staffNos = staffSelected.length > 0 ? staffSelected.map((o) => o.staffNo) : undefined; setChartLoading("staffPerf", true); fetchStaffDeliveryPerformance(s, e, staffNos) .then((data) => setChartData((prev) => ({ ...prev, staffPerf: data as { date: string; staffName: string; orderCount: number; totalMinutes: number; }[], })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("staffPerf", false)); }, [criteria.staffPerf, staffSelected, setChartLoading]); React.useEffect(() => { fetchStaffDeliveryPerformanceHandlers() .then(setStaffOptions) .catch(() => setStaffOptions([])); }, []); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.topItems.rangeDays); fetchTopDeliveryItemsItemOptions(s, e).then(setTopItemOptions).catch(() => setTopItemOptions([])); }, [criteria.topItems.rangeDays]); const staffPerfByStaff = useMemo(() => { const map = new Map(); for (const r of chartData.staffPerf) { const name = r.staffName || "Unknown"; const cur = map.get(name) ?? { orderCount: 0, totalMinutes: 0 }; map.set(name, { orderCount: cur.orderCount + r.orderCount, totalMinutes: cur.totalMinutes + r.totalMinutes, }); } return Array.from(map.entries()).map(([staffName, v]) => ({ staffName, orderCount: v.orderCount, totalMinutes: v.totalMinutes, avgMinutesPerOrder: v.orderCount > 0 ? Math.round(v.totalMinutes / v.orderCount) : 0, })); }, [chartData.staffPerf]); return ( {PAGE_TITLE} {error && ( setError(null)}> {error} )} ({ 日期: d.date, 單數: d.orderCount }))} filters={ updateCriteria("delivery", (c) => ({ ...c, rangeDays: v }))} /> } > {loadingCharts.delivery ? ( ) : ( d.date) }, yaxis: { title: { text: "單數" } }, plotOptions: { bar: { horizontal: false, columnWidth: "60%" } }, dataLabels: { enabled: false }, }} series={[{ name: "單數", data: chartData.delivery.map((d) => d.orderCount) }]} type="bar" width="100%" height={320} /> )} ({ 物料編碼: i.itemCode, 物料名稱: i.itemName, 數量: i.totalQty }))} filters={ <> updateCriteria("topItems", (c) => ({ ...c, rangeDays: v }))} /> 顯示 setTopItemsSelected(v)} getOptionLabel={(opt) => [opt.itemCode, opt.itemName].filter(Boolean).join(" - ") || opt.itemCode} isOptionEqualToValue={(a, b) => a.itemCode === b.itemCode} renderInput={(params) => ( )} renderTags={(value, getTagProps) => value.map((option, index) => { const { key: _key, ...tagProps } = getTagProps({ index }); return ( ); }) } sx={{ minWidth: 280 }} /> } > {loadingCharts.topItems ? ( ) : ( `${i.itemCode} ${i.itemName}`.trim()), }, plotOptions: { bar: { horizontal: true, barHeight: "70%" } }, dataLabels: { enabled: true }, }} series={[{ name: "數量", data: chartData.topItems.map((i) => i.totalQty) }]} type="bar" width="100%" height={Math.max(320, chartData.topItems.length * 36)} /> )} ({ 日期: r.date, 員工: r.staffName, 揀單數: r.orderCount, 總分鐘: r.totalMinutes }))} filters={ <> updateCriteria("staffPerf", (c) => ({ ...c, rangeDays: v }))} /> setStaffSelected(v)} getOptionLabel={(opt) => [opt.staffNo, opt.name].filter(Boolean).join(" - ") || opt.staffNo} isOptionEqualToValue={(a, b) => a.staffNo === b.staffNo} renderInput={(params) => ( )} renderTags={(value, getTagProps) => value.map((option, index) => { const { key: _key, ...tagProps } = getTagProps({ index }); return ( ); }) } sx={{ minWidth: 260 }} /> } > {loadingCharts.staffPerf ? ( ) : chartData.staffPerf.length === 0 ? ( 此日期範圍內尚無完成之發貨單,或無揀貨人資料。請更換日期範圍或確認發貨單(DO)已由員工完成並有紀錄揀貨時間。 ) : ( <> 週期內每人揀單數及總耗時(首揀至完成) 員工 揀單數 總分鐘 平均分鐘/單 {staffPerfByStaff.length === 0 ? ( 無數據 ) : ( staffPerfByStaff.map((row) => ( {row.staffName} {row.orderCount} {row.totalMinutes} {row.avgMinutesPerOrder} )) )} 每日按員工單數 r.date))].sort(), }, yaxis: { title: { text: "單數" } }, plotOptions: { bar: { columnWidth: "60%", stacked: true } }, dataLabels: { enabled: false }, legend: { position: "top" }, }} series={(() => { const staffNames = [...new Set(chartData.staffPerf.map((r) => r.staffName))].filter(Boolean).sort(); const dates = Array.from(new Set(chartData.staffPerf.map((r) => r.date))).sort(); return staffNames.map((name) => ({ name: name || "Unknown", data: dates.map((d) => { const row = chartData.staffPerf.find((r) => r.date === d && r.staffName === name); return row ? row.orderCount : 0; }), })); })()} type="bar" width="100%" height={320} /> )} ); }