"use client"; import React, { useCallback, useState } from "react"; import { Box, Typography, Skeleton, Alert, TextField, Button, Chip, Stack } from "@mui/material"; import dynamic from "next/dynamic"; import dayjs from "dayjs"; import WarehouseIcon from "@mui/icons-material/Warehouse"; import { fetchStockTransactionsByDate, fetchStockInOutByDate, fetchStockBalanceTrend, fetchConsumptionTrendByMonth, } from "@/app/api/chart/client"; import ChartCard from "../_components/ChartCard"; import DateRangeSelect from "../_components/DateRangeSelect"; import { toDateRange, DEFAULT_RANGE_DAYS, ITEM_CODE_DEBOUNCE_MS } from "../_components/constants"; const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); const PAGE_TITLE = "庫存與倉儲"; type Criteria = { stockTxn: { rangeDays: number }; stockInOut: { rangeDays: number }; balance: { rangeDays: number }; consumption: { rangeDays: number }; }; const defaultCriteria: Criteria = { stockTxn: { rangeDays: DEFAULT_RANGE_DAYS }, stockInOut: { rangeDays: DEFAULT_RANGE_DAYS }, balance: { rangeDays: DEFAULT_RANGE_DAYS }, consumption: { rangeDays: DEFAULT_RANGE_DAYS }, }; export default function WarehouseChartPage() { const [criteria, setCriteria] = useState(defaultCriteria); const [itemCodeBalance, setItemCodeBalance] = useState(""); const [debouncedItemCodeBalance, setDebouncedItemCodeBalance] = useState(""); const [consumptionItemCodes, setConsumptionItemCodes] = useState([]); const [consumptionItemCodeInput, setConsumptionItemCodeInput] = useState(""); const [error, setError] = useState(null); const [chartData, setChartData] = useState<{ stockTxn: { date: string; inQty: number; outQty: number; totalQty: number }[]; stockInOut: { date: string; inQty: number; outQty: number }[]; balance: { date: string; balance: number }[]; consumption: { month: string; outQty: number }[]; consumptionByItems?: { months: string[]; series: { name: string; data: number[] }[] }; }>({ stockTxn: [], stockInOut: [], balance: [], consumption: [] }); 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 t = setTimeout(() => setDebouncedItemCodeBalance(itemCodeBalance), ITEM_CODE_DEBOUNCE_MS); return () => clearTimeout(t); }, [itemCodeBalance]); const addConsumptionItem = useCallback(() => { const code = consumptionItemCodeInput.trim(); if (!code || consumptionItemCodes.includes(code)) return; setConsumptionItemCodes((prev) => [...prev, code].sort()); setConsumptionItemCodeInput(""); }, [consumptionItemCodeInput, consumptionItemCodes]); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.stockTxn.rangeDays); setChartLoading("stockTxn", true); fetchStockTransactionsByDate(s, e) .then((data) => setChartData((prev) => ({ ...prev, stockTxn: data as { date: string; inQty: number; outQty: number; totalQty: number }[], })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("stockTxn", false)); }, [criteria.stockTxn, setChartLoading]); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.stockInOut.rangeDays); setChartLoading("stockInOut", true); fetchStockInOutByDate(s, e) .then((data) => setChartData((prev) => ({ ...prev, stockInOut: data as { date: string; inQty: number; outQty: number }[], })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("stockInOut", false)); }, [criteria.stockInOut, setChartLoading]); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.balance.rangeDays); const item = debouncedItemCodeBalance.trim() || undefined; setChartLoading("balance", true); fetchStockBalanceTrend(s, e, item) .then((data) => setChartData((prev) => ({ ...prev, balance: data as { date: string; balance: number }[], })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("balance", false)); }, [criteria.balance, debouncedItemCodeBalance, setChartLoading]); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.consumption.rangeDays); setChartLoading("consumption", true); if (consumptionItemCodes.length === 0) { fetchConsumptionTrendByMonth(dayjs().year(), s, e, undefined) .then((data) => setChartData((prev) => ({ ...prev, consumption: data as { month: string; outQty: number }[], consumptionByItems: undefined, })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("consumption", false)); return; } Promise.all( consumptionItemCodes.map((code) => fetchConsumptionTrendByMonth(dayjs().year(), s, e, code) ) ) .then((results) => { const byItem = results.map((rows, i) => ({ itemCode: consumptionItemCodes[i], rows: rows as { month: string; outQty: number }[], })); const allMonths = Array.from( new Set(byItem.flatMap((x) => x.rows.map((r) => r.month))) ).sort(); const series = byItem.map(({ itemCode, rows }) => ({ name: itemCode, data: allMonths.map((m) => { const r = rows.find((x) => x.month === m); return r ? r.outQty : 0; }), })); setChartData((prev) => ({ ...prev, consumption: [], consumptionByItems: { months: allMonths, series }, })); }) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("consumption", false)); }, [criteria.consumption, consumptionItemCodes, setChartLoading]); return ( {PAGE_TITLE} {error && ( setError(null)}> {error} )} ({ 日期: s.date, 入庫: s.inQty, 出庫: s.outQty, 合計: s.totalQty }))} filters={ updateCriteria("stockTxn", (c) => ({ ...c, rangeDays: v }))} /> } > {loadingCharts.stockTxn ? ( ) : ( s.date) }, yaxis: { title: { text: "數量" } }, stroke: { curve: "smooth" }, dataLabels: { enabled: false }, }} series={[ { name: "入庫", data: chartData.stockTxn.map((s) => s.inQty) }, { name: "出庫", data: chartData.stockTxn.map((s) => s.outQty) }, { name: "合計", data: chartData.stockTxn.map((s) => s.totalQty) }, ]} type="line" width="100%" height={320} /> )} ({ 日期: s.date, 入庫: s.inQty, 出庫: s.outQty }))} filters={ updateCriteria("stockInOut", (c) => ({ ...c, rangeDays: v }))} /> } > {loadingCharts.stockInOut ? ( ) : ( s.date) }, yaxis: { title: { text: "數量" } }, stroke: { curve: "smooth" }, dataLabels: { enabled: false }, }} series={[ { name: "入庫", data: chartData.stockInOut.map((s) => s.inQty) }, { name: "出庫", data: chartData.stockInOut.map((s) => s.outQty) }, ]} type="area" width="100%" height={320} /> )} ({ 日期: b.date, 餘額: b.balance }))} filters={ <> updateCriteria("balance", (c) => ({ ...c, rangeDays: v }))} /> setItemCodeBalance(e.target.value)} sx={{ minWidth: 180 }} /> } > {loadingCharts.balance ? ( ) : ( b.date) }, yaxis: { title: { text: "餘額" } }, stroke: { curve: "smooth" }, dataLabels: { enabled: false }, }} series={[{ name: "餘額", data: chartData.balance.map((b) => b.balance) }]} type="line" width="100%" height={320} /> )} s.data.map((qty, i) => ({ 月份: chartData.consumptionByItems!.months[i], 物料編碼: s.name, 出庫量: qty, })) ) : chartData.consumption.map((c) => ({ 月份: c.month, 出庫量: c.outQty })) } filters={ <> updateCriteria("consumption", (c) => ({ ...c, rangeDays: v }))} /> setConsumptionItemCodeInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addConsumptionItem())} sx={{ minWidth: 180 }} /> {consumptionItemCodes.map((code) => ( setConsumptionItemCodes((prev) => prev.filter((c) => c !== code)) } /> ))} } > {loadingCharts.consumption ? ( ) : chartData.consumptionByItems ? ( ) : ( c.month) }, yaxis: { title: { text: "出庫量" } }, plotOptions: { bar: { columnWidth: "60%" } }, dataLabels: { enabled: false }, }} series={[{ name: "出庫量", data: chartData.consumption.map((c) => c.outQty) }]} type="bar" width="100%" height={320} /> )} ); }