diff --git a/src/app/api/dashboard/actions.ts b/src/app/api/dashboard/actions.ts index bd71988..931d4b4 100644 --- a/src/app/api/dashboard/actions.ts +++ b/src/app/api/dashboard/actions.ts @@ -193,12 +193,18 @@ export const testing = cache(async (queryParams?: Record) => { export interface GoodsReceiptStatusRow { supplierId: number | null; + supplierCode: string | null; supplierName: string; + purchaseOrderCode: string | null; + statistics: string; expectedNoOfDelivery: number; noOfOrdersReceivedAtDock: number; noOfItemsInspected: number; noOfItemsWithIqcIssue: number; noOfItemsCompletedPutAwayAtStore: number; + // When true, this PO should be hidden from the dashboard table, + // but still counted in the overall statistics (訂單已處理). + hideFromDashboard?: boolean; } export const fetchGoodsReceiptStatus = cache(async (date?: string) => { diff --git a/src/components/DashboardPage/DashboardPage.tsx b/src/components/DashboardPage/DashboardPage.tsx index 6265dd9..c9ad686 100644 --- a/src/components/DashboardPage/DashboardPage.tsx +++ b/src/components/DashboardPage/DashboardPage.tsx @@ -15,7 +15,7 @@ import OrderCompletionChart from "./chart/OrderCompletionChart"; import { EscalationResult } from "@/app/api/escalation"; import EscalationLogTable from "./escalation/EscalationLogTable"; import { TruckScheduleDashboard } from "./truckSchedule"; -import { GoodsReceiptStatus } from "./goodsReceiptStatus"; +import GoodsReceiptStatusNew from "./goodsReceiptStatus/GoodsReceiptStatusNew"; import { CardFilterContext } from "../CollapsibleCard/CollapsibleCard"; interface TabPanelProps { @@ -85,7 +85,7 @@ const DashboardPage: React.FC = ({ aria-label="dashboard tabs" > - + 0 ? getPendingLog().length : t("No")})`} @@ -99,7 +99,7 @@ const DashboardPage: React.FC = ({ - + { + const { t } = useTranslation("dashboard"); + const [selectedDate, setSelectedDate] = useState(dayjs()); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [lastUpdated, setLastUpdated] = useState(null); + const [screenCleared, setScreenCleared] = useState(false); + + const loadData = useCallback(async () => { + if (screenCleared) return; + try { + setLoading(true); + const dateParam = selectedDate.format('YYYY-MM-DD'); + const result = await fetchGoodsReceiptStatusClient(dateParam); + setData(result ?? []); + setLastUpdated(dayjs()); + } catch (error) { + console.error('Error fetching goods receipt status:', error); + setData([]); + } finally { + setLoading(false); + } + }, [selectedDate, screenCleared]); + + useEffect(() => { + if (screenCleared) return; + loadData(); + + const refreshInterval = setInterval(() => { + loadData(); + }, REFRESH_MS); + + return () => clearInterval(refreshInterval); + }, [loadData, screenCleared]); + + + const selectedDateLabel = useMemo(() => { + return selectedDate.format('YYYY-MM-DD'); + }, [selectedDate]); + + // Sort rows by supplier code alphabetically (A -> Z) + const sortedData = useMemo(() => { + return [...data].sort((a, b) => { + const codeA = (a.supplierCode || '').toUpperCase(); + const codeB = (b.supplierCode || '').toUpperCase(); + if (codeA < codeB) return -1; + if (codeA > codeB) return 1; + return 0; + }); + }, [data]); + + const totalStatistics = useMemo(() => { + // Overall statistics should count ALL POs, including those hidden from the table + const totalReceived = sortedData.reduce((sum, row) => sum + (row.noOfOrdersReceivedAtDock || 0), 0); + const totalExpected = sortedData.reduce((sum, row) => sum + (row.expectedNoOfDelivery || 0), 0); + return { received: totalReceived, expected: totalExpected }; + }, [sortedData]); + + type StatusKey = 'pending' | 'receiving' | 'accepted'; + + const getStatusKey = useCallback((row: GoodsReceiptStatusRow): StatusKey => { + // Only when the whole PO is processed (all items finished IQC and PO completed) + // should we treat it as "accepted" (已收貨). + if (row.noOfOrdersReceivedAtDock === 1) { + return 'accepted'; + } + + // If some items have been inspected or put away but the order is not fully processed, + // treat as "receiving" / "processing". + if ((row.noOfItemsInspected ?? 0) > 0 || (row.noOfItemsCompletedPutAwayAtStore ?? 0) > 0) { + return 'receiving'; + } + + // Otherwise, nothing has started yet -> "pending". + return 'pending'; + }, []); + + const renderStatusChip = useCallback((row: GoodsReceiptStatusRow) => { + const statusKey = getStatusKey(row); + const label = t(statusKey); + + // Color mapping: pending -> red, receiving -> yellow, accepted -> default/green-ish + const color = + statusKey === 'pending' + ? 'error' + : statusKey === 'receiving' + ? 'warning' + : 'success'; + + return ( + + ); + }, [getStatusKey, t]); + + if (screenCleared) { + return ( + + + + + {t("Screen cleared")} + + + + + + ); + } + + return ( + + + {/* Header */} + + + + {t("Date")}: + + + { + if (!value) return; + setSelectedDate(value); + }} + slotProps={{ + textField: { + size: "small", + sx: { minWidth: 160 } + } + }} + /> + + + 訂單已處理: {totalStatistics.received}/{totalStatistics.expected} + + + + + + + {t("Auto-refresh every 15 minutes")} | {t("Last updated")}: {lastUpdated ? lastUpdated.format('HH:mm:ss') : '--:--:--'} + + + + + + {/* Table */} + + {loading ? ( + + + + ) : ( + + + + + {t("Supplier")} + {t("Purchase Order Code")} + {t("Status")} + {t("No. of Items with IQC Issue")} + + + + {sortedData.length === 0 ? ( + + + + {t("No data available")} ({selectedDateLabel}) + + + + ) : ( + sortedData + .filter((row) => !row.hideFromDashboard) // hide completed/rejected POs from table only + .map((row, index) => ( + + + + + {row.supplierCode || '-'} + + + - + + + {row.supplierName || '-'} + + + + + {row.purchaseOrderCode || '-'} + + + {renderStatusChip(row)} + + + {row.noOfItemsWithIqcIssue ?? 0} + + + )) + )} + +
+
+ )} +
+
+
+ ); +}; + +export default GoodsReceiptStatusNew; +4 \ No newline at end of file diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index e51da6e..4154022 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -115,10 +115,6 @@ export const REPORTS: ReportDefinition[] = [ title: "庫存結餘報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-balance`, fields: [ - { label: "最後入倉日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, - { label: "最後入倉日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, - { label: "最後出倉日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false }, - { label: "最後出倉日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false }, { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, @@ -129,8 +125,12 @@ export const REPORTS: ReportDefinition[] = [ fields: [ { label: "出貨日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false }, { label: "出貨日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false }, - { label: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "提料人 Handler", name: "handler", type: "select", required: false, + multiple: true, + dynamicOptions: true, + dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/fg-stock-out-traceability-handlers`, + options: [] }, ] }, @@ -151,6 +151,11 @@ export const REPORTS: ReportDefinition[] = [ { label: "庫存日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, { label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "提料人 Handler", name: "handler", type: "select", required: false, + multiple: true, + dynamicOptions: true, + dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/material-stock-out-traceability-handlers`, + options: [] }, ] }, { diff --git a/src/i18n/en/dashboard.json b/src/i18n/en/dashboard.json index 9434bbf..914459f 100644 --- a/src/i18n/en/dashboard.json +++ b/src/i18n/en/dashboard.json @@ -103,5 +103,13 @@ "Column 1": "Column 1", "Column 2": "Column 2", "Column 3": "Column 3", - "No data available": "No data available" + "No data available": "No data available", + "Supplier Code": "Supplier Code", + "Supplier Name": "Supplier Name", + "Purchase Order Code": "Purchase Order Code", + "Statistics": "Statistics", + "Show Supplier Code": "Show Supplier Code", + "Show Purchase Order Codes": "Show Purchase Order Codes", + "x/y orders received": "x/y orders received", + "Goods Receipt Status New": "Goods Receipt Status" } diff --git a/src/i18n/zh/dashboard.json b/src/i18n/zh/dashboard.json index 78646d2..dacab8b 100644 --- a/src/i18n/zh/dashboard.json +++ b/src/i18n/zh/dashboard.json @@ -103,5 +103,14 @@ "Column 1": "欄位1", "Column 2": "欄位2", "Column 3": "欄位3", - "No data available": "暫無資料" + "No data available": "暫無資料", + "Supplier Code": "供應商編號", + "Supplier Name": "供應商名稱", + "Purchase Order Code": "採購訂單編號", + "Statistics": "統計", + "Show Supplier Code": "顯示供應商編號", + "Show Purchase Order Codes": "顯示採購訂單編號", + "x/y orders received": "x/y張單已處理", + "Goods Receipt Status New": "採購單接收狀態", + "Status": "狀態" }