| @@ -216,7 +216,6 @@ export async function fetchWorkbenchStoreLaneSummary( | |||
| return serverFetchJson<StoreLaneSummary>(url, { | |||
| method: "GET", | |||
| cache: "no-store", | |||
| next: { revalidate: 0 }, | |||
| }); | |||
| } | |||
| @@ -235,7 +234,6 @@ export async function fetchWorkbenchEtraLaneSummary( | |||
| const data = await serverFetchJson<WorkbenchEtraShopLaneGroup[]>(url, { | |||
| method: "GET", | |||
| cache: "no-store", | |||
| next: { revalidate: 0 }, | |||
| }); | |||
| return Array.isArray(data) ? data : []; | |||
| } | |||
| @@ -632,7 +632,6 @@ export async function fetchStoreLaneSummary(storeId: string, requiredDate?: stri | |||
| const response = await serverFetchJson<StoreLaneSummary>(url, { | |||
| method: "GET", | |||
| cache: "no-store", | |||
| next: { revalidate: 0 }, | |||
| }); | |||
| console.timeEnd(label); | |||
| return response; | |||
| @@ -856,7 +855,6 @@ export const fetchFGPickOrdersByUserIdWorkbench = async (userId: number) => { | |||
| method: "GET", | |||
| // Must be fresh: determines whether shell shows Floor/Lane panel or Detail. | |||
| cache: "no-store", | |||
| next: { revalidate: 0 }, | |||
| }, | |||
| ); | |||
| }; | |||
| @@ -150,6 +150,34 @@ function buildApexConfig( | |||
| const EMPTY_MESSAGE = "暫無圖表資料(後端無法連線或此區間無資料)。"; | |||
| /** Apex mutates `options` in place (circular refs). Never throw during render. */ | |||
| function safeStringify(value: unknown): string { | |||
| const seen = new WeakSet<object>(); | |||
| try { | |||
| return JSON.stringify(value, (_key, nested) => { | |||
| if (typeof nested === "function") return undefined; | |||
| if (typeof nested === "object" && nested !== null) { | |||
| if (seen.has(nested)) return undefined; | |||
| seen.add(nested); | |||
| } | |||
| return nested; | |||
| }); | |||
| } catch { | |||
| return ""; | |||
| } | |||
| } | |||
| function destroyChart(chartRef: { current: { destroy: () => void } | null }) { | |||
| const current = chartRef.current; | |||
| chartRef.current = null; | |||
| if (!current) return; | |||
| try { | |||
| current.destroy(); | |||
| } catch { | |||
| /* ignore */ | |||
| } | |||
| } | |||
| export default function SafeApexCharts(props: SafeApexChartsProps) { | |||
| const { type, series, options, height, width, chartRevision, allowZeroSeries = false, ...rest } = props; | |||
| const containerRef = useRef<HTMLDivElement>(null); | |||
| @@ -162,18 +190,11 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { | |||
| sanitized = alignSeriesToCategoryCount(sanitized, cats.length); | |||
| } | |||
| if (shouldShowPlaceholder(type, options, sanitized, allowZeroSeries)) { | |||
| return ( | |||
| <Typography color="text.secondary" sx={{ py: 3 }}> | |||
| {EMPTY_MESSAGE} | |||
| </Typography> | |||
| ); | |||
| } | |||
| let chartOptions = options; | |||
| let renderSeries: ApexChartProps["series"] = sanitized; | |||
| let showPlaceholder = shouldShowPlaceholder(type, options, sanitized, allowZeroSeries); | |||
| if (isRadialChart(type) && isNumberArray(sanitized)) { | |||
| if (!showPlaceholder && isRadialChart(type) && isNumberArray(sanitized)) { | |||
| const prev = Array.isArray(options?.labels) ? (options!.labels as unknown[]) : []; | |||
| const pairs = sanitized.map((v, i) => { | |||
| const n = Number.isFinite(v) ? v : 0; | |||
| @@ -183,20 +204,31 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { | |||
| }); | |||
| const nonzero = pairs.filter((p) => p.v > 0); | |||
| if (nonzero.length === 0) { | |||
| return ( | |||
| <Typography color="text.secondary" sx={{ py: 3 }}> | |||
| {EMPTY_MESSAGE} | |||
| </Typography> | |||
| ); | |||
| showPlaceholder = true; | |||
| } else { | |||
| renderSeries = nonzero.map((p) => p.v); | |||
| chartOptions = { ...options, labels: nonzero.map((p) => p.label) }; | |||
| } | |||
| renderSeries = nonzero.map((p) => p.v); | |||
| chartOptions = { ...options, labels: nonzero.map((p) => p.label) }; | |||
| } | |||
| const chartType = String(type ?? "line"); | |||
| const configSnapshot = `${String(chartRevision ?? "")}|${chartType}|${JSON.stringify(renderSeries ?? null)}|${JSON.stringify(chartOptions ?? {})}|${String(height ?? "")}|${String(width ?? "")}`; | |||
| const configSnapshot = [ | |||
| String(chartRevision ?? ""), | |||
| chartType, | |||
| String(showPlaceholder), | |||
| safeStringify(renderSeries ?? null), | |||
| safeStringify(cats ?? null), | |||
| safeStringify(chartOptions?.labels ?? null), | |||
| String(height ?? ""), | |||
| String(width ?? ""), | |||
| ].join("|"); | |||
| useEffect(() => { | |||
| if (showPlaceholder) { | |||
| destroyChart(chartRef); | |||
| return; | |||
| } | |||
| const el = containerRef.current; | |||
| if (!el) return; | |||
| @@ -219,12 +251,7 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { | |||
| if (disposed || containerRef.current !== el) { | |||
| if (chartRef.current === instance) { | |||
| try { | |||
| instance.destroy(); | |||
| } catch { | |||
| /* ignore */ | |||
| } | |||
| chartRef.current = null; | |||
| destroyChart(chartRef); | |||
| } | |||
| return; | |||
| } | |||
| @@ -236,21 +263,21 @@ export default function SafeApexCharts(props: SafeApexChartsProps) { | |||
| return () => { | |||
| disposed = true; | |||
| const c = chartRef.current; | |||
| chartRef.current = null; | |||
| if (c) { | |||
| try { | |||
| c.destroy(); | |||
| } catch { | |||
| /* ignore */ | |||
| } | |||
| } | |||
| destroyChart(chartRef); | |||
| }; | |||
| }, [configSnapshot]); | |||
| const minH = typeof height === "number" ? height : typeof height === "string" ? height : 240; | |||
| const dom = rest as { className?: string; id?: string; style?: CSSProperties; sx?: object }; | |||
| if (showPlaceholder) { | |||
| return ( | |||
| <Typography color="text.secondary" sx={{ py: 3 }}> | |||
| {EMPTY_MESSAGE} | |||
| </Typography> | |||
| ); | |||
| } | |||
| return ( | |||
| <Box | |||
| ref={containerRef} | |||
| @@ -60,7 +60,28 @@ | |||
| "Truck Routing Summary (Workbench)": "Truck Routing Summary (Workbench)", | |||
| "2/F or 4/F": "2/F or 4/F", | |||
| "Lane": "Lane", | |||
| "Shop Code": "Shop group", | |||
| "All lanes": "All lanes", | |||
| "All shops": "All shop groups", | |||
| "Cartons by lane": "Cartons by lane", | |||
| "Cartons by shop": "Cartons by shop", | |||
| "Cartons by shop group": "Cartons by shop group", | |||
| "Cartons by floor": "Cartons by floor", | |||
| "Share": "Share", | |||
| "Click a row to filter": "Click a row to filter", | |||
| "Click a bar to filter": "Click a bar to filter", | |||
| "Reset filters": "Reset filters", | |||
| "Cartons": "Cartons", | |||
| "Date": "Date", | |||
| "Download Excel": "Download Excel", | |||
| "Download this view Excel": "Download Excel (Selected)", | |||
| "Download period Excel": "Download Excel (All)", | |||
| "Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.", | |||
| "Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.", | |||
| "Filtered": "Filtered", | |||
| "Breakdown": "Breakdown", | |||
| "All floors": "All floors", | |||
| "FG carton qty filtered title": "FG carton qty (filtered) - {{floor}} - {{lane}} - {{shop}} - {{date}}", | |||
| "Generating...": "Generating...", | |||
| "Download report (PDF)": "Download report (PDF)", | |||
| "Unpicked orders confirm download": "This lane still has {{count}} unpicked order(s).\nPrint / download the truck routing summary anyway?", | |||
| @@ -624,9 +624,27 @@ | |||
| "FG carton qty this year title": "FG carton qty (this year) - {{floor}} - {{period}}", | |||
| "Exporting...": "Exporting...", | |||
| "Download Excel": "Download Excel", | |||
| "Download this view Excel": "Download Excel (Selected)", | |||
| "Download period Excel": "Download Excel (All)", | |||
| "Download this view Excel hint": "Excel of the current floor, lane, shop, and date, plus the chart breakdown.", | |||
| "Download period Excel hint": "Excel with last 7 days, this month, and this year. Not limited to the current view.", | |||
| "Filtered": "Filtered", | |||
| "Breakdown": "Breakdown", | |||
| "FG carton qty filtered title": "FG carton qty (filtered) - {{floor}} - {{lane}} - {{shop}} - {{date}}", | |||
| "Truck Routing Summary (Workbench)": "Truck Routing Summary (Workbench)", | |||
| "2/F or 4/F": "2/F or 4/F", | |||
| "Lane": "Lane", | |||
| "Shop Code": "Shop group", | |||
| "All lanes": "All lanes", | |||
| "All shops": "All shop groups", | |||
| "Cartons by lane": "Cartons by lane", | |||
| "Cartons by shop": "Cartons by shop", | |||
| "Cartons by shop group": "Cartons by shop group", | |||
| "Cartons by floor": "Cartons by floor", | |||
| "Share": "Share", | |||
| "Click a row to filter": "Click a row to filter", | |||
| "Click a bar to filter": "Click a bar to filter", | |||
| "Reset filters": "Reset filters", | |||
| "Generating...": "Generating...", | |||
| "Download report (PDF)": "Download report (PDF)", | |||
| "Unpicked orders confirm download": "This lane still has {{count}} unpicked order(s).\nPrint / download the truck routing summary anyway?", | |||
| @@ -55,7 +55,28 @@ | |||
| "Truck Routing Summary (Workbench)": "送貨路線摘要 (Workbench)", | |||
| "2/F or 4/F": "2/F 或 4/F", | |||
| "Lane": "車線", | |||
| "Shop Code": "店鋪組別", | |||
| "All lanes": "全部車線", | |||
| "All shops": "全部店鋪組別", | |||
| "Cartons by lane": "按車線統計箱數", | |||
| "Cartons by shop": "按店鋪編號統計箱數", | |||
| "Cartons by shop group": "按店鋪組別統計箱數", | |||
| "Cartons by floor": "按樓層統計箱數", | |||
| "Share": "佔比", | |||
| "Click a row to filter": "點選列以篩選", | |||
| "Click a bar to filter": "點選長條以篩選", | |||
| "Reset filters": "重設篩選", | |||
| "Cartons": "箱數", | |||
| "Date": "日期", | |||
| "Download Excel": "下載 Excel", | |||
| "Download this view Excel": "下載 Excel(已選)", | |||
| "Download period Excel": "下載 Excel(全部)", | |||
| "Download this view Excel hint": "匯出目前樓層、車線、店鋪組別、日期的箱數,以及圖表分項。", | |||
| "Download period Excel hint": "匯出近7天、本月、本年的箱數,不限目前篩選畫面。", | |||
| "Filtered": "篩選", | |||
| "Breakdown": "分項", | |||
| "All floors": "全部樓層", | |||
| "FG carton qty filtered title": "成品出倉出箱數量(篩選)- {{floor}} - {{lane}} - {{shop}} - {{date}}", | |||
| "Generating...": "生成中...", | |||
| "Download report (PDF)": "下載報告 (PDF)", | |||
| "Unpicked orders confirm download": "此車線仍有 {{count}} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?", | |||
| @@ -636,9 +636,27 @@ | |||
| "FG carton qty this year title": "成品出倉出箱數量(本年)- {{floor}} - {{period}}", | |||
| "Exporting...": "匯出中...", | |||
| "Download Excel": "下載 Excel", | |||
| "Download this view Excel": "下載 Excel(已選)", | |||
| "Download period Excel": "下載 Excel(全部)", | |||
| "Download this view Excel hint": "匯出目前樓層、車線、店鋪組別、日期的箱數,以及圖表分項。", | |||
| "Download period Excel hint": "匯出近7天、本月、本年的箱數,不限目前篩選畫面。", | |||
| "Filtered": "篩選", | |||
| "Breakdown": "分項", | |||
| "FG carton qty filtered title": "成品出倉出箱數量(篩選)- {{floor}} - {{lane}} - {{shop}} - {{date}}", | |||
| "Truck Routing Summary (Workbench)": "送貨路線摘要 (Workbench)", | |||
| "2/F or 4/F": "2/F 或 4/F", | |||
| "Lane": "車線", | |||
| "Shop Code": "店鋪組別", | |||
| "All lanes": "全部車線", | |||
| "All shops": "全部店鋪組別", | |||
| "Cartons by lane": "按車線統計箱數", | |||
| "Cartons by shop": "按店鋪編號統計箱數", | |||
| "Cartons by shop group": "按店鋪組別統計箱數", | |||
| "Cartons by floor": "按樓層統計箱數", | |||
| "Share": "佔比", | |||
| "Click a row to filter": "點選列以篩選", | |||
| "Click a bar to filter": "點選長條以篩選", | |||
| "Reset filters": "重設篩選", | |||
| "Generating...": "生成中...", | |||
| "Download report (PDF)": "下載報告 (PDF)", | |||
| "Unpicked orders confirm download": "此車線仍有 {{count}} 張訂單未執拾。\n是否仍要列印 / 下載送貨路線摘要?", | |||