"use client"; import React, { useState } from "react"; import { Box, Typography, Skeleton, Alert, TextField, CircularProgress, Button, Stack, Grid, Autocomplete, Chip, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, } from "@mui/material"; import dynamic from "next/dynamic"; import ShoppingCart from "@mui/icons-material/ShoppingCart"; import TableChart from "@mui/icons-material/TableChart"; import { fetchPurchaseOrderByStatus, fetchPurchaseOrderDetailsByStatus, fetchPurchaseOrderItems, fetchPurchaseOrderItemsByStatus, fetchPurchaseOrderFilterOptions, fetchPurchaseOrderEstimatedArrivalSummary, fetchPurchaseOrderEstimatedArrivalBreakdown, PurchaseOrderDetailByStatusRow, PurchaseOrderItemRow, PurchaseOrderChartFilters, PurchaseOrderFilterOptions, PurchaseOrderEstimatedArrivalRow, PurchaseOrderDrillQuery, PurchaseOrderEstimatedArrivalBreakdown, } from "@/app/api/chart/client"; import ChartCard from "../_components/ChartCard"; import { exportPurchaseChartMasterToFile } from "./exportPurchaseChartMaster"; import dayjs from "dayjs"; const ApexCharts = dynamic(() => import("react-apexcharts"), { ssr: false }); const PAGE_TITLE = "採購"; const DEFAULT_DRILL_STATUS = "completed"; /** Must match backend `getPurchaseOrderByStatus` (orderDate). Using "complete" here desyncs drill-down from the donut counts. */ const DRILL_DATE_FILTER = "order" as const; const EST_BUCKETS = ["delivered", "not_delivered", "cancelled", "other"] as const; /** 預計送貨 — 已送 / 未送 / 已取消 / 其他 */ const ESTIMATE_DONUT_COLORS = ["#2e7d32", "#f57c00", "#78909c", "#7b1fa2"]; /** 實際已送貨(依狀態)— 依序上色 */ const STATUS_DONUT_COLORS = ["#1565c0", "#00838f", "#6a1b9a", "#c62828", "#5d4037", "#00695c"]; /** ApexCharts + React: avoid updating state inside dataPointSelection synchronously (DOM getAttribute null). */ function deferChartClick(fn: () => void) { window.setTimeout(fn, 0); } /** UI labels only; API still uses English status values. */ function poStatusLabelZh(status: string): string { const s = status.trim().toLowerCase(); switch (s) { case "pending": return "待處理"; case "completed": return "已完成"; case "receiving": return "收貨中"; default: return status; } } function bucketLabelZh(bucket: string): string { switch (bucket) { case "delivered": return "已送"; case "not_delivered": return "未送"; case "cancelled": return "已取消"; case "other": return "其他"; default: return bucket; } } function emptyFilterOptions(): PurchaseOrderFilterOptions { return { suppliers: [], items: [], poNos: [] }; } export default function PurchaseChartPage() { const [poTargetDate, setPoTargetDate] = useState(() => dayjs().format("YYYY-MM-DD")); const [error, setError] = useState(null); const [chartData, setChartData] = useState<{ status: string; count: number }[]>([]); const [estimatedArrivalData, setEstimatedArrivalData] = useState([]); const [loading, setLoading] = useState(true); const [estimatedLoading, setEstimatedLoading] = useState(true); const [filterOptions, setFilterOptions] = useState(emptyFilterOptions); const [filterOptionsLoading, setFilterOptionsLoading] = useState(false); const [filterSupplierIds, setFilterSupplierIds] = useState([]); const [filterItemCodes, setFilterItemCodes] = useState([]); const [filterPoNos, setFilterPoNos] = useState([]); const [selectedStatus, setSelectedStatus] = useState(null); /** Prefer id (shop row); code-only used when supplierId missing */ const [selectedSupplierId, setSelectedSupplierId] = useState(null); const [selectedSupplierCode, setSelectedSupplierCode] = useState(null); const [selectedItemCode, setSelectedItemCode] = useState(null); /** 預計送貨 donut — filters lower charts via API */ const [selectedEstimatedBucket, setSelectedEstimatedBucket] = useState(null); const [poDetails, setPoDetails] = useState([]); const [poDetailsLoading, setPoDetailsLoading] = useState(false); const [selectedPo, setSelectedPo] = useState(null); const [itemsSummary, setItemsSummary] = useState([]); const [itemsSummaryLoading, setItemsSummaryLoading] = useState(false); const [poLineItems, setPoLineItems] = useState([]); const [poLineItemsLoading, setPoLineItemsLoading] = useState(false); const [masterExportLoading, setMasterExportLoading] = useState(false); const [eaBreakdown, setEaBreakdown] = useState(null); const [eaBreakdownLoading, setEaBreakdownLoading] = useState(false); const effectiveStatus = selectedStatus ?? DEFAULT_DRILL_STATUS; /** Top charts (實際已送貨 + 預計送貨): date + multi-select only — no drill-down from lower charts. */ const barFilters = React.useMemo((): PurchaseOrderChartFilters => { return { supplierIds: filterSupplierIds.length ? filterSupplierIds : undefined, itemCodes: filterItemCodes.length ? filterItemCodes : undefined, purchaseOrderNos: filterPoNos.length ? filterPoNos : undefined, }; }, [filterSupplierIds, filterItemCodes, filterPoNos]); /** Drill-down: bar filters ∩ supplier/貨品 chart selection. */ const drillFilters = React.useMemo((): PurchaseOrderChartFilters | null => { if ( selectedSupplierId != null && selectedSupplierId > 0 && filterSupplierIds.length > 0 && !filterSupplierIds.includes(selectedSupplierId) ) { return null; } if ( selectedItemCode?.trim() && filterItemCodes.length > 0 && !filterItemCodes.includes(selectedItemCode.trim()) ) { return null; } if (selectedSupplierCode?.trim() && filterSupplierIds.length > 0) { const opt = filterOptions.suppliers.find((s) => s.code === selectedSupplierCode.trim()); if (!opt || opt.supplierId <= 0 || !filterSupplierIds.includes(opt.supplierId)) { return null; } } const out: PurchaseOrderChartFilters = { supplierIds: filterSupplierIds.length ? [...filterSupplierIds] : undefined, itemCodes: filterItemCodes.length ? [...filterItemCodes] : undefined, purchaseOrderNos: filterPoNos.length ? [...filterPoNos] : undefined, }; if (selectedSupplierId != null && selectedSupplierId > 0) { if (!out.supplierIds?.length) { out.supplierIds = [selectedSupplierId]; } else if (out.supplierIds.includes(selectedSupplierId)) { out.supplierIds = [selectedSupplierId]; } out.supplierCode = undefined; } else if (selectedSupplierCode?.trim()) { const code = selectedSupplierCode.trim(); const opt = filterOptions.suppliers.find((s) => s.code === code); if (out.supplierIds?.length) { if (opt && opt.supplierId > 0 && out.supplierIds.includes(opt.supplierId)) { out.supplierIds = [opt.supplierId]; } else { return null; } } else { out.supplierIds = undefined; out.supplierCode = code; } } if (selectedItemCode?.trim()) { const ic = selectedItemCode.trim(); if (!out.itemCodes?.length) { out.itemCodes = [ic]; } else if (out.itemCodes.includes(ic)) { out.itemCodes = [ic]; } } return out; }, [ filterSupplierIds, filterItemCodes, filterPoNos, selectedSupplierId, selectedSupplierCode, selectedItemCode, filterOptions.suppliers, ]); const drillQueryOpts = React.useMemo((): PurchaseOrderDrillQuery | null => { if (drillFilters === null) return null; return { ...drillFilters, estimatedArrivalBucket: selectedEstimatedBucket ?? undefined, }; }, [drillFilters, selectedEstimatedBucket]); React.useEffect(() => { setFilterOptionsLoading(true); fetchPurchaseOrderFilterOptions(poTargetDate) .then(setFilterOptions) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setFilterOptionsLoading(false)); }, [poTargetDate]); React.useEffect(() => { setLoading(true); fetchPurchaseOrderByStatus(poTargetDate, barFilters) .then((data) => setChartData(data as { status: string; count: number }[])) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setLoading(false)); }, [poTargetDate, barFilters]); React.useEffect(() => { setEstimatedLoading(true); fetchPurchaseOrderEstimatedArrivalSummary(poTargetDate, barFilters) .then(setEstimatedArrivalData) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setEstimatedLoading(false)); }, [poTargetDate, barFilters]); React.useEffect(() => { if (!selectedEstimatedBucket || !poTargetDate) { setEaBreakdown(null); return; } setEaBreakdownLoading(true); fetchPurchaseOrderEstimatedArrivalBreakdown(poTargetDate, selectedEstimatedBucket, barFilters) .then(setEaBreakdown) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setEaBreakdownLoading(false)); }, [selectedEstimatedBucket, poTargetDate, barFilters]); React.useEffect(() => { if (drillQueryOpts === null) { setPoDetails([]); return; } setPoDetailsLoading(true); fetchPurchaseOrderDetailsByStatus(effectiveStatus, poTargetDate, { dateFilter: DRILL_DATE_FILTER, ...drillQueryOpts, }) .then((rows) => setPoDetails(rows)) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setPoDetailsLoading(false)); }, [effectiveStatus, poTargetDate, drillQueryOpts]); React.useEffect(() => { if (selectedPo) return; if (drillQueryOpts === null) { setItemsSummary([]); return; } setItemsSummaryLoading(true); fetchPurchaseOrderItemsByStatus(effectiveStatus, poTargetDate, { dateFilter: DRILL_DATE_FILTER, ...drillQueryOpts, }) .then((rows) => setItemsSummary(rows)) .catch((err) => setError(err instanceof Error ? err.message : "Request failed")) .finally(() => setItemsSummaryLoading(false)); }, [selectedPo, effectiveStatus, poTargetDate, drillQueryOpts]); React.useEffect(() => { if (selectedPo) return; setPoLineItems([]); setPoLineItemsLoading(false); }, [selectedPo]); const handleStatusClick = (status: string) => { const normalized = status.trim().toLowerCase(); setSelectedStatus((prev) => (prev === normalized ? null : normalized)); /** 與「預計送貨」圓環互斥:只顯示一則上方圓環篩選說明 */ setSelectedEstimatedBucket(null); setSelectedPo(null); setSelectedSupplierId(null); setSelectedSupplierCode(null); setSelectedItemCode(null); setPoLineItems([]); }; const handleEstimatedBucketClick = (index: number) => { const bucket = EST_BUCKETS[index]; if (!bucket) return; setSelectedEstimatedBucket((prev) => (prev === bucket ? null : bucket)); /** 與「實際已送貨」圓環互斥:只顯示一則上方圓環篩選說明 */ setSelectedStatus(null); setSelectedPo(null); setPoLineItems([]); }; const handleClearFilters = () => { setFilterSupplierIds([]); setFilterItemCodes([]); setFilterPoNos([]); setSelectedSupplierId(null); setSelectedSupplierCode(null); setSelectedItemCode(null); setSelectedEstimatedBucket(null); setSelectedStatus(null); setSelectedPo(null); setPoLineItems([]); }; const handleItemSummaryClick = (index: number) => { const row = itemsSummary[index]; if (!row?.itemCode) return; setSelectedItemCode((prev) => (prev === row.itemCode ? null : row.itemCode)); setSelectedPo(null); setPoLineItems([]); }; const handleSupplierClick = (row: { supplierId: number | null; supplierCode: string; }) => { if (row.supplierId != null && row.supplierId > 0) { setSelectedSupplierId((prev) => (prev === row.supplierId ? null : row.supplierId)); setSelectedSupplierCode(null); } else if (row.supplierCode.trim()) { setSelectedSupplierCode((prev) => (prev === row.supplierCode ? null : row.supplierCode)); setSelectedSupplierId(null); } setSelectedPo(null); setPoLineItems([]); }; const handlePoClick = async (row: PurchaseOrderDetailByStatusRow) => { setSelectedPo(row); setPoLineItems([]); setPoLineItemsLoading(true); try { const rows = await fetchPurchaseOrderItems(row.purchaseOrderId); setPoLineItems(rows); } catch (err) { setError(err instanceof Error ? err.message : "Request failed"); } finally { setPoLineItemsLoading(false); } }; const supplierChartData = React.useMemo(() => { const map = new Map< string, { supplier: string; supplierId: number | null; supplierCode: string; count: number; totalQty: number; } >(); poDetails.forEach((row) => { const sid = row.supplierId != null && row.supplierId > 0 ? row.supplierId : null; const code = String(row.supplierCode ?? "").trim(); const name = String(row.supplierName ?? "").trim(); const label = `${code} ${name}`.trim() || (sid != null ? `(Supplier #${sid})` : "(Unknown supplier)"); const key = sid != null ? `sid:${sid}` : `code:${code}|name:${name}`; const curr = map.get(key) ?? { supplier: label, supplierId: sid, supplierCode: code, count: 0, totalQty: 0, }; curr.count += 1; curr.totalQty += Number(row.totalQty ?? 0); map.set(key, curr); }); return Array.from(map.values()).sort((a, b) => b.totalQty - a.totalQty); }, [poDetails]); const estimatedChartSeries = React.useMemo(() => { const m = new Map(estimatedArrivalData.map((r) => [r.bucket, r.count])); return EST_BUCKETS.map((b) => m.get(b) ?? 0); }, [estimatedArrivalData]); const handleExportPurchaseMaster = React.useCallback(async () => { setMasterExportLoading(true); try { const exportedAtIso = new Date().toISOString(); const filterSupplierText = filterOptions.suppliers .filter((s) => filterSupplierIds.includes(s.supplierId)) .map((s) => `${s.code} ${s.name}`.trim()) .join(";") || "(未選)"; const filterItemText = filterOptions.items .filter((i) => filterItemCodes.includes(i.itemCode)) .map((i) => `${i.itemCode} ${i.itemName}`.trim()) .join(";") || "(未選)"; const filterPoText = filterPoNos.length ? filterPoNos.join(";") : "(未選)"; const metaRows: Record[] = [ { 項目: "匯出時間_UTC", 值: exportedAtIso }, { 項目: "訂單日期", 值: poTargetDate }, { 項目: "多選_供應商", 值: filterSupplierText }, { 項目: "多選_貨品", 值: filterItemText }, { 項目: "多選_採購單號", 值: filterPoText }, { 項目: "預計送貨圓環_點選", 值: selectedEstimatedBucket ? bucketLabelZh(selectedEstimatedBucket) : "(未選)", }, { 項目: "實際已送貨圓環_點選狀態", 值: selectedStatus != null ? poStatusLabelZh(selectedStatus) : `(未選;下方圖表預設狀態 ${poStatusLabelZh(DEFAULT_DRILL_STATUS)})`, }, { 項目: "下方圖表套用狀態_英文", 值: effectiveStatus }, { 項目: "下方圖表套用狀態_中文", 值: poStatusLabelZh(effectiveStatus) }, { 項目: "圓環篩選_供應商", 值: selectedSupplierId != null && selectedSupplierId > 0 ? `supplierId=${selectedSupplierId}` : selectedSupplierCode?.trim() ? `supplierCode=${selectedSupplierCode.trim()}` : "(未選)", }, { 項目: "圓環篩選_貨品", 值: selectedItemCode?.trim() ? selectedItemCode.trim() : "(未選)" }, { 項目: "下方查詢是否有效", 值: drillQueryOpts === null ? "否(篩選交集無效,下方表為空)" : "是", }, { 項目: "圖表說明", 值: "預計送貨圖:預計到貨日=訂單日期。實際已送貨圖:訂單日期。貨品/供應商/採購單表:依下方查詢與預計送貨扇形。採購單行明細:匯出當前列表中每張採購單之全部行。", }, ]; const estimatedDonutRows = EST_BUCKETS.map((b, i) => ({ 類別: bucketLabelZh(b), bucket代碼: b, 數量: estimatedChartSeries[i] ?? 0, })); const actualStatusDonutRows = chartData.map((p) => ({ 狀態中文: poStatusLabelZh(p.status), status代碼: p.status, 數量: p.count, })); const itemSummaryRows = itemsSummary.map((i) => ({ 貨品: i.itemCode, 名稱: i.itemName, 訂購數量: i.orderedQty, 已收貨: i.receivedQty, 待收貨: i.pendingQty, UOM: i.uom, })); const supplierDistributionRows = supplierChartData.map((s) => ({ 供應商: s.supplier, 供應商編號: s.supplierCode, supplierId: s.supplierId ?? "", 採購單數: s.count, 總數量: s.totalQty, })); const purchaseOrderListRows = poDetails.map((p) => ({ 採購單號: p.purchaseOrderNo, 狀態: poStatusLabelZh(p.status), status代碼: p.status, 訂單日期: p.orderDate, 預計到貨日: p.estimatedArrivalDate, 供應商編號: p.supplierCode, 供應商名稱: p.supplierName, supplierId: p.supplierId ?? "", 項目數: p.itemCount, 總數量: p.totalQty, })); const purchaseOrderLineRows: Record[] = []; if (poDetails.length > 0) { const lineBatches = await Promise.all( poDetails.map((po) => fetchPurchaseOrderItems(po.purchaseOrderId).then((lines) => lines.map((line) => ({ 採購單號: po.purchaseOrderNo, 採購單ID: po.purchaseOrderId, 狀態: poStatusLabelZh(po.status), 訂單日期: po.orderDate, 預計到貨日: po.estimatedArrivalDate, 供應商編號: po.supplierCode, 供應商名稱: po.supplierName, 貨品: line.itemCode, 品名: line.itemName, UOM: line.uom, 訂購數量: line.orderedQty, 已收貨: line.receivedQty, 待收貨: line.pendingQty, })) ) ) ); lineBatches.flat().forEach((row) => purchaseOrderLineRows.push(row)); } exportPurchaseChartMasterToFile( { exportedAtIso, metaRows, estimatedDonutRows, actualStatusDonutRows, itemSummaryRows, supplierDistributionRows, purchaseOrderListRows, purchaseOrderLineRows, }, `採購圖表總表_${poTargetDate}_${dayjs().format("HHmmss")}` ); } catch (err) { setError(err instanceof Error ? err.message : "總表匯出失敗"); } finally { setMasterExportLoading(false); } }, [ poTargetDate, filterOptions.suppliers, filterOptions.items, filterSupplierIds, filterItemCodes, filterPoNos, selectedEstimatedBucket, selectedStatus, effectiveStatus, drillQueryOpts, estimatedChartSeries, chartData, itemsSummary, supplierChartData, poDetails, selectedSupplierId, selectedSupplierCode, selectedItemCode, ]); const itemChartKey = `${effectiveStatus}|${poTargetDate}|${DRILL_DATE_FILTER}|${JSON.stringify(drillQueryOpts)}|ea:${selectedEstimatedBucket ?? ""}|s`; const supplierChartKey = `${itemChartKey}|${selectedItemCode ?? ""}|sup`; const poChartKey = `${supplierChartKey}|po`; /** 下方三張圖:僅選預計送貨扇形時,資料依 bucket 不再套用「實際已送貨」單一狀態。 */ const lowerChartsTitlePrefix = React.useMemo(() => { if (selectedEstimatedBucket) { return `預計送貨「${bucketLabelZh(selectedEstimatedBucket)}」`; } return `實際已送貨 ${poStatusLabelZh(effectiveStatus)}`; }, [selectedEstimatedBucket, effectiveStatus]); const lowerChartsDefaultStatusHint = React.useMemo(() => { if (selectedEstimatedBucket) return ""; if (selectedStatus) return ""; return `(預設狀態:${poStatusLabelZh(DEFAULT_DRILL_STATUS)})`; }, [selectedEstimatedBucket, selectedStatus]); const filterHint = [ selectedItemCode ? `貨品: ${selectedItemCode}` : null, selectedSupplierId != null && selectedSupplierId > 0 ? `供應商 id: ${selectedSupplierId}` : selectedSupplierCode ? `供應商 code: ${selectedSupplierCode}` : null, ] .filter(Boolean) .join(" · "); const hasBarFilters = filterSupplierIds.length > 0 || filterItemCodes.length > 0 || filterPoNos.length > 0; const hasChartDrill = selectedStatus != null || selectedItemCode || selectedSupplierId != null || selectedSupplierCode || selectedEstimatedBucket || selectedPo; return ( {PAGE_TITLE} {error && ( setError(null)}> {error} )} 上方「預計送貨」與「實際已送貨」依查詢日期與篩選條件;點擊圓環可篩選下方圖表(與其他條件交集)。 {(hasBarFilters || hasChartDrill) && ( )} setPoTargetDate(e.target.value)} InputLabelProps={{ shrink: true }} sx={{ minWidth: 160 }} /> `${o.code} ${o.name}`.trim() || String(o.supplierId)} value={filterOptions.suppliers.filter((s) => filterSupplierIds.includes(s.supplierId))} onChange={(_, v) => setFilterSupplierIds(v.map((x) => x.supplierId))} renderTags={(tagValue, getTagProps) => tagValue.map((option, index) => ( )) } renderInput={(params) => } /> `${o.itemCode} ${o.itemName}`.trim()} value={filterOptions.items.filter((i) => filterItemCodes.includes(i.itemCode))} onChange={(_, v) => setFilterItemCodes(v.map((x) => x.itemCode))} renderTags={(tagValue, getTagProps) => tagValue.map((option, index) => ( )) } renderInput={(params) => } /> o.poNo} value={filterOptions.poNos.filter((p) => filterPoNos.includes(p.poNo))} onChange={(_, v) => setFilterPoNos(v.map((x) => x.poNo))} renderTags={(tagValue, getTagProps) => tagValue.map((option, index) => ( )) } renderInput={(params) => } /> ({ 類別: bucketLabelZh(b), 數量: estimatedChartSeries[i] ?? 0, }))} > {estimatedLoading ? ( ) : ( { const idx = config?.dataPointIndex ?? -1; if (idx < 0 || idx >= EST_BUCKETS.length) return; deferChartClick(() => handleEstimatedBucketClick(idx)); }, }, animations: { enabled: false }, }, labels: EST_BUCKETS.map(bucketLabelZh), colors: ESTIMATE_DONUT_COLORS, legend: { position: "bottom" }, plotOptions: { pie: { donut: { labels: { show: true, total: { show: true, label: "預計送貨", }, }, }, }, }, }} series={estimatedChartSeries} type="donut" width="100%" height={320} /> )} ({ 狀態: poStatusLabelZh(p.status), 數量: p.count }))} > {loading ? ( ) : ( { const idx = config?.dataPointIndex ?? -1; if (idx < 0 || idx >= chartData.length) return; const row = chartData[idx]; if (!row?.status) return; const status = row.status; deferChartClick(() => handleStatusClick(status)); }, }, animations: { enabled: false }, }, labels: chartData.map((p) => poStatusLabelZh(p.status)), colors: chartData.map((_, i) => STATUS_DONUT_COLORS[i % STATUS_DONUT_COLORS.length]), legend: { position: "bottom" }, }} series={chartData.map((p) => p.count)} type="donut" width="100%" height={320} /> )} {selectedEstimatedBucket && ( 「{bucketLabelZh(selectedEstimatedBucket)}」關聯對象 條件與左側「預計送貨」圓環一致:預計到貨日 = 查詢日期({poTargetDate}),並含上方供應商/貨品/採購單號多選。 {eaBreakdownLoading ? ( ) : eaBreakdown ? ( 供應商 編號 名稱 採購單數 {eaBreakdown.suppliers.length === 0 ? ( ) : ( eaBreakdown.suppliers.map((s, i) => ( {s.supplierCode} {s.supplierName} {s.poCount} )) )}
貨品 貨品編號 名稱 採購單數 總數量 {eaBreakdown.items.length === 0 ? ( ) : ( eaBreakdown.items.map((it, i) => ( {it.itemCode} {it.itemName} {it.poCount} {it.totalQty} )) )}
採購單 採購單號 供應商 狀態 訂單日期 {eaBreakdown.purchaseOrders.length === 0 ? ( ) : ( eaBreakdown.purchaseOrders.map((po) => ( {po.purchaseOrderNo} {`${po.supplierCode} ${po.supplierName}`.trim()} {poStatusLabelZh(po.status)} {po.orderDate} )) )}
) : null}
)} {(selectedEstimatedBucket || selectedStatus != null) && ( {selectedEstimatedBucket && ( 下方圖表已依「預計送貨」篩選:{bucketLabelZh(selectedEstimatedBucket)} (預計到貨日 = 查詢日期,並含多選;此時不再套用右側「實際已送貨」狀態;再點同一扇形可取消)。 )} {selectedStatus != null && ( 下方圖表已依「實際已送貨」所選狀態:{poStatusLabelZh(selectedStatus)}(再點同一狀態可取消)。 )} )} ({ 貨品: i.itemCode, 名稱: i.itemName, 訂購數量: i.orderedQty, 已收貨: i.receivedQty, UOM: i.uom, }))} > {drillQueryOpts === null ? ( 無符合交集的篩選(請調整上方條件或圖表點選) ) : itemsSummaryLoading ? ( ) : itemsSummary.length === 0 ? ( 無資料(請確認訂單日期{selectedEstimatedBucket ? "與篩選" : "與狀態"}) ) : ( { const idx = config?.dataPointIndex ?? -1; if (idx < 0 || idx >= itemsSummary.length) return; deferChartClick(() => handleItemSummaryClick(idx)); }, }, animations: { enabled: false }, }, labels: itemsSummary.map((i) => `${i.itemCode} ${i.itemName}`.trim()), legend: { position: "bottom" }, plotOptions: { pie: { donut: { labels: { show: true, total: { show: true, label: "貨品", }, }, }, }, }, }} series={itemsSummary.map((i) => i.orderedQty)} type="donut" width="100%" height={380} /> )} ({ 供應商: s.supplier, 採購單數: s.count, 總數量: s.totalQty, }))} > {drillQueryOpts === null ? ( 無符合交集的篩選 ) : poDetailsLoading ? ( ) : supplierChartData.length === 0 ? ( 無供應商資料(請先確認上方貨品篩選或日期) ) : ( { const idx = config?.dataPointIndex ?? -1; if (idx < 0 || idx >= supplierChartData.length) return; const row = supplierChartData[idx]; if (!row.supplierId && !row.supplierCode?.trim()) return; deferChartClick(() => handleSupplierClick(row)); }, }, animations: { enabled: false }, }, labels: supplierChartData.map((s) => s.supplier), legend: { position: "bottom" }, }} series={supplierChartData.map((s) => s.totalQty)} type="donut" width="100%" height={360} /> )} ({ 採購單號: p.purchaseOrderNo, 供應商: `${p.supplierCode} ${p.supplierName}`.trim(), 總數量: p.totalQty, 項目數: p.itemCount, 訂單日期: p.orderDate, }))} > {drillQueryOpts === null ? ( 無符合交集的篩選 ) : poDetailsLoading ? ( ) : poDetails.length === 0 ? ( 無採購單。請確認該「訂單日期」是否有此狀態的採購單。 ) : ( { const idx = config?.dataPointIndex ?? -1; if (idx < 0 || idx >= poDetails.length) return; const po = poDetails[idx]; deferChartClick(() => void handlePoClick(po)); }, }, animations: { enabled: false }, }, xaxis: { categories: poDetails.map((p) => p.purchaseOrderNo) }, dataLabels: { enabled: false }, }} series={[ { name: "總數量", data: poDetails.map((p) => p.totalQty), }, ]} type="bar" width="100%" height={360} /> )} {selectedPo && ( ({ 貨品: i.itemCode, 名稱: i.itemName, 訂購數量: i.orderedQty, 已收貨: i.receivedQty, 待收貨: i.pendingQty, UOM: i.uom, }))} > {poLineItemsLoading ? ( ) : ( `${i.itemCode} ${i.itemName}`.trim()) }, plotOptions: { bar: { horizontal: true } }, dataLabels: { enabled: false }, }} series={[ { name: "訂購數量", data: poLineItems.map((i) => i.orderedQty) }, { name: "待收貨", data: poLineItems.map((i) => i.pendingQty) }, ]} type="bar" width="100%" height={Math.max(320, poLineItems.length * 38)} /> )} )}
); }