"use client"; import React, { useCallback, useState } from "react"; import { Box, Typography, Skeleton, Alert, FormControl, InputLabel, Select, MenuItem, Checkbox, ListItemText, } from "@mui/material"; import TrendingUp from "@mui/icons-material/TrendingUp"; import { fetchProductionScheduleByDate, fetchPlannedOutputByDateAndItem, } from "@/app/api/chart/client"; import ChartCard from "../_components/ChartCard"; import DateRangeSelect from "../_components/DateRangeSelect"; import { toDateRange, DEFAULT_RANGE_DAYS } from "../_components/constants"; import SafeApexCharts from "@/components/charts/SafeApexCharts"; const PAGE_TITLE = "預測與計劃"; const DISTINCT_ITEM_COLORS = [ "#d60000", "#018700", "#b500ff", "#05acc6", "#97ff00", "#ffa52f", "#ff8ec8", "#79525f", "#00fdcf", "#afa5ff", "#93ac83", "#9a6900", "#366962", "#d3008c", "#fdf490", "#c86e66", "#9ee2ff", "#00c846", "#ffa6b8", "#5f7a78", "#da81ff", "#ffc93d", "#4b5600", "#ff54a8", "#25bfff", "#4b3b00", "#ff7a00", "#8ed4a8", "#6e4b87", "#91b8ff", "#a03f00", "#00b395", "#c8a2c8", "#e67e22", "#16a085", "#8e44ad", "#2ecc71", "#f1c40f", "#e74c3c", "#2980b9", "#27ae60", "#f39c12", "#c0392b", "#1abc9c", "#9b59b6", "#34495e", "#ff1493", "#00ced1", "#7fff00", "#ff4500", "#00ff7f", "#4169e1", "#ff00ff", "#00bfff", "#ff6347", "#32cd32", "#ffd700", "#8b0000", "#006400", "#4b0082", "#b22222", "#228b22", "#00008b", "#ff69b4", "#20b2aa", "#ffb6c1", "#87cefa", "#adff2f", "#ffdead", "#40e0d0", "#ff7f50", "#7b68ee", ]; function getItemCodeColor(itemCode: string): string { let hash = 0; for (let i = 0; i < itemCode.length; i += 1) { hash = (hash * 31 + itemCode.charCodeAt(i)) | 0; } return DISTINCT_ITEM_COLORS[Math.abs(hash) % DISTINCT_ITEM_COLORS.length]; } type Criteria = { prodSchedule: { rangeDays: number }; plannedOutputByDate: { rangeDays: number; itemCodes: string[] }; }; const defaultCriteria: Criteria = { prodSchedule: { rangeDays: DEFAULT_RANGE_DAYS }, plannedOutputByDate: { rangeDays: DEFAULT_RANGE_DAYS, itemCodes: [] }, }; export default function ForecastChartPage() { const [criteria, setCriteria] = useState(defaultCriteria); const [error, setError] = useState(null); const [chartData, setChartData] = useState<{ prodSchedule: { date: string; scheduledItemCount: number; totalEstProdCount: number }[]; plannedOutputByDate: { date: string; itemCode: string; itemName: string; qty: number }[]; }>({ prodSchedule: [], plannedOutputByDate: [] }); 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.prodSchedule.rangeDays); setChartLoading("prodSchedule", true); fetchProductionScheduleByDate(s, e) .then((data) => setChartData((prev) => ({ ...prev, prodSchedule: data as { date: string; scheduledItemCount: number; totalEstProdCount: number; }[], })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("prodSchedule", false)); }, [criteria.prodSchedule, setChartLoading]); React.useEffect(() => { const { startDate: s, endDate: e } = toDateRange(criteria.plannedOutputByDate.rangeDays); setChartLoading("plannedOutputByDate", true); fetchPlannedOutputByDateAndItem(s, e) .then((data) => setChartData((prev) => ({ ...prev, plannedOutputByDate: data as { date: string; itemCode: string; itemName: string; qty: number }[], })) ) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setChartLoading("plannedOutputByDate", false)); }, [criteria.plannedOutputByDate.rangeDays, setChartLoading]); const plannedOutputRows = chartData.plannedOutputByDate; const plannedOutputItemOptions = Array.from( new Map(plannedOutputRows.map((r) => [r.itemCode, { itemCode: r.itemCode, itemName: r.itemName || "" }])).values() ).sort((a, b) => a.itemCode.localeCompare(b.itemCode)); const filteredPlannedOutputRows = criteria.plannedOutputByDate.itemCodes.length === 0 ? plannedOutputRows : plannedOutputRows.filter((r) => criteria.plannedOutputByDate.itemCodes.includes(r.itemCode)); const plannedOutputChart = React.useMemo(() => { const rows = filteredPlannedOutputRows; const dates = Array.from(new Set(rows.map((r) => r.date))).sort(); const items = Array.from( new Map(rows.map((r) => [r.itemCode, { itemCode: r.itemCode, itemName: r.itemName || "" }])).values() ).sort((a, b) => a.itemCode.localeCompare(b.itemCode)); const series = items.map(({ itemCode, itemName }) => ({ name: [itemCode, itemName].filter(Boolean).join(" ") || itemCode, data: dates.map((d) => { const r = rows.find((x) => x.date === d && x.itemCode === itemCode); return r != null && r.qty != null ? Number(r.qty) : 0; }), })); const colors = items.map(({ itemCode }) => getItemCodeColor(itemCode)); const hasData = dates.length > 0 && series.length > 0; // Remount chart when structure changes — avoids ApexCharts internal series/colors desync ("reading 'data'"). const chartKey = `${dates.join(",")}|${items.map((i) => i.itemCode).join(",")}|${series.length}`; return { dates, series, colors, hasData, chartKey }; }, [filteredPlannedOutputRows]); return ( {PAGE_TITLE} {error && ( setError(null)}> {error} )} ({ 日期: r.date, 物料編碼: r.itemCode, 物料名稱: r.itemName, 數量: r.qty }))} filters={ updateCriteria("plannedOutputByDate", (c) => ({ ...c, rangeDays: v }))} /> 物料編碼 } > {loadingCharts.plannedOutputByDate ? ( ) : !plannedOutputChart.hasData ? ( 此日期範圍內尚無排程資料。 ) : ( )} ({ 日期: d.date, 已排物料: d.scheduledItemCount, 預估產量: d.totalEstProdCount }))} filters={ updateCriteria("prodSchedule", (c) => ({ ...c, rangeDays: v }))} /> } > {loadingCharts.prodSchedule ? ( ) : ( d.date) }, yaxis: { title: { text: "數量" } }, plotOptions: { bar: { columnWidth: "60%" } }, dataLabels: { enabled: false }, }} series={[ { name: "已排物料", data: chartData.prodSchedule.map((d) => d.scheduledItemCount) }, { name: "預估產量", data: chartData.prodSchedule.map((d) => d.totalEstProdCount) }, ]} type="bar" width="100%" height={320} /> )} ); }