From 263d12e2486f0d92257415b7fe9886c3eb4b1779 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 10 Feb 2026 14:00:21 +0800 Subject: [PATCH 01/22] update job pick dashboard --- src/app/api/jo/actions.ts | 10 ++++-- .../Jodetail/MaterialPickStatusTable.tsx | 35 +++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 5c33b1c..0aa7986 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -1201,9 +1201,15 @@ export interface MaterialPickStatusItem { pickStatus: string | null; } -export const fetchMaterialPickStatus = cache(async (): Promise => { +export const fetchMaterialPickStatus = cache(async (date?: string): Promise => { + const params = new URLSearchParams(); + if (date) params.set("date", date); // yyyy-MM-dd + + const qs = params.toString(); + const url = `${BASE_API_URL}/jo/material-pick-status${qs ? `?${qs}` : ""}`; + return await serverFetchJson( - `${BASE_API_URL}/jo/material-pick-status`, + url, { method: "GET", } diff --git a/src/components/Jodetail/MaterialPickStatusTable.tsx b/src/components/Jodetail/MaterialPickStatusTable.tsx index 4246138..a445100 100644 --- a/src/components/Jodetail/MaterialPickStatusTable.tsx +++ b/src/components/Jodetail/MaterialPickStatusTable.tsx @@ -15,6 +15,9 @@ import { Paper, CircularProgress, TablePagination, + FormControl, + Select, + MenuItem, } from '@mui/material'; import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; @@ -28,20 +31,18 @@ const MaterialPickStatusTable: React.FC = () => { const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const refreshCountRef = useRef(0); + const [selectedDate, setSelectedDate] = useState(dayjs().format("YYYY-MM-DD")); const [paginationController, setPaginationController] = useState({ pageNum: 0, pageSize: 10, }); + const loadData = useCallback(async () => { setLoading(true); try { - const result = await fetchMaterialPickStatus(); - // On second refresh, clear completed pick orders + const result = await fetchMaterialPickStatus(selectedDate); if (refreshCountRef.current >= 1) { - // const filtered = result.filter(item => - // item.pickStatus?.toLowerCase() !== 'completed' - //); setData(result); } else { setData(result || []); @@ -49,23 +50,19 @@ const MaterialPickStatusTable: React.FC = () => { refreshCountRef.current += 1; } catch (error) { console.error('Error fetching material pick status:', error); - setData([]); // Set empty array on error to stop loading + setData([]); } finally { setLoading(false); } - }, []); // Remove refreshCount from dependencies + }, [selectedDate]); useEffect(() => { - // Initial load loadData(); - - // Set up auto-refresh every 10 minutes const interval = setInterval(() => { loadData(); }, REFRESH_INTERVAL); - return () => clearInterval(interval); - }, [loadData]); // Only depend on loadData, which is now stable + }, [loadData]); const formatTime = (timeData: any): string => { if (!timeData) return ''; @@ -235,10 +232,18 @@ const MaterialPickStatusTable: React.FC = () => { {t("Material Pick Status")} - - - + + + + {loading ? ( From e5feedc2a700723f7b5bb839d3b8f86401274b67 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 10 Feb 2026 17:10:52 +0800 Subject: [PATCH 02/22] update --- src/config/reportConfig.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index 710105a..dd90ec8 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -194,14 +194,21 @@ export const REPORTS: ReportDefinition[] = [ ] }, { id: "rep-010", - title: "物料出倉追蹤報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-material-stock-out-traceability`, + title: "庫存品質檢測報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-item-qc-fail`, fields: [ { label: "物料編號 Item Code", name: "itemCode", type: "text", 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: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, - + { label: "QC 不合格日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, + { label: "QC 不合格日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, + ] + }, + { id: "rep-011", + title: "庫存明細報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-ledger`, + fields: [ + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "庫存日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, + { label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, ] }, ] \ No newline at end of file From ca8b3ea050e3eea4c5a2a00035de2c2dd31e2507 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 10 Feb 2026 20:53:07 +0800 Subject: [PATCH 03/22] update --- src/config/reportConfig.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index dd90ec8..03a975a 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -211,4 +211,13 @@ export const REPORTS: ReportDefinition[] = [ { label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, ] }, + { id: "rep-012", + title: "庫存盤點報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance`, + fields: [ + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "庫存日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, + { label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, + ] + }, ] \ No newline at end of file From e8ef71601f8a79a4fc020962fe8d81391ca121ac Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 10 Feb 2026 21:18:54 +0800 Subject: [PATCH 04/22] update --- src/config/reportConfig.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index 03a975a..fb31bd0 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -193,6 +193,7 @@ export const REPORTS: ReportDefinition[] = [ ] }, + { id: "rep-010", title: "庫存品質檢測報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-item-qc-fail`, @@ -220,4 +221,13 @@ export const REPORTS: ReportDefinition[] = [ { label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, ] }, + { id: "rep-013", + title: "物料出倉追蹤報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-material-stock-out-traceability`, + fields: [ + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "庫存日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, + { label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, + ] + }, ] \ No newline at end of file From 1059b8770af7e1c0ff0d3b5e9975d6a8725009d1 Mon Sep 17 00:00:00 2001 From: "Tommy\\2Fi-Staff" Date: Tue, 10 Feb 2026 21:42:43 +0800 Subject: [PATCH 05/22] update --- src/config/reportConfig.ts | 74 ++++++++------------------------------ 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index fb31bd0..63c4806 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -72,20 +72,10 @@ export const REPORTS: ReportDefinition[] = [ title: "入倉記錄報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-in-traceability`, fields: [ - { label: "倉存類別 Stock Category", name: "stockCategory", type: "select", required: true, - multiple: true, - options: [ - { label: "All", value: "All"}, - { label: "MAT", value: "MAT" }, - { label: "FG", value: "FG" }, - { label: "WIP", value: "WIP" }, - { label: "NM", value: "NM" }, - { label: "CMB", value: "CMB" } - ] - }, - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "入倉日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: true }, { label: "入倉日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: true }, + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, { @@ -93,13 +83,8 @@ export const REPORTS: ReportDefinition[] = [ title: "成品/半成品生產分析報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis`, fields: [ - { label: "倉存類別 Stock Category", name: "stockCategory", type: "select", required: false, - multiple: true, - options: [ - { label: "All", value: "All" }, - { label: "FG", value: "FG" }, - { label: "WIP", value: "WIP" } - ] }, + { label: "完成生產日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false, placeholder: "dd/mm/yyyy" }, + { label: "完成生產日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false, placeholder: "dd/mm/yyyy" }, { label: "物料編號 Item Code", name: "itemCode", type: "select", required: false, multiple: true, allowInput: true, @@ -107,8 +92,6 @@ export const REPORTS: ReportDefinition[] = [ dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`, dynamicOptionsParam: "stockCategory", options: [] }, - { label: "完成生產日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false, placeholder: "dd/mm/yyyy" }, - { label: "完成生產日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false, placeholder: "dd/mm/yyyy" }, { label: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, ] @@ -118,16 +101,8 @@ export const REPORTS: ReportDefinition[] = [ title: "庫存材料消耗趨勢報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-item-consumption-trend`, fields: [ - { label: "倉存類別 Stock Category", name: "stockCategory", type: "select", required: false, - multiple: true, - options: [ - { label: "All", value: "All" }, - { label: "MAT", value: "MAT" }, - { label: "WIP", value: "WIP" }, - { label: "NM", value: "NM" }, - { label: "FG", value: "FG" }, - { label: "CMB", value: "CMB" } - ] }, + { 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: "select", required: false, multiple: true, allowInput: true, @@ -135,8 +110,6 @@ export const REPORTS: ReportDefinition[] = [ dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-item-code-prefixes`, dynamicOptionsParam: "stockCategory", options: [] }, - { 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" }, ] @@ -145,26 +118,12 @@ export const REPORTS: ReportDefinition[] = [ id: "rep-007", title: "庫存結餘報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-balance`, - fields: [ - { label: "倉存類別 Stock Category", name: "stockCategory", type: "select", required: false, - multiple: true, - options: [ - { label: "All", value: "All" }, - { label: "MAT", value: "MAT" }, - { label: "WIP", value: "WIP" }, - { label: "NM", value: "NM" }, - { label: "FG", value: "FG" }, - { label: "CMB", value: "CMB" } - ] }, - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + 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: "存量:由 Current Balance Start", name: "balanceFilterStart", type: "number", required: false}, - { label: "存量:至 Current Balance End", name: "balanceFilterEnd", type: "number", required: false}, - { label: "存貨位置 Store Location", name: "storeLocation", type: "text", required: false, placeholder: "例如:2F-W201-#Z-01, 2F, W201" }, - + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, @@ -173,24 +132,20 @@ export const REPORTS: ReportDefinition[] = [ title: "成品出倉報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-fg-delivery-report`, fields: [ - - { label: "物料編號 Item Code", name: "itemCode", type: "text", 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: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, - + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, { id: "rep-009", title: "成品出倉追蹤報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-fg-stock-out-traceability`, fields: [ - - { label: "物料編號 Item Code", name: "itemCode", type: "text", 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: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, - + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, @@ -198,36 +153,37 @@ export const REPORTS: ReportDefinition[] = [ title: "庫存品質檢測報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-item-qc-fail`, fields: [ - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, + { label: "QC 不合格日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, { label: "QC 不合格日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, + { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, { id: "rep-011", title: "庫存明細報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-ledger`, fields: [ - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, { 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}, ] }, { id: "rep-012", title: "庫存盤點報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance`, fields: [ - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, { 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}, ] }, { id: "rep-013", title: "物料出倉追蹤報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-material-stock-out-traceability`, fields: [ - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, { 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}, ] }, ] \ No newline at end of file From d726d933b5a7bb94d5d548342ff73f1b4038f196 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Thu, 12 Feb 2026 00:32:50 +0800 Subject: [PATCH 06/22] Report Page Update --- src/config/reportConfig.ts | 129 +++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index 63c4806..2521d52 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -69,7 +69,7 @@ export const REPORTS: ReportDefinition[] = [ //}, { id: "rep-004", - title: "入倉記錄報告", + title: "入倉追蹤報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-in-traceability`, fields: [ @@ -78,42 +78,39 @@ export const REPORTS: ReportDefinition[] = [ { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, + { - id: "rep-005", - title: "成品/半成品生產分析報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis`, + id: "rep-008", + title: "成品出倉報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-fg-delivery-report`, fields: [ - { label: "完成生產日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false, placeholder: "dd/mm/yyyy" }, - { label: "完成生產日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false, placeholder: "dd/mm/yyyy" }, - { label: "物料編號 Item Code", name: "itemCode", type: "select", required: false, - multiple: true, - allowInput: true, - dynamicOptions: true, - dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`, - dynamicOptionsParam: "stockCategory", - options: [] }, + { 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}, ] }, - { - id: "rep-006", - title: "庫存材料消耗趨勢報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-item-consumption-trend`, + + { id: "rep-012", + title: "庫存盤點報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance`, fields: [ - { 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: "select", required: false, - multiple: true, - allowInput: true, - dynamicOptions: true, - dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-item-code-prefixes`, - dynamicOptionsParam: "stockCategory", - options: [] }, - { label: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, + { 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}, + ] + }, + { id: "rep-011", + title: "庫存明細報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-ledger`, + fields: [ + { 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}, ] }, + { id: "rep-007", title: "庫存結餘報告", @@ -126,11 +123,10 @@ export const REPORTS: ReportDefinition[] = [ { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, - - { - id: "rep-008", - title: "成品出倉報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-fg-delivery-report`, + + { id: "rep-009", + title: "成品出倉追蹤報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-fg-stock-out-traceability`, fields: [ { label: "出貨日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false }, { label: "出貨日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false }, @@ -138,18 +134,18 @@ export const REPORTS: ReportDefinition[] = [ { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, - { id: "rep-009", - title: "成品出倉追蹤報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-fg-stock-out-traceability`, + + { id: "rep-013", + title: "物料出倉追蹤報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-material-stock-out-traceability`, 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: "庫存日期:由 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}, ] }, - { id: "rep-010", + { id: "rep-010", title: "庫存品質檢測報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-item-qc-fail`, fields: [ @@ -159,31 +155,40 @@ export const REPORTS: ReportDefinition[] = [ { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, - { id: "rep-011", - title: "庫存明細報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-ledger`, - fields: [ - { 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}, - ] - }, - { id: "rep-012", - title: "庫存盤點報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance`, + + { + id: "rep-006", + title: "庫存材料消耗趨勢報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-item-consumption-trend`, fields: [ - { 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: "材料消耗日期:由 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: "select", required: false, + multiple: true, + allowInput: true, + dynamicOptions: true, + dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-item-code-prefixes`, + dynamicOptionsParam: "stockCategory", + options: [] }, ] }, - { id: "rep-013", - title: "物料出倉追蹤報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-material-stock-out-traceability`, + + { + id: "rep-005", + title: "成品/半成品生產分析報告", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis`, fields: [ - { 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: "完成生產日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false, placeholder: "dd/mm/yyyy" }, + { label: "完成生產日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false, placeholder: "dd/mm/yyyy" }, + { label: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, + { label: "物料編號 Item Code", name: "itemCode", type: "select", required: false, + multiple: true, + allowInput: true, + dynamicOptions: true, + dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`, + dynamicOptionsParam: "stockCategory", + options: [] }, ] - }, + } ] \ No newline at end of file From eb9714a79b016dbc272495e725084341775e756f Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Fri, 13 Feb 2026 02:40:24 +0800 Subject: [PATCH 07/22] update --- src/config/reportConfig.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index 63c4806..c336549 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -163,8 +163,8 @@ export const REPORTS: ReportDefinition[] = [ title: "庫存明細報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-ledger`, fields: [ - { label: "庫存日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, - { label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, + { label: "開始日期:由 Start Date Start", name: "startDateStart", type: "date", required: false }, + { label: "結束日期:至 End Date End", name: "startDateEnd", type: "date", required: false }, { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, @@ -172,8 +172,8 @@ export const REPORTS: ReportDefinition[] = [ title: "庫存盤點報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance`, fields: [ - { label: "庫存日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, - { label: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, + { label: "開始日期:由 Start Date Start", name: "startDateStart", type: "date", required: false }, + { label: "結束日期:至 End Date End", name: "startDateEnd", type: "date", required: false }, { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, From 0eb0936e4582b0f706f07f5abb3c993d78563ab8 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Mon, 16 Feb 2026 12:02:22 +0800 Subject: [PATCH 08/22] Update --- src/config/reportConfig.ts | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index 2b013f9..74c26e4 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -78,7 +78,6 @@ export const REPORTS: ReportDefinition[] = [ { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, - { id: "rep-008", title: "成品出倉報告", @@ -145,24 +144,6 @@ export const REPORTS: ReportDefinition[] = [ { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, - { id: "rep-011", - title: "庫存明細報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-ledger`, - fields: [ - { label: "開始日期:由 Start Date Start", name: "startDateStart", type: "date", required: false }, - { label: "結束日期:至 End Date End", name: "startDateEnd", type: "date", required: false }, - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, - ] - }, - { id: "rep-012", - title: "庫存盤點報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-stock-take-variance`, - fields: [ - { label: "開始日期:由 Start Date Start", name: "startDateStart", type: "date", required: false }, - { label: "結束日期:至 End Date End", name: "startDateEnd", type: "date", required: false }, - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, - ] - }, { id: "rep-013", title: "物料出倉追蹤報告", apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-material-stock-out-traceability`, @@ -172,18 +153,6 @@ export const REPORTS: ReportDefinition[] = [ { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, ] }, - - { id: "rep-010", - title: "庫存品質檢測報告", - apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-item-qc-fail`, - fields: [ - - { label: "QC 不合格日期:由 Last In Date Start", name: "lastInDateStart", type: "date", required: false }, - { label: "QC 不合格日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, - { label: "物料編號 Item Code", name: "itemCode", type: "text", required: false}, - ] - }, - { id: "rep-006", title: "庫存材料消耗趨勢報告", From b0356b7a8a193af4a1cfeade54c128f2541d7959 Mon Sep 17 00:00:00 2001 From: "PC-20260115JRSN\\Administrator" Date: Mon, 23 Feb 2026 11:41:35 +0800 Subject: [PATCH 09/22] Fix the files that make project failed to compile --- src/app/(main)/do/edit/page.tsx | 2 +- src/app/(main)/ps/page.tsx | 54 +- src/app/(main)/report/page.tsx | 30 +- .../report/semiFGProductionAnalysisApi.ts | 50 +- src/app/(main)/testing/page.tsx | 36 +- src/app/api/qc/index.ts | 6 +- .../api/settings/m18ImportTesting/actions.ts | 10 +- src/app/api/user/actions.ts | 3 +- src/app/utils/clientAuthFetch.ts | 31 + src/components/CreateUser/CreateUser.tsx | 5 +- .../DetailedScheduleSearchView.tsx | 4 +- src/components/DoSearch/DoSearchWrapper.tsx | 2 +- .../EscalationComponent.tsx | 2 +- .../PickQcStockInModalVer2.tsx | 12 +- .../PickQcStockInModalVer3.tsx | 2 +- .../FinishedGoodSearch/StockInFormVer2.tsx | 4 +- .../FinishedGoodSearch/newcreatitem copy.tsx | 703 ------------------ .../FinishedGoodSearch/pickorderModelVer2.tsx | 7 +- .../InventorySearch/InventorySearch.tsx | 1 + src/components/ItemsSearch/ItemsSearch.tsx | 2 +- .../Jodetail/EscalationComponent.tsx | 2 +- src/components/Jodetail/StockInFormVer2.tsx | 4 +- .../PickOrderSearch/EscalationComponent.tsx | 2 +- src/components/PickOrderSearch/LotTable.tsx | 3 +- .../PickOrderSearch/PickExecution.tsx | 4 +- .../PickQcStockInModalVer2.tsx | 12 +- .../PickOrderSearch/StockInFormVer2.tsx | 4 +- .../PickOrderSearch/pickorderModelVer2.tsx | 7 +- src/components/PoDetail/EscalationTab.tsx | 5 +- src/components/PoDetail/QCDatagrid.tsx | 2 +- src/components/PoDetail/QcFormOld.tsx | 14 +- src/components/PoDetail/QcStockInModal.tsx | 2 +- src/components/PoDetail/QrModal.tsx | 5 + .../ProductionOutputFormPage.tsx | 17 +- src/theme/EmotionCache.tsx | 2 +- tsconfig.json | 2 +- 36 files changed, 173 insertions(+), 880 deletions(-) create mode 100644 src/app/utils/clientAuthFetch.ts diff --git a/src/app/(main)/do/edit/page.tsx b/src/app/(main)/do/edit/page.tsx index a9d8d94..857f3b1 100644 --- a/src/app/(main)/do/edit/page.tsx +++ b/src/app/(main)/do/edit/page.tsx @@ -1,5 +1,5 @@ import { SearchParams } from "@/app/utils/fetchUtil"; -import DoDetail from "@/components/DoDetail/DodetailWrapper"; +import DoDetail from "@/components/DoDetail/DoDetailWrapper"; import { I18nProvider, getServerI18n } from "@/i18n"; import { Typography } from "@mui/material"; import { isArray } from "lodash"; diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index 0d74e1c..b008657 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -12,8 +12,8 @@ import { OnlinePrediction, FileDownload, SettingsEthernet } from "@mui/icons-material"; import dayjs from "dayjs"; -import { redirect } from "next/navigation"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; export default function ProductionSchedulePage() { // ── Main states ── @@ -69,21 +69,14 @@ export default function ProductionSchedulePage() { // ── API Actions ── const handleSearch = async () => { - const token = localStorage.getItem("accessToken"); setLoading(true); try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, { method: 'GET', - headers: { 'Authorization': `Bearer ${token}` } }); - if (response.status === 401 || response.status === 403) { - console.warn(`Auth error ${response.status} → clearing token & redirecting`); - window.location.href = "/login?session=expired"; - - return; // ← stops execution here - } + if (response.status === 401 || response.status === 403) return; const data = await response.json(); @@ -101,7 +94,6 @@ export default function ProductionSchedulePage() { return; } - const token = localStorage.getItem("accessToken"); setLoading(true); setIsForecastDialogOpen(false); @@ -113,10 +105,9 @@ export default function ProductionSchedulePage() { const url = `${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule?${params.toString()}`; - const response = await fetch(url, { - method: 'GET', - headers: { 'Authorization': `Bearer ${token}` } - }); + const response = await clientAuthFetch(url, { method: 'GET' }); + + if (response.status === 401 || response.status === 403) return; if (response.ok) { await handleSearch(); // refresh list @@ -140,7 +131,6 @@ export default function ProductionSchedulePage() { return; } - const token = localStorage.getItem("accessToken"); setLoading(true); setIsExportDialogOpen(false); @@ -149,11 +139,11 @@ export default function ProductionSchedulePage() { fromDate: exportFromDate, }); - const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, { - method: 'GET', // or keep POST if backend requires it - headers: { 'Authorization': `Bearer ${token}` } + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, { + method: 'GET', }); + if (response.status === 401 || response.status === 403) return; if (!response.ok) throw new Error(`Export failed: ${response.status}`); const blob = await response.blob(); @@ -183,25 +173,15 @@ export default function ProductionSchedulePage() { return; } - const token = localStorage.getItem("accessToken"); - console.log("Token exists:", !!token); - setSelectedPs(ps); setLoading(true); try { const url = `${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`; - console.log("Sending request to:", url); - const response = await fetch(url, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); + const response = await clientAuthFetch(url, { method: 'GET' }); - console.log("Response status:", response.status); - console.log("Response ok?", response.ok); + if (response.status === 401 || response.status === 403) return; if (!response.ok) { const errorText = await response.text().catch(() => "(no text)"); @@ -229,19 +209,17 @@ export default function ProductionSchedulePage() { const handleAutoGenJob = async () => { - //if (!isDateToday) return; - const token = localStorage.getItem("accessToken"); + //if (!isDateToday) return; setIsGenerating(true); try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: selectedPs.id }) }); + if (response.status === 401 || response.status === 403) return; + if (response.ok) { const data = await response.json(); const displayMessage = data.message || "Operation completed."; diff --git a/src/app/(main)/report/page.tsx b/src/app/(main)/report/page.tsx index 100c194..f170845 100644 --- a/src/app/(main)/report/page.tsx +++ b/src/app/(main)/report/page.tsx @@ -17,6 +17,7 @@ import { import PrintIcon from '@mui/icons-material/Print'; import { REPORTS, ReportDefinition } from '@/config/reportConfig'; import { NEXT_PUBLIC_API_URL } from '@/config/api'; +import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; import { fetchSemiFGItemCodes, @@ -90,25 +91,17 @@ export default function ReportPage() { } // Handle other reports with dynamic options - const token = localStorage.getItem("accessToken"); - - // Handle multiple stockCategory values (comma-separated) - // If "All" is included or no value, fetch all - // Otherwise, fetch for all selected categories let url = field.dynamicOptionsEndpoint; if (paramValue && paramValue !== 'All' && !paramValue.includes('All')) { - // Multiple categories selected (e.g., "FG,WIP") url = `${field.dynamicOptionsEndpoint}?${field.dynamicOptionsParam}=${paramValue}`; } - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, }); + if (response.status === 401 || response.status === 403) return; if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); @@ -159,21 +152,18 @@ export default function ReportPage() { const executePrint = async () => { if (!currentReport) return; - + setLoading(true); try { - const token = localStorage.getItem("accessToken"); const queryParams = new URLSearchParams(criteria).toString(); const url = `${currentReport.apiEndpoint}?${queryParams}`; - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/pdf', - }, + headers: { 'Accept': 'application/pdf' }, }); + if (response.status === 401 || response.status === 403) return; if (!response.ok) { const errorText = await response.text(); console.error("Response error:", errorText); diff --git a/src/app/(main)/report/semiFGProductionAnalysisApi.ts b/src/app/(main)/report/semiFGProductionAnalysisApi.ts index 04cfc8a..ba7a1d7 100644 --- a/src/app/(main)/report/semiFGProductionAnalysisApi.ts +++ b/src/app/(main)/report/semiFGProductionAnalysisApi.ts @@ -1,4 +1,7 @@ +"use client"; + import { NEXT_PUBLIC_API_URL } from '@/config/api'; +import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; export interface ItemCodeWithName { code: string; @@ -19,24 +22,18 @@ export interface ItemCodeWithCategory { export const fetchSemiFGItemCodes = async ( stockCategory: string = '' ): Promise => { - const token = localStorage.getItem("accessToken"); - let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`; if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { url = `${url}?stockCategory=${stockCategory}`; } - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); }; @@ -49,24 +46,18 @@ export const fetchSemiFGItemCodes = async ( export const fetchSemiFGItemCodesWithCategory = async ( stockCategory: string = '' ): Promise => { - const token = localStorage.getItem("accessToken"); - let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes-with-category`; if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { url = `${url}?stockCategory=${stockCategory}`; } - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); }; @@ -81,21 +72,16 @@ export const generateSemiFGProductionAnalysisReport = async ( criteria: Record, reportTitle: string = '成品/半成品生產分析報告' ): Promise => { - const token = localStorage.getItem("accessToken"); const queryParams = new URLSearchParams(criteria).toString(); const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis?${queryParams}`; - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/pdf', - }, + headers: { 'Accept': 'application/pdf' }, }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); diff --git a/src/app/(main)/testing/page.tsx b/src/app/(main)/testing/page.tsx index 3efaf70..ae22386 100644 --- a/src/app/(main)/testing/page.tsx +++ b/src/app/(main)/testing/page.tsx @@ -10,6 +10,7 @@ import { import { FileDownload, Print, SettingsEthernet, Lan, Router } from "@mui/icons-material"; import dayjs from "dayjs"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; // Simple TabPanel component for conditional rendering interface TabPanelProps { @@ -97,14 +98,14 @@ export default function TestingPage() { // TSC Print (Section 1) const handleTscPrint = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: tscConfig.ip, printerPort: tscConfig.port }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-tsc`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-tsc`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; if (response.ok) alert(`TSC Print Command Sent for ${row.itemCode}!`); else alert("TSC Print Failed"); } catch (e) { console.error("TSC Error:", e); } @@ -112,14 +113,14 @@ export default function TestingPage() { // DataFlex Print (Section 2) const handleDfPrint = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: dfConfig.ip, printerPort: dfConfig.port }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; if (response.ok) alert(`DataFlex Print Command Sent for ${row.itemCode}!`); else alert("DataFlex Print Failed"); } catch (e) { console.error("DataFlex Error:", e); } @@ -127,14 +128,13 @@ export default function TestingPage() { // OnPack Zip Download (Section 3) const handleDownloadPrintJob = async () => { - const token = localStorage.getItem("accessToken"); const params = new URLSearchParams(printerFormData); try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${params.toString()}`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${params.toString()}`, { method: 'GET', - headers: { 'Authorization': `Bearer ${token}` } }); + if (response.status === 401 || response.status === 403) return; if (!response.ok) throw new Error('Download failed'); const blob = await response.blob(); @@ -153,34 +153,33 @@ export default function TestingPage() { // Laser Print (Section 4 - original) const handleLaserPrint = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; if (response.ok) alert(`Laser Command Sent: ${row.templateId}`); } catch (e) { console.error(e); } }; const handleLaserPreview = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; if (response.ok) alert("Red light preview active!"); } catch (e) { console.error("Preview Error:", e); } }; // HANS600S-M TCP Print (Section 5) const handleHansPrint = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { printerIp: hansConfig.ip, printerPort: hansConfig.port, @@ -190,11 +189,12 @@ export default function TestingPage() { text4ObjectName: row.text4ObjectName }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; const result = await response.text(); if (response.ok) { alert(`HANS600S-M Mark Success: ${result}`); diff --git a/src/app/api/qc/index.ts b/src/app/api/qc/index.ts index b862d63..f079ec5 100644 --- a/src/app/api/qc/index.ts +++ b/src/app/api/qc/index.ts @@ -29,9 +29,9 @@ export interface QcData { name?: string, order?: number, description?: string, - // qcPassed: boolean | undefined - // failQty: number | undefined - // remarks: string | undefined + qcPassed?: boolean, + failQty?: number, + remarks?: string, } export interface QcResult extends QcData{ id?: number; diff --git a/src/app/api/settings/m18ImportTesting/actions.ts b/src/app/api/settings/m18ImportTesting/actions.ts index 4a9bc80..7fbdffc 100644 --- a/src/app/api/settings/m18ImportTesting/actions.ts +++ b/src/app/api/settings/m18ImportTesting/actions.ts @@ -2,7 +2,7 @@ // import { serverFetchWithNoContent } from '@/app/utils/fetchUtil'; // import { BASE_API_URL } from "@/config/api"; -import { serverFetchWithNoContent } from "../../../utils/fetchUtil"; +import { serverFetch, serverFetchWithNoContent } from "../../../utils/fetchUtil"; import { BASE_API_URL } from "../../../../config/api"; export interface M18ImportPoForm { @@ -85,13 +85,13 @@ export const triggerScheduler = async (type: 'po' | 'do1' | 'do2' | 'master-data console.log("Fetching URL:", url); - const response = await serverFetchWithNoContent(url, { + const response = await serverFetch(url, { method: "GET", cache: "no-store", }); if (!response.ok) throw new Error(`Failed: ${response.status}`); - + return await response.text(); } catch (error) { console.error("Scheduler Action Error:", error); @@ -103,13 +103,13 @@ export const refreshCronSchedules = async () => { // Simply reuse the triggerScheduler logic to avoid duplication // or call serverFetch directly as shown below: try { - const response = await serverFetchWithNoContent(`${BASE_API_URL}/scheduler/refresh-cron`, { + const response = await serverFetch(`${BASE_API_URL}/scheduler/refresh-cron`, { method: "GET", cache: "no-store", }); if (!response.ok) throw new Error(`Failed to refresh: ${response.status}`); - + return await response.text(); } catch (error) { console.error("Refresh Cron Error:", error); diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index 1fb5495..66c2ef4 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -13,10 +13,11 @@ export interface UserInputs { username: string; name: string; staffNo?: string; + locked?: boolean; addAuthIds?: number[]; removeAuthIds?: number[]; password?: string; - confirmPassword?: string; + confirmPassword?: string; } export interface PasswordInputs { diff --git a/src/app/utils/clientAuthFetch.ts b/src/app/utils/clientAuthFetch.ts new file mode 100644 index 0000000..1bc8462 --- /dev/null +++ b/src/app/utils/clientAuthFetch.ts @@ -0,0 +1,31 @@ +"use client"; + +const LOGIN_REDIRECT = "/login?session=expired"; + +/** + * Client-side fetch that adds Bearer token from localStorage and redirects + * to /login?session=expired on 401 or 403 (session timeout / unauthorized). + * Use this for all authenticated API requests so session expiry is handled consistently. + */ +export async function clientAuthFetch( + input: RequestInfo | URL, + init?: RequestInit +): Promise { + const token = + typeof window !== "undefined" ? localStorage.getItem("accessToken") : null; + const headers = new Headers(init?.headers); + if (token) { + headers.set("Authorization", `Bearer ${token}`); + } + + const response = await fetch(input, { ...init, headers }); + + if (response.status === 401 || response.status === 403) { + if (typeof window !== "undefined") { + console.warn(`Auth error ${response.status} → redirecting to login`); + window.location.href = LOGIN_REDIRECT; + } + } + + return response; +} diff --git a/src/components/CreateUser/CreateUser.tsx b/src/components/CreateUser/CreateUser.tsx index 4b6ea25..33b9e79 100644 --- a/src/components/CreateUser/CreateUser.tsx +++ b/src/components/CreateUser/CreateUser.tsx @@ -143,9 +143,10 @@ const CreateUser: React.FC = ({ rules, auths }) => { }); } } - const userData = { + const userData: UserInputs = { username: data.username, - // name: data.name, + name: data.name ?? "", + staffNo: data.staffNo, locked: false, addAuthIds: data.addAuthIds || [], removeAuthIds: data.removeAuthIds || [], diff --git a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx index ddccc5a..6c32223 100644 --- a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx +++ b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx @@ -26,7 +26,7 @@ import isToday from 'dayjs/plugin/isToday'; import useUploadContext from "../UploadProvider/useUploadContext"; import { FileDownload, CalendarMonth } from "@mui/icons-material"; import { useSession } from "next-auth/react"; -import { VIEW_USER } from "@/authorities"; +import { AUTH } from "@/authorities"; dayjs.extend(isToday); @@ -384,7 +384,7 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { {t("Export Schedule")} - {false && abilities.includes(VIEW_USER) && ( + {false && abilities.includes(AUTH.VIEW_USER) && ( } className="mb-4" />`. + - Do **not** put a bare `

` or `` as the main page heading; use PageTitleBar for consistency. + +2. **Content:** Fragments or divs with `space-y-4` (or `Stack spacing={2}` in MUI) between sections. No extra full-width background wrapper. + +### Search criteria + +- **When using the shared SearchBox component:** It already uses the standard card style. Ensure the parent page does not wrap it in another card. +- **When building a custom search/query bar:** Use the shared class so it matches SearchBox: + - Wrapper: `className="app-search-criteria ..."` (plus layout classes like `flex flex-wrap items-center gap-2 p-4`). + - Label for “Search criteria” style: `className="app-search-criteria-label"` if you need a small uppercase label. +- **Search button:** Primary action = blue (MUI `variant="contained"` `color="primary"`, or Tailwind `bg-blue-500 text-white`). Reset = outline with neutral border (e.g. MUI `variant="outlined"` with slate border, or Tailwind `border border-slate-300`). + +### Forms & inputs + +- **Standard look (enforced by MUI theme):** White background, border `#e2e8f0` (neutral 200), focus ring primary blue. Use MUI `TextField` / `FormControl` / date pickers as-is; the theme in `src/theme/devias-material-kit` already matches this. +- **Tailwind-only forms (e.g. /ps):** Use the same tokens: `border border-slate-300`, `bg-white`, `focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20`, `text-slate-900`, `placeholder-slate-400`. + +### Buttons + +- **Primary action:** Blue filled — MUI `variant="contained"` `color="primary"` or Tailwind `bg-blue-500 text-white hover:bg-blue-600`. +- **Secondary / cancel:** Outline, neutral — MUI `variant="outlined"` with border `#e2e8f0` / `#334155` text, or Tailwind `border border-slate-300 text-slate-700 hover:bg-slate-100`. +- **Accent (e.g. export, success):** Green — MUI `color="success"` or Tailwind `bg-emerald-500` / `text-emerald-600` for outline. +- **Spacing:** Use `gap-2` or `gap-4` between buttons; keep padding multiples of 4 (e.g. `px-4 py-2`). + +### Tables & grids + +- **Container:** Wrap tables/grids in a card-style container so they match across pages: + - MUI: `` (theme already uses 8px radius, neutral border). + - Tailwind: `rounded-lg border border-slate-200 bg-white shadow-sm`. +- **Data grid (MUI X DataGrid):** Use `StyledDataGrid` from `@/components/StyledDataGrid`. It applies header bg neutral[50], header text neutral[700], cell padding and borders to match the standard. +- **Table (MUI Table):** Use `SearchResults` when you have a paginated list; it uses `Paper variant="outlined"` and theme table styles (header bg, borders). +- **Header row:** Background `bg-slate-50` / `neutral[50]`, text `text-slate-700` / `neutral[700]`, font-weight 600, padding `px-4 py-3` or theme default. +- **Body rows:** Border `border-slate-200` / theme divider, hover `hover:bg-slate-50` / `action.hover`. + +### Cards & surfaces + +- **Standard card:** 8px radius, 1px border (`var(--border)` or `neutral[200]`), white background (`var(--card)`), light shadow (`0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)`). MUI `Card` and `Paper` are themed to match. +- **Search-criteria card:** Use class `app-search-criteria` (left 4px primary border, same radius and shadow as above). + +### Menu bar & sidebar + +- **App bar (top):** White background, 1px bottom border (`palette.divider`), no heavy shadow (`elevation={0}`). Toolbar with consistent min-height and horizontal padding. Profile and title use `text.secondary` and font-weight 600. +- **Sidebar (navigation drawer):** Same as cards: white background, 1px right border, light shadow. Logo area with padding and bottom border; nav list with 4px/8px margins, 8px border-radius on items. **Selected item:** primary light background tint, primary text/icon, font-weight 600. **Hover:** neutral hover background. Use `ListItemButton` with `mx: 1`, `minWidth: 40` on icons. Child items slightly smaller font (0.875rem). +- **Profile dropdown:** Menu with 8px radius, 1px border (outlined Paper). Dense list, padding on header and items. Sign out as `MenuItem`. +- **Selection logic:** Nav item is selected when `pathname === item.path` or `pathname.startsWith(item.path + "/")`. Parent with children expands on click; leaf items navigate via Link. +- **Icons:** Use one icon per menu item that matches the action or section (e.g. Dashboard, LocalShipping for delivery, CalendarMonth for scheduling, Settings for settings). Prefer distinct MUI icons so items are easy to scan; avoid reusing the same icon for many items. + +### Reference implementations + +- **/ps** — Tailwind-only: query bar (`app-search-criteria`), buttons, table container, modals. Good reference for Tailwind patterns. +- **/do** — SearchBox + StyledDataGrid inside Paper; page title on layout. Good reference for MUI + layout. +- **/jo** — SearchBox + SearchResults (Paper-wrapped table); page title on layout. Same layout and search pattern as /do. + +When adding a **new page**, reuse the same structure: rely on the main layout for background/padding, use one optional standard `

`, then SearchBox (or `app-search-criteria` for custom bars), then Paper-wrapped grid/table or other content, with buttons and forms following the rules above. + +### Checklist for new pages + +- [ ] No extra full-page wrapper (background/padding come from main layout). +- [ ] Page title: use `` (optional `actions`). Add `className="mb-4"` for spacing below. +- [ ] Search/filter: use `SearchBox` or a div with `className="app-search-criteria"` for the bar. +- [ ] Tables/grids: wrap in `Paper variant="outlined"` (MUI) or `rounded-lg border border-slate-200 bg-white shadow-sm` (Tailwind); use `StyledDataGrid` or `SearchResults` where applicable. +- [ ] Buttons: primary = blue contained, secondary = outlined neutral, accent = green for success/export. +- [ ] Spacing: multiples of 4px (`p-4`, `gap-2`, `mb-4`); responsive with `sm`/`md`/`lg`. diff --git a/src/app/(main)/do/edit/page.tsx b/src/app/(main)/do/edit/page.tsx index 857f3b1..a3200a5 100644 --- a/src/app/(main)/do/edit/page.tsx +++ b/src/app/(main)/do/edit/page.tsx @@ -1,38 +1,36 @@ import { SearchParams } from "@/app/utils/fetchUtil"; import DoDetail from "@/components/DoDetail/DoDetailWrapper"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Typography } from "@mui/material"; import { isArray } from "lodash"; import { Metadata } from "next"; import { notFound } from "next/navigation"; import { Suspense } from "react"; export const metadata: Metadata = { - title: "Edit Delivery Order Detail" -} + title: "Edit Delivery Order Detail", +}; type Props = SearchParams; const DoEdit: React.FC = async ({ searchParams }) => { - const { t } = await getServerI18n("do"); - const id = searchParams["id"]; + const { t } = await getServerI18n("do"); + const id = searchParams["id"]; - if (!id || isArray(id) || !isFinite(parseInt(id))) { - notFound(); - } + if (!id || isArray(id) || !isFinite(parseInt(id))) { + notFound(); + } - return ( - <> - - {t("Edit Delivery Order Detail")} - - - }> - - - - - ); -} + return ( + <> + + + }> + + + + + ); +}; export default DoEdit; \ No newline at end of file diff --git a/src/app/(main)/do/page.tsx b/src/app/(main)/do/page.tsx index 8560496..e1ef75d 100644 --- a/src/app/(main)/do/page.tsx +++ b/src/app/(main)/do/page.tsx @@ -2,7 +2,7 @@ // import { getServerI18n } from "@/i18n" import DoSearch from "../../../components/DoSearch"; import { getServerI18n } from "../../../i18n"; -import { Stack, Typography } from "@mui/material"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider } from "@/i18n"; import { Metadata } from "next"; import { Suspense } from "react"; @@ -16,13 +16,7 @@ const DeliveryOrder: React.FC = async () => { return ( <> - - + }> diff --git a/src/app/(main)/jo/edit/page.tsx b/src/app/(main)/jo/edit/page.tsx index 6868224..4a6b8ed 100644 --- a/src/app/(main)/jo/edit/page.tsx +++ b/src/app/(main)/jo/edit/page.tsx @@ -1,52 +1,51 @@ import { fetchJoDetail } from "@/app/api/jo"; import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; import JoSave from "@/components/JoSave"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Typography } from "@mui/material"; import { isArray } from "lodash"; import { Metadata } from "next"; import { notFound } from "next/navigation"; import { Suspense } from "react"; export const metadata: Metadata = { - title: "Edit Job Order Detail" -} + title: "Edit Job Order Detail", +}; type Props = SearchParams; const JoEdit: React.FC = async ({ searchParams }) => { - const { t } = await getServerI18n("jo"); - const id = searchParams["id"]; - - if (!id || isArray(id) || !isFinite(parseInt(id))) { - notFound(); - } - - try { - await fetchJoDetail(parseInt(id)) - } catch (e) { - - if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { - console.log("Job Order not found:", e); - } else { - console.error("Error fetching Job Order detail:", e); - } - notFound(); + const { t } = await getServerI18n("jo"); + const id = searchParams["id"]; + + if (!id || isArray(id) || !isFinite(parseInt(id))) { + notFound(); + } + + try { + await fetchJoDetail(parseInt(id)); + } catch (e) { + if ( + e instanceof ServerFetchError && + (e.response?.status === 404 || e.response?.status === 400) + ) { + console.log("Job Order not found:", e); + } else { + console.error("Error fetching Job Order detail:", e); } - - - return ( - <> - - {t("Edit Job Order Detail")} - - - }> - - - - - ); -} + notFound(); + } + + return ( + <> + + + }> + + + + + ); +}; export default JoEdit; \ No newline at end of file diff --git a/src/app/(main)/jo/page.tsx b/src/app/(main)/jo/page.tsx index 6e2f73a..d794c57 100644 --- a/src/app/(main)/jo/page.tsx +++ b/src/app/(main)/jo/page.tsx @@ -1,38 +1,29 @@ import { preloadBomCombo } from "@/app/api/bom"; import JoSearch from "@/components/JoSearch"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Stack, Typography } from "@mui/material"; import { Metadata } from "next"; import React, { Suspense } from "react"; export const metadata: Metadata = { - title: "Job Order" -} + title: "Job Order", +}; -const jo: React.FC = async () => { - const { t } = await getServerI18n("jo"); +const Jo: React.FC = async () => { + const { t } = await getServerI18n("jo"); - preloadBomCombo() + preloadBomCombo(); - return ( - <> - - - {t("Search Job Order/ Create Job Order")} - - - {/* TODO: Improve */} - }> - - - - - ) -} + return ( + <> + + + }> + + + + + ); +}; -export default jo; \ No newline at end of file +export default Jo; \ No newline at end of file diff --git a/src/app/(main)/jodetail/edit/page.tsx b/src/app/(main)/jodetail/edit/page.tsx index 5172798..43a8027 100644 --- a/src/app/(main)/jodetail/edit/page.tsx +++ b/src/app/(main)/jodetail/edit/page.tsx @@ -1,8 +1,8 @@ import { fetchJoDetail } from "@/app/api/jo"; import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; import JoSave from "@/components/JoSave/JoSave"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Typography } from "@mui/material"; import { isArray } from "lodash"; import { Metadata } from "next"; import { notFound } from "next/navigation"; @@ -10,40 +10,41 @@ import { Suspense } from "react"; import GeneralLoading from "@/components/General/GeneralLoading"; export const metadata: Metadata = { - title: "Edit Job Order Detail" -} + title: "Edit Job Order Detail", +}; type Props = SearchParams; -const JoEdit: React.FC = async ({ searchParams }) => { - const { t } = await getServerI18n("jo"); - const id = searchParams["id"]; +const JodetailEdit: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("jo"); + const id = searchParams["id"]; - if (!id || isArray(id) || !isFinite(parseInt(id))) { - notFound(); - } + if (!id || isArray(id) || !isFinite(parseInt(id))) { + notFound(); + } - try { - await fetchJoDetail(parseInt(id)) - } catch (e) { - if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { - console.log(e) - notFound(); - } + try { + await fetchJoDetail(parseInt(id)); + } catch (e) { + if ( + e instanceof ServerFetchError && + (e.response?.status === 404 || e.response?.status === 400) + ) { + console.log(e); + notFound(); } + } - return ( - <> - - {t("Edit Job Order Detail")} - - - }> - - - - - ); -} + return ( + <> + + + }> + + + + + ); +}; -export default JoEdit; \ No newline at end of file +export default JodetailEdit; \ No newline at end of file diff --git a/src/app/(main)/jodetail/page.tsx b/src/app/(main)/jodetail/page.tsx index 37a61eb..7769148 100644 --- a/src/app/(main)/jodetail/page.tsx +++ b/src/app/(main)/jodetail/page.tsx @@ -1,39 +1,30 @@ import { preloadBomCombo } from "@/app/api/bom"; -import JodetailSearch from "@/components/Jodetail/JodetailSearch"; +import JodetailSearchWrapper from "@/components/Jodetail/FinishedGoodSearchWrapper"; +import GeneralLoading from "@/components/General/GeneralLoading"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Stack, Typography } from "@mui/material"; import { Metadata } from "next"; import React, { Suspense } from "react"; -import GeneralLoading from "@/components/General/GeneralLoading"; -import JodetailSearchWrapper from "@/components/Jodetail/FinishedGoodSearchWrapper"; + export const metadata: Metadata = { - title: "Job Order Pickexcution" -} + title: "Job Order Pick Execution", +}; -const jo: React.FC = async () => { - const { t } = await getServerI18n("jo"); +const Jodetail: React.FC = async () => { + const { t } = await getServerI18n("jo"); - preloadBomCombo() + preloadBomCombo(); - return ( - <> - - - {t("Job Order Pickexcution")} - - - - }> - - - - - ) -} + return ( + <> + + + }> + + + + + ); +}; -export default jo; \ No newline at end of file +export default Jodetail; \ No newline at end of file diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index b8feb8f..45b6396 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -49,8 +49,8 @@ export default async function MainLayout({ component="main" sx={{ marginInlineStart: { xs: 0, xl: NAVIGATION_CONTENT_WIDTH }, - padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, }} + className="min-h-screen bg-slate-50 p-4 sm:p-4 md:p-6 lg:p-8 dark:bg-slate-900" > diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index b008657..de32437 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -1,85 +1,65 @@ "use client"; import React, { useState, useEffect, useMemo } from "react"; -import { - Box, Paper, Typography, Button, Dialog, DialogTitle, - DialogContent, DialogActions, TextField, Stack, Table, - TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, - CircularProgress, Tooltip, DialogContentText -} from "@mui/material"; -import { - Search, Visibility, ListAlt, CalendarMonth, - OnlinePrediction, FileDownload, SettingsEthernet -} from "@mui/icons-material"; +import { + Search, + Eye, + ListOrdered, + LineChart, + Download, + Network, + Loader2, +} from "lucide-react"; +import PageTitleBar from "@/components/PageTitleBar"; import dayjs from "dayjs"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; export default function ProductionSchedulePage() { - // ── Main states ── - const [searchDate, setSearchDate] = useState(dayjs().format('YYYY-MM-DD')); + const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD")); const [schedules, setSchedules] = useState([]); const [selectedLines, setSelectedLines] = useState([]); const [isDetailOpen, setIsDetailOpen] = useState(false); - const [selectedPs, setSelectedPs] = useState(null); + const [selectedPs, setSelectedPs] = useState(null); const [loading, setLoading] = useState(false); const [isGenerating, setIsGenerating] = useState(false); - // Forecast dialog const [isForecastDialogOpen, setIsForecastDialogOpen] = useState(false); - const [forecastStartDate, setForecastStartDate] = useState(dayjs().format('YYYY-MM-DD')); - const [forecastDays, setForecastDays] = useState(7); // default 7 days + const [forecastStartDate, setForecastStartDate] = useState( + dayjs().format("YYYY-MM-DD") + ); + const [forecastDays, setForecastDays] = useState(7); - // Export dialog const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [exportFromDate, setExportFromDate] = useState(dayjs().format('YYYY-MM-DD')); + const [exportFromDate, setExportFromDate] = useState( + dayjs().format("YYYY-MM-DD") + ); - // Auto-search on mount useEffect(() => { handleSearch(); }, []); - // ── Formatters & Helpers ── const formatBackendDate = (dateVal: any) => { if (Array.isArray(dateVal)) { const [year, month, day] = dateVal; - return dayjs(new Date(year, month - 1, day)).format('DD MMM (dddd)'); + return dayjs(new Date(year, month - 1, day)).format("DD MMM (dddd)"); } - return dayjs(dateVal).format('DD MMM (dddd)'); + return dayjs(dateVal).format("DD MMM (dddd)"); }; const formatNum = (num: any) => { - return new Intl.NumberFormat('en-US').format(Number(num) || 0); + return new Intl.NumberFormat("en-US").format(Number(num) || 0); }; - const isDateToday = useMemo(() => { - if (!selectedPs?.produceAt) return false; - const todayStr = dayjs().format('YYYY-MM-DD'); - let scheduleDateStr = ""; - - if (Array.isArray(selectedPs.produceAt)) { - const [y, m, d] = selectedPs.produceAt; - scheduleDateStr = dayjs(new Date(y, m - 1, d)).format('YYYY-MM-DD'); - } else { - scheduleDateStr = dayjs(selectedPs.produceAt).format('YYYY-MM-DD'); - } - - return todayStr === scheduleDateStr; - }, [selectedPs]); - - // ── API Actions ── const handleSearch = async () => { setLoading(true); - try { - const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, { - method: 'GET', - }); - + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, + { method: "GET" } + ); if (response.status === 401 || response.status === 403) return; - const data = await response.json(); - setSchedules(Array.isArray(data) ? data : []); } catch (e) { console.error("Search Error:", e); @@ -89,28 +69,22 @@ export default function ProductionSchedulePage() { }; const handleConfirmForecast = async () => { - if (!forecastStartDate || forecastDays === '' || forecastDays < 1) { + if (!forecastStartDate || forecastDays === "" || forecastDays < 1) { alert("Please enter a valid start date and number of days (≥1)."); return; } - setLoading(true); setIsForecastDialogOpen(false); - try { const params = new URLSearchParams({ startDate: forecastStartDate, days: forecastDays.toString(), }); - const url = `${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule?${params.toString()}`; - - const response = await clientAuthFetch(url, { method: 'GET' }); - + const response = await clientAuthFetch(url, { method: "GET" }); if (response.status === 401 || response.status === 403) return; - if (response.ok) { - await handleSearch(); // refresh list + await handleSearch(); alert("成功計算排期!"); } else { const errorText = await response.text(); @@ -130,27 +104,21 @@ export default function ProductionSchedulePage() { alert("Please select a from date."); return; } - setLoading(true); setIsExportDialogOpen(false); - try { - const params = new URLSearchParams({ - fromDate: exportFromDate, - }); - - const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, { - method: 'GET', - }); - + const params = new URLSearchParams({ fromDate: exportFromDate }); + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, + { method: "GET" } + ); if (response.status === 401 || response.status === 403) return; if (!response.ok) throw new Error(`Export failed: ${response.status}`); - const blob = await response.blob(); const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; - a.download = `production_schedule_from_${exportFromDate.replace(/-/g, '')}.xlsx`; + a.download = `production_schedule_from_${exportFromDate.replace(/-/g, "")}.xlsx`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); @@ -164,41 +132,24 @@ export default function ProductionSchedulePage() { }; const handleViewDetail = async (ps: any) => { - console.log("=== VIEW DETAIL CLICKED ==="); - console.log("Schedule ID:", ps?.id); - console.log("Full ps object:", ps); - if (!ps?.id) { alert("Cannot open details: missing schedule ID"); return; } - setSelectedPs(ps); setLoading(true); - try { const url = `${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`; - - const response = await clientAuthFetch(url, { method: 'GET' }); - + const response = await clientAuthFetch(url, { method: "GET" }); if (response.status === 401 || response.status === 403) return; - if (!response.ok) { const errorText = await response.text().catch(() => "(no text)"); - console.error("Server error response:", errorText); alert(`Server error ${response.status}: ${errorText}`); return; } - const data = await response.json(); - console.log("Full received lines (JSON):", JSON.stringify(data, null, 2)); - console.log("Received data type:", typeof data); - console.log("Received data:", data); - console.log("Number of lines:", Array.isArray(data) ? data.length : "not an array"); - setSelectedLines(Array.isArray(data) ? data : []); setIsDetailOpen(true); - } catch (err) { console.error("Fetch failed:", err); alert("Network or fetch error – check console"); @@ -207,25 +158,21 @@ export default function ProductionSchedulePage() { } }; - const handleAutoGenJob = async () => { - //if (!isDateToday) return; setIsGenerating(true); try { - const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: selectedPs.id }) - }); - + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: selectedPs.id }), + } + ); if (response.status === 401 || response.status === 403) return; - if (response.ok) { const data = await response.json(); - const displayMessage = data.message || "Operation completed."; - - alert(displayMessage); - //alert("Job Orders generated successfully!"); + alert(data.message || "Operation completed."); setIsDetailOpen(false); } else { alert("Failed to generate jobs."); @@ -238,263 +185,380 @@ export default function ProductionSchedulePage() { }; return ( - - - {/* Header */} - - - - 排程 - - - - - - - - - {/* Query Bar – unchanged */} - - + + + + + } + className="mb-4" + /> + + {/* Query Bar */} +
+ + setSearchDate(e.target.value)} + className="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder-slate-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" /> - - - - {/* Main Table – unchanged */} - - - - - 詳細 - 生產日期 - 預計生產數 - 成品款數 - - - - {schedules.map((ps) => ( - - - handleViewDetail(ps)}> - - - - {formatBackendDate(ps.produceAt)} - {formatNum(ps.totalEstProdCount)} - {formatNum(ps.totalFGType)} - - ))} - -
-
- - {/* Detail Dialog – unchanged */} - setIsDetailOpen(false)} maxWidth="lg" fullWidth> - - - - 排期詳細: {selectedPs?.id} ({formatBackendDate(selectedPs?.produceAt)}) - - - - - - - - 工單號 - 物料編號 - 物料名稱 - 每日平均出貨量 - 出貨前預計存貨量 - 單位 - 可用日 - 生產量(批) - 預計生產包數 - 優先度 - - - - {selectedLines.map((line: any) => ( - - {line.joCode || '-'} - {line.itemCode} - {line.itemName} - {formatNum(line.avgQtyLastMonth)} - {formatNum(line.stockQty)} - {line.stockUnit} - + + + + {/* Main Table */} +
+
+
+ + + + + + + + + + {schedules.map((ps) => ( + + + + + + + ))} + +
+ 詳細 + + 生產日期 + + 預計生產數 + + 成品款數 +
+ + + {formatBackendDate(ps.produceAt)} + + {formatNum(ps.totalEstProdCount)} + + {formatNum(ps.totalFGType)} +
+
+ + + {/* Detail Modal */} + {isDetailOpen && ( +
+
!isGenerating && setIsDetailOpen(false)} + /> +
+
+ +

+ 排期詳細: {selectedPs?.id} ( + {formatBackendDate(selectedPs?.produceAt)}) +

+
+
+ + + + + + + + + + + + + + + + + {selectedLines.map((line: any) => ( + + + + + + + +
+ 工單號 + + 物料編號 + + 物料名稱 + + 每日平均出貨量 + + 出貨前預計存貨量 + + 單位 + + 可用日 + + 生產量(批) + + 預計生產包數 + + 優先度 +
+ {line.joCode || "-"} + + {line.itemCode} + {line.itemName} + {formatNum(line.avgQtyLastMonth)} + + {formatNum(line.stockQty)} + {line.stockUnit} {line.daysLeft} - - {formatNum(line.batchNeed)} - {formatNum(line.prodQty)} - {line.itemPriority} - - ))} - -
- - - - {/* Footer Actions */} - - - {/* - - */} - -
+
+ + +
+
+
+ )} + + {/* Forecast Dialog */} + {isForecastDialogOpen && ( +
+
setIsForecastDialogOpen(false)} + /> +
+

+ 準備生成預計排期 +

+
+
+
+
+ + { + const val = + e.target.value === "" ? "" : Number(e.target.value); + if ( + val === "" || + (Number.isInteger(val) && val >= 1 && val <= 365) + ) { + setForecastDays(val); + } + }} + className="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" + /> +
+
+
+ + +
+
+
+ )} + + {/* Export Dialog */} + {isExportDialogOpen && ( +
+
setIsExportDialogOpen(false)} + /> +
+

+ 匯出排期/物料用量預計 +

+

+ 選擇要匯出的起始日期 +

+
+
+ )} +
); -} \ No newline at end of file +} diff --git a/src/app/global.css b/src/app/global.css index 7d2ff9a..261f31c 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -1,7 +1,52 @@ - +@tailwind base; @tailwind components; @tailwind utilities; -html, body { +/* UI standard: light default, primary #3b82f6, accent #10b981 */ +@layer base { + :root { + --primary: #3b82f6; + --accent: #10b981; + --background: #f8fafc; + --foreground: #0f172a; + --card: #ffffff; + --card-foreground: #0f172a; + --border: #e2e8f0; + --muted: #64748b; + } + .dark { + --background: #0f172a; + --foreground: #f1f5f9; + --card: #1e293b; + --card-foreground: #f1f5f9; + --border: #334155; + --muted: #94a3b8; + } +} + +html, +body { overscroll-behavior: none; -} \ No newline at end of file +} + +body { + background-color: var(--background); + color: var(--foreground); +} + +.app-search-criteria { + border-radius: 8px; + border: 1px solid var(--border); + border-left-width: 4px; + border-left-color: var(--primary); + background-color: var(--card); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); +} + +.app-search-criteria-label { + font-size: 0.75rem; + font-weight: 500; + color: #334155; + text-transform: uppercase; + letter-spacing: 0.05em; +} diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 1c1cec3..cb70811 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -5,7 +5,7 @@ import Profile from "./Profile"; import Box from "@mui/material/Box"; import NavigationToggle from "./NavigationToggle"; import { I18nProvider } from "@/i18n"; -import { Divider, Grid, Typography } from "@mui/material"; +import { Typography } from "@mui/material"; import Breadcrumb from "@/components/Breadcrumb"; import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; @@ -17,23 +17,29 @@ export interface AppBarProps { const AppBar: React.FC = ({ avatarImageSrc, profileName }) => { return ( - - + + - + - + - + {profileName} diff --git a/src/components/AppBar/NavigationToggle.tsx b/src/components/AppBar/NavigationToggle.tsx index 9f61753..5144411 100644 --- a/src/components/AppBar/NavigationToggle.tsx +++ b/src/components/AppBar/NavigationToggle.tsx @@ -31,13 +31,16 @@ const NavigationToggle: React.FC = () => { - + ); diff --git a/src/components/AppBar/Profile.tsx b/src/components/AppBar/Profile.tsx index dc37351..dc6ed1d 100644 --- a/src/components/AppBar/Profile.tsx +++ b/src/components/AppBar/Profile.tsx @@ -35,24 +35,25 @@ const Profile: React.FC = ({ avatarImageSrc, profileName }) => { - + {profileName} - signOut()}>{t("Sign out")} + signOut()} sx={{ py: 1.5, fontSize: "0.875rem" }}> + {t("Sign out")} + ); diff --git a/src/components/DoSearch/DoSearch.tsx b/src/components/DoSearch/DoSearch.tsx index f1c570a..278f97b 100644 --- a/src/components/DoSearch/DoSearch.tsx +++ b/src/components/DoSearch/DoSearch.tsx @@ -27,7 +27,7 @@ import { SubmitHandler, useForm, } from "react-hook-form"; -import { Box, Button, Grid, Stack, Typography, TablePagination} from "@mui/material"; +import { Box, Button, Paper, Stack, Typography, TablePagination } from "@mui/material"; import StyledDataGrid from "../StyledDataGrid"; import { GridRowSelectionModel } from "@mui/x-data-grid"; import Swal from "sweetalert2"; @@ -605,32 +605,17 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { component="form" onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} > - - - - {t("Delivery Order")} - - - - - {hasSearched && hasResults && ( - - )} + {hasSearched && hasResults && ( + + - - + )} { onReset={onReset} /> - { - setRowSelectionModel(newRowSelectionModel); - formProps.setValue("ids", newRowSelectionModel); - }} - slots={{ - footer: FooterToolbar, - noRowsOverlay: NoRowsOverlay, - }} - /> - - + + { + setRowSelectionModel(newRowSelectionModel); + formProps.setValue("ids", newRowSelectionModel); + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + /> + + diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index de2ea41..8e18be2 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -1,32 +1,89 @@ +"use client"; + interface Props { width?: number; height?: number; + className?: string; } -const Logo: React.FC = ({ width, height }) => { +/** + * Logo: 3D-style badge (FP) + MTMS wordmark. + * Badge uses gradient and highlight for depth; FP = Food Production, MTMS = system name. + */ +const Logo: React.FC = ({ height = 44, className = "" }) => { + const size = Math.max(28, height); + const badgeSize = Math.round(size * 0.7); + const fontSize = Math.round(size * 0.5); + const fpSize = badgeSize <= 22 ? 10 : badgeSize <= 28 ? 12 : 14; + return ( - - - - - + + {/* Energetic blue gradient: bright top → deep blue bottom */} + + + + + + + + + + + + + + {/* Shadow layer - deep blue */} + + {/* Main 3D body */} + + {/* Top bevel (inner 3D) */} + + {/* FP text */} + + FP + + + {/* Wordmark: MTMS + subtitle — strong, energetic */} +
+ + MTMS + + + Food Production + +
+
); }; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 3709623..996a12c 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -1,31 +1,44 @@ import { useSession } from "next-auth/react"; -import Divider from "@mui/material/Divider"; import Box from "@mui/material/Box"; -import React, { useEffect } from "react"; +import React from "react"; import List from "@mui/material/List"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemText from "@mui/material/ListItemText"; import ListItemIcon from "@mui/material/ListItemIcon"; -import WorkHistory from "@mui/icons-material/WorkHistory"; import Dashboard from "@mui/icons-material/Dashboard"; -import SummarizeIcon from "@mui/icons-material/Summarize"; -import PaymentsIcon from "@mui/icons-material/Payments"; -import AccountTreeIcon from "@mui/icons-material/AccountTree"; -import RequestQuote from "@mui/icons-material/RequestQuote"; -import PeopleIcon from "@mui/icons-material/People"; -import Task from "@mui/icons-material/Task"; +import Storefront from "@mui/icons-material/Storefront"; +import LocalShipping from "@mui/icons-material/LocalShipping"; import Assignment from "@mui/icons-material/Assignment"; -import Settings from "@mui/icons-material/Settings"; -import Analytics from "@mui/icons-material/Analytics"; -import Payments from "@mui/icons-material/Payments"; +import Inventory from "@mui/icons-material/Inventory"; +import AssignmentTurnedIn from "@mui/icons-material/AssignmentTurnedIn"; +import ReportProblem from "@mui/icons-material/ReportProblem"; import QrCodeIcon from "@mui/icons-material/QrCode"; +import ViewModule from "@mui/icons-material/ViewModule"; +import Description from "@mui/icons-material/Description"; +import CalendarMonth from "@mui/icons-material/CalendarMonth"; +import Factory from "@mui/icons-material/Factory"; +import PostAdd from "@mui/icons-material/PostAdd"; +import Kitchen from "@mui/icons-material/Kitchen"; +import Inventory2 from "@mui/icons-material/Inventory2"; +import Print from "@mui/icons-material/Print"; +import Assessment from "@mui/icons-material/Assessment"; +import Settings from "@mui/icons-material/Settings"; +import Person from "@mui/icons-material/Person"; +import Group from "@mui/icons-material/Group"; +import Category from "@mui/icons-material/Category"; +import TrendingUp from "@mui/icons-material/TrendingUp"; +import Build from "@mui/icons-material/Build"; +import Warehouse from "@mui/icons-material/Warehouse"; +import VerifiedUser from "@mui/icons-material/VerifiedUser"; +import Label from "@mui/icons-material/Label"; +import Checklist from "@mui/icons-material/Checklist"; +import Science from "@mui/icons-material/Science"; +import UploadFile from "@mui/icons-material/UploadFile"; import { useTranslation } from "react-i18next"; -import Typography from "@mui/material/Typography"; import { usePathname } from "next/navigation"; import Link from "next/link"; import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; -import BugReportIcon from "@mui/icons-material/BugReport"; import { AUTH } from "../../authorities"; interface NavigationItem { @@ -57,81 +70,55 @@ const NavigationContent: React.FC = () => { path: "/dashboard", }, { - icon: , + icon: , label: "Store Management", path: "", requiredAbility: [AUTH.PURCHASE, AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_FG, AUTH.STOCK_IN_BIND, AUTH.ADMIN], children: [ { - icon: , + icon: , label: "Purchase Order", requiredAbility: [AUTH.PURCHASE, AUTH.ADMIN], path: "/po", }, { - icon: , + icon: , label: "Pick Order", requiredAbility: [AUTH.STOCK, AUTH.ADMIN], path: "/pickOrder", }, - // { - // icon: , - // label: "Cons. Pick Order", - // path: "", - // }, - // { - // icon: , - // label: "Delivery Pick Order", - // path: "", - // }, - // { - // icon: , - // label: "Warehouse", - // path: "", - // }, - // { - // icon: , - // label: "Location Transfer Order", - // path: "", - // }, { - icon: , + icon: , label: "View item In-out And inventory Ledger", requiredAbility: [AUTH.STOCK, AUTH.ADMIN], path: "/inventory", }, { - icon: , + icon: , label: "Stock Take Management", requiredAbility: [AUTH.STOCK_TAKE, AUTH.ADMIN], path: "/stocktakemanagement", }, { - icon: , + icon: , label: "Stock Issue", requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN], path: "/stockIssue", }, - //TODO: anna - // { - // icon: , - // label: "Stock Issue", - // path: "/stockIssue", - // }, { - icon: , + icon: , label: "Put Away Scan", requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.ADMIN], path: "/putAway", }, { - icon: , + icon: , label: "Finished Good Order", requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN], path: "/finishedGood", }, { - icon: , + icon: , label: "Stock Record", requiredAbility: [AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN], path: "/stockRecord", @@ -139,106 +126,44 @@ const NavigationContent: React.FC = () => { ], }, { - icon: , - label: "Delivery", - path: "", + icon: , + label: "Delivery Order", + path: "/do", requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN], - children: [ - { - icon: , - label: "Delivery Order", - requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN], - path: "/do", - }, - ], }, - // { - // icon: , - // label: "Report", - // path: "", - // children: [ - // { - // icon: , - // label: "report", - // path: "", - // }, - // ], - // }, - // { - // icon: , - // label: "Recipe", - // path: "", - // children: [ - // { - // icon: , - // label: "FG Recipe", - // path: "", - // }, - // { - // icon: , - // label: "SFG Recipe", - // path: "", - // }, - // { - // icon: , - // label: "Recipe", - // path: "", - // }, - // ], - // }, - /* { - icon: , - label: "Scheduling", - path: "", - requiredAbility: [AUTH.FORECAST, AUTH.ADMIN], - children: [ - { - icon: , - label: "Demand Forecast", - path: "/scheduling/rough", - }, - { - icon: , - label: "Detail Scheduling", - path: "/scheduling/detailed", - }, - ], - }, - */ - { - icon: , + icon: , label: "Scheduling", path: "/ps", requiredAbility: [AUTH.FORECAST, AUTH.ADMIN], isHidden: false, }, { - icon: , + icon: , label: "Management Job Order", path: "", requiredAbility: [AUTH.JOB_CREATE, AUTH.JOB_PICK, AUTH.JOB_PROD, AUTH.ADMIN], children: [ { - icon: , + icon: , label: "Search Job Order/ Create Job Order", requiredAbility: [AUTH.JOB_CREATE, AUTH.ADMIN], path: "/jo", }, { - icon: , + icon: , label: "Job Order Pickexcution", requiredAbility: [AUTH.JOB_PICK, AUTH.JOB_MAT, AUTH.ADMIN], path: "/jodetail", }, { - icon: , + icon: , label: "Job Order Production Process", requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN], path: "/productionProcess", }, { - icon: , + icon: , label: "Bag Usage", requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN], path: "/bag", @@ -246,134 +171,99 @@ const NavigationContent: React.FC = () => { ], }, { - icon: , + icon: , label: "打袋機列印", path: "/testing", requiredAbility: [AUTH.TESTING, AUTH.ADMIN], isHidden: false, }, { - icon: , + icon: , label: "報告管理", path: "/report", requiredAbility: [AUTH.TESTING, AUTH.ADMIN], isHidden: false, }, { - icon: , + icon: , label: "Settings", path: "", requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN], children: [ { - icon: , + icon: , label: "User", path: "/settings/user", requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN], }, { - icon: , + icon: , label: "User Group", path: "/settings/user", requiredAbility: [AUTH.VIEW_GROUP, AUTH.ADMIN], }, - // { - // icon: , - // label: "Material", - // path: "/settings/material", - // }, - // { - // icon: , - // label: "By-product", - // path: "/settings/byProduct", - // }, { - icon: , + icon: , label: "Items", path: "/settings/items", }, { - icon: , + icon: , label: "ShopAndTruck", path: "/settings/shop", }, { - icon: , + icon: , label: "Demand Forecast Setting", path: "/settings/rss", }, - //{ - // icon: , - // label: "Equipment Type", - // path: "/settings/equipmentType", - //}, { - icon: , + icon: , label: "Equipment", path: "/settings/equipment", }, { - icon: , + icon: , label: "Warehouse", path: "/settings/warehouse", }, { - icon: , + icon: , label: "Printer", path: "/settings/printer", }, - //{ - // icon: , - // label: "Supplier", - // path: "/settings/user", - //}, { - icon: , + icon: , label: "Customer", path: "/settings/user", }, { - icon: , + icon: , label: "QC Check Item", path: "/settings/qcItem", }, { - icon: , + icon:
@@ -270,7 +268,7 @@ export default function ProductionSchedulePage() { onClick={() => handleViewDetail(ps)} className="rounded p-1 text-blue-500 hover:bg-blue-50 hover:text-blue-600 dark:text-blue-400 dark:hover:bg-blue-500/20" > - + @@ -303,7 +301,7 @@ export default function ProductionSchedulePage() { />
- +

排期詳細: {selectedPs?.id} ( {formatBackendDate(selectedPs?.produceAt)}) @@ -396,9 +394,9 @@ export default function ProductionSchedulePage() { className="inline-flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-600 disabled:opacity-50" > {isGenerating ? ( - + ) : ( - + )} 自動生成工單 @@ -491,9 +489,9 @@ export default function ProductionSchedulePage() { className="inline-flex items-center gap-2 rounded-lg bg-blue-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-600 disabled:opacity-50" > {loading ? ( - + ) : ( - + )} 計算預測排期 @@ -549,9 +547,9 @@ export default function ProductionSchedulePage() { className="inline-flex items-center gap-2 rounded-lg bg-emerald-500 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-emerald-600 disabled:opacity-50" > {loading ? ( - + ) : ( - + )} 匯出 From eac95c343cd4161498856c32030a2c9cfa427583 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Mon, 23 Feb 2026 15:34:26 +0800 Subject: [PATCH 13/22] update --- src/config/reportConfig.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index 74c26e4..e51da6e 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -161,6 +161,16 @@ export const REPORTS: ReportDefinition[] = [ { 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: "類別 Category", name: "stockCategory", type: "select", required: false, + multiple: true, + options: [ + { label: "All", value: "All" }, + { label: "MAT", value: "MAT" }, + { label: "WIP", value: "WIP" }, + { label: "NM", value: "NM" }, + { label: "FG", value: "FG" }, + { label: "CMB", value: "CMB" } + ] }, { label: "物料編號 Item Code", name: "itemCode", type: "select", required: false, multiple: true, allowInput: true, @@ -179,6 +189,13 @@ export const REPORTS: ReportDefinition[] = [ { label: "完成生產日期:由 Last Out Date Start", name: "lastOutDateStart", type: "date", required: false, placeholder: "dd/mm/yyyy" }, { label: "完成生產日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false, placeholder: "dd/mm/yyyy" }, { label: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, + { label: "類別 Category", name: "stockCategory", type: "select", required: false, + multiple: true, + options: [ + { label: "All", value: "All" }, + { label: "WIP", value: "WIP" }, + { label: "FG", value: "FG" }, + ] }, { label: "物料編號 Item Code", name: "itemCode", type: "select", required: false, multiple: true, allowInput: true, From febf75eb385b21b0060d7e77e21c86a1d7cc3c66 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Mon, 23 Feb 2026 16:38:45 +0800 Subject: [PATCH 14/22] it says it can control the popup keyboard size in tablet --- src/app/global.css | 23 +++++++++++++++++++++++ src/app/layout.tsx | 10 +++++++++- src/components/Logo/Logo.tsx | 6 +++--- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/app/global.css b/src/app/global.css index 261f31c..2b2a85d 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -29,11 +29,34 @@ body { overscroll-behavior: none; } +/* Tablet/mobile: stable layout when virtual keyboard opens */ +html { + /* Prefer dynamic viewport height so layout can adapt to keyboard (if browser resizes) */ + height: 100%; +} body { + min-height: 100%; + min-height: 100dvh; background-color: var(--background); color: var(--foreground); } +/* Full-height containers: use dvh so keyboard doesn’t squash the layout when overlay is used */ +@media (max-width: 1024px) { + .min-h-screen { + min-height: 100dvh; + } +} + +/* Avoid iOS zoom on input focus (keep inputs ≥16px where possible) */ +@media (max-width: 1024px) { + input, + select, + textarea { + font-size: max(16px, 1rem); + } +} + .app-search-criteria { border-radius: 8px; border: 1px solid var(--border); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dde610b..027afb7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; // import { detectLanguage } from "@/i18n"; // import ThemeRegistry from "@/theme/ThemeRegistry"; import { detectLanguage } from "../i18n"; @@ -9,6 +9,14 @@ export const metadata: Metadata = { description: "FPSMS - xxxx Management System", }; +/** Tablet/mobile: virtual keyboard overlays content instead of resizing viewport (avoids "half screen gone"). */ +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + viewportFit: "cover", + interactiveWidget: "overlays-content", +}; + export default async function RootLayout({ children, }: { diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index 8e18be2..0bc601c 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -7,7 +7,7 @@ interface Props { } /** - * Logo: 3D-style badge (FP) + MTMS wordmark. + * Logo: 3D-style badge (FP) + FP-MTMS wordmark. * Badge uses gradient and highlight for depth; FP = Food Production, MTMS = system name. */ const Logo: React.FC = ({ height = 44, className = "" }) => { @@ -68,13 +68,13 @@ const Logo: React.FC = ({ height = 44, className = "" }) => { FP - {/* Wordmark: MTMS + subtitle — strong, energetic */} + {/* Wordmark: FP-MTMS + subtitle — strong, energetic */}
- MTMS + FP-MTMS Date: Mon, 23 Feb 2026 17:20:31 +0800 Subject: [PATCH 15/22] update jobmatch --- .../Jodetail/JobPickExecutionForm.tsx | 40 ++++++++------ .../Jodetail/JobPickExecutionsecondscan.tsx | 55 +++++-------------- src/components/Jodetail/JobmatchForm.tsx | 2 +- src/i18n/zh/common.json | 24 +++++++- src/i18n/zh/jo.json | 9 +++ src/i18n/zh/pickOrder.json | 1 + 6 files changed, 70 insertions(+), 61 deletions(-) diff --git a/src/components/Jodetail/JobPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx index 6d6a054..42916c3 100644 --- a/src/components/Jodetail/JobPickExecutionForm.tsx +++ b/src/components/Jodetail/JobPickExecutionForm.tsx @@ -91,7 +91,9 @@ const PickExecutionForm: React.FC = ({ const [handlers, setHandlers] = useState>([]); const [verifiedQty, setVerifiedQty] = useState(0); const { data: session } = useSession() as { data: SessionWithTokens | null }; - + const missSet = formData.missQty != null; +const badItemSet = formData.badItemQty != null; +const badPackageSet = (formData as any).badPackageQty != null; const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { return lot.availableQty || 0; }, []); @@ -162,9 +164,9 @@ useEffect(() => { storeLocation: selectedLot.location, requiredQty: selectedLot.requiredQty, actualPickQty: initialVerifiedQty, - missQty: 0, - badItemQty: 0, - badPackageQty: 0, // Bad Package Qty (frontend only) + missQty: undefined, + badItemQty: undefined, + badPackageQty: undefined, issueRemark: "", pickerName: "", handledBy: undefined, @@ -195,10 +197,10 @@ useEffect(() => { const newErrors: FormErrors = {}; const ap = Number(verifiedQty) || 0; const miss = Number(formData.missQty) || 0; - const badItem = Number(formData.badItemQty) || 0; - const badPackage = Number((formData as any).badPackageQty) || 0; - const totalBad = badItem + badPackage; - const total = ap + miss + totalBad; + const badItem = Number(formData.badItemQty) ?? 0; + const badPackage = Number((formData as any).badPackageQty) ?? 0; + const totalBadQty = badItem + badPackage; + const total = ap + miss + totalBadQty; const availableQty = selectedLot?.availableQty || 0; // 1. Check actualPickQty cannot be negative @@ -231,7 +233,7 @@ useEffect(() => { } // 5. At least one field must have a value - if (ap === 0 && miss === 0 && totalBad === 0) { + if (ap === 0 && miss === 0 && totalBadQty === 0) { newErrors.actualPickQty = t("Enter pick qty or issue qty"); } @@ -288,11 +290,12 @@ useEffect(() => { const submissionData: PickExecutionIssueData = { ...(formData as PickExecutionIssueData), actualPickQty: verifiedQty, - lotId: formData.lotId || selectedLot?.lotId || 0, - lotNo: formData.lotNo || selectedLot?.lotNo || '', - pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '', - pickerName: session?.user?.name || '', - badItemQty: totalBadQty, + lotId: formData.lotId ?? selectedLot?.lotId ?? 0, + lotNo: formData.lotNo ?? selectedLot?.lotNo ?? '', + pickOrderCode: formData.pickOrderCode ?? selectedPickOrderLine?.pickOrderCode ?? '', + pickerName: session?.user?.name ?? '', + missQty: formData.missQty ?? 0, // 这里:null/undefined → 0 + badItemQty: totalBadQty, // totalBadQty 下面用 ?? 0 算 badReason, }; @@ -397,7 +400,8 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={formData.missQty || 0} + disabled={badItemSet || badPackageSet} + value={formData.missQty || ""} onChange={(e) => { handleInputChange( "missQty", @@ -421,7 +425,7 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={formData.badItemQty || 0} + value={formData.badItemQty || ""} onChange={(e) => { const newBadItemQty = e.target.value === "" ? undefined @@ -429,6 +433,7 @@ useEffect(() => { handleInputChange('badItemQty', newBadItemQty); }} error={!!errors.badItemQty} + disabled={missSet || badPackageSet} helperText={errors.badItemQty} variant="outlined" /> @@ -444,7 +449,7 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={(formData as any).badPackageQty || 0} + value={(formData as any).badPackageQty || ""} onChange={(e) => { handleInputChange( "badPackageQty", @@ -453,6 +458,7 @@ useEffect(() => { : Math.max(0, Number(e.target.value) || 0) ); }} + disabled={missSet || badItemSet} error={!!errors.badItemQty} variant="outlined" /> diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx index 823f79c..21163e1 100644 --- a/src/components/Jodetail/JobPickExecutionsecondscan.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -868,7 +868,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { qty: submitQty, isMissing: false, isBad: false, - reason: undefined + reason: undefined, + userId: currentUserId ?? 0 } ); @@ -881,7 +882,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { } catch (error) { console.error("Error submitting second scan quantity:", error); } - }, [fetchJobOrderData]); + }, [fetchJobOrderData, currentUserId]); const handlePickExecutionForm = useCallback((lot: any) => { console.log("=== Pick Execution Form ==="); @@ -1263,55 +1264,24 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} - {/* - - {lot.matchStatus?.toLowerCase() === 'scanned' || - lot.matchStatus?.toLowerCase() === 'completed' ? ( - - - - ) : ( - - {t(" ")} - - )} - - */} + - - ))} @@ -675,6 +672,7 @@ const CompleteJobOrderRecord: React.FC = ({ onPageChange={handlePageChange} onRowsPerPageChange={handlePageSizeChange} rowsPerPageOptions={[5, 10, 25, 50]} + labelRowsPerPage={t("Rows per page")} /> )} diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index b569b70..f00ddf5 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -422,7 +422,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { const productionProcessesLineRemarkTableColumns: GridColDef[] = [ { field: "seqNo", - headerName: t("Seq"), + headerName: t("SEQ"), flex: 0.2, align: "left", headerAlign: "left", diff --git a/src/components/Qc/QcForm.tsx b/src/components/Qc/QcForm.tsx index 10d46d4..46a33a4 100644 --- a/src/components/Qc/QcForm.tsx +++ b/src/components/Qc/QcForm.tsx @@ -232,12 +232,23 @@ const QcForm: React.FC = ({ rows, disabled = false }) => { return ( <> + // autoHeight 'auto'} + initialState={{ + pagination: { paginationModel: { page: 0, pageSize: 100 } }, + }} + pageSizeOptions={[100]} + slotProps={{ + pagination: { + sx: { + display: "none", + }, + }, + }} /> ); diff --git a/src/components/SearchResults/SearchResults.tsx b/src/components/SearchResults/SearchResults.tsx index 923298a..43e692c 100644 --- a/src/components/SearchResults/SearchResults.tsx +++ b/src/components/SearchResults/SearchResults.tsx @@ -198,7 +198,7 @@ function SearchResults({ setCheckboxIds = undefined, onRowClick = undefined, }: Props) { - const { t } = useTranslation("dashboard"); + const { t } = useTranslation(); const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(10); diff --git a/src/components/StyledDataGrid/StyledDataGrid.tsx b/src/components/StyledDataGrid/StyledDataGrid.tsx index 7899dc4..0ea456c 100644 --- a/src/components/StyledDataGrid/StyledDataGrid.tsx +++ b/src/components/StyledDataGrid/StyledDataGrid.tsx @@ -1,6 +1,8 @@ import { styled } from "@mui/material"; import { DataGrid ,DataGridProps,zhTW} from "@mui/x-data-grid"; import { forwardRef } from "react"; +import { useTranslation } from "react-i18next"; + const StyledDataGridBase = styled(DataGrid)(({ theme }) => ({ "--unstable_DataGrid-radius": 0, "& .MuiDataGrid-columnHeaders": { @@ -29,12 +31,14 @@ const StyledDataGridBase = styled(DataGrid)(({ theme }) => ({ }, })); const StyledDataGrid = forwardRef((props, ref) => { + const { t } = useTranslation(); return ( diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 5dc8e3b..57d08fe 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -112,6 +112,7 @@ "Today": "今天", "Yesterday": "昨天", + "Two Days Ago": "前天", "Input Equipment is not match with process": "輸入的設備與流程不匹配", "Staff No is required": "員工編號必填", @@ -120,6 +121,8 @@ "Production Date": "生產日期", "QC Check Item": "QC品檢項目", "QC Category": "QC品檢模板", + "QC Item All": "QC 綜合管理", + "qcItemAll": "QC 綜合管理", "qcCategory": "品檢模板", "QC Check Template": "QC檢查模板", "Mail": "郵件", @@ -136,6 +139,7 @@ "Production Date":"生產日期", "QC Check Item":"QC品檢項目", "QC Category":"QC品檢模板", + "QC Item All":"QC 綜合管理", "qcCategory":"品檢模板", "QC Check Template":"QC檢查模板", "QR Code Handle":"二維碼列印及下載", @@ -272,6 +276,7 @@ "Please scan equipment code": "請掃描設備編號", "Equipment Code": "設備編號", "Seq": "步驟", + "SEQ": "步驟", "Item Name": "物料名稱", "Job Order Info": "工單信息", "Matching Stock": "工單對料", diff --git a/src/i18n/zh/inventory.json b/src/i18n/zh/inventory.json index a222604..cd05554 100644 --- a/src/i18n/zh/inventory.json +++ b/src/i18n/zh/inventory.json @@ -39,6 +39,7 @@ "DO Order Code": "送貨單編號", "JO Order Code": "工單編號", "Picker Name": "提料員", + "Rows per page": "每頁行數", "rejected": "已拒絕", "miss": "缺貨", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 5c53546..e726dea 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -93,6 +93,11 @@ "Bag Code": "包裝袋編號", "Sequence": "序", + "Seq": "步驟", + "SEQ": "步驟", + "Today": "今天", + "Yesterday": "昨天", + "Two Days Ago": "前天", "Item Code": "成品/半成品編號", "Paused": "已暫停", "paused": "已暫停", @@ -347,7 +352,7 @@ "receivedQty": "接收數量", "stock in information": "庫存信息", "No Uom": "沒有單位", - "Print Pick Record": "打印板頭紙", + "Print Pick Record": "打印版頭紙", "Printed Successfully.": "成功列印", "Submit All Scanned": "提交所有已掃描項目", "Submitting...": "提交中...", From 0d0a05ed5597f1a7cd9d613bb61d64d309e1166a Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 23 Feb 2026 18:12:40 +0800 Subject: [PATCH 17/22] update zh --- src/i18n/zh/common.json | 2 ++ src/i18n/zh/jo.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 5dc8e3b..1f82b09 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -470,6 +470,8 @@ "Missing items": "缺少物品", "Total (Verified + Bad + Missing) must equal Required quantity": "總數必須等於需求數量", "Missing item Qty": "缺少物品數量", + "seq": "序號", + "Job Order Pick Execution": "工單提料", "Bad Item Qty": "不良物品數量", "Issue Remark": "問題備註", "At least one issue must be reported": "至少需要報告一個問題", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 5c53546..f12766a 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -564,7 +564,9 @@ "Missing item Qty": "缺少物品數量", "Bad Item Qty": "不良物品數量", "Issue Remark": "問題備註", + "seq": "序號", "Handled By": "處理者", + "Job Order Pick Execution": "工單提料", "Finish": "完成" } From f17ed17f878c51157d495e0c5c68a9bf7b505eb0 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Mon, 23 Feb 2026 18:19:01 +0800 Subject: [PATCH 18/22] no message --- src/components/LoginPage/LoginPage.tsx | 5 +- src/components/Logo/Logo.tsx | 79 +++++++++++++++++++------- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx index 73f4c8a..5eb7c30 100644 --- a/src/components/LoginPage/LoginPage.tsx +++ b/src/components/LoginPage/LoginPage.tsx @@ -29,10 +29,11 @@ const LoginPage = () => { display: "flex", alignItems: "flex-end", justifyContent: "center", - svg: { maxHeight: 120 }, + backgroundImage: "linear-gradient(135deg, rgba(59,130,246,0.15) 0%, #f1f5f9 45%, #f8fafc 100%)", + backgroundColor: "#f8fafc", }} > - + diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index 0bc601c..f8bc458 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -6,14 +6,18 @@ interface Props { className?: string; } +/** Same logo height everywhere so login and main page look identical. */ +const DEFAULT_LOGO_HEIGHT = 42; + /** - * Logo: 3D-style badge (FP) + FP-MTMS wordmark. - * Badge uses gradient and highlight for depth; FP = Food Production, MTMS = system name. + * Logo: rounded badge (FP) with links motif inside + FP-MTMS wordmark. + * Uses fixed typography so words look the same on login and main page. */ -const Logo: React.FC = ({ height = 44, className = "" }) => { +const Logo: React.FC = ({ height = DEFAULT_LOGO_HEIGHT, className = "" }) => { const size = Math.max(28, height); const badgeSize = Math.round(size * 0.7); - const fontSize = Math.round(size * 0.5); + const titleFontSize = 21; + const subtitleFontSize = 10; const fpSize = badgeSize <= 22 ? 10 : badgeSize <= 28 ? 12 : 14; return ( @@ -22,7 +26,7 @@ const Logo: React.FC = ({ height = 44, className = "" }) => { style={{ display: "flex", flexShrink: 0 }} aria-label="FP-MTMS" > - {/* 3D badge: FP with gradient, top bevel, and soft shadow */} + {/* Badge: rounded square with links motif inside + FP */} = ({ height = 44, className = "" }) => { aria-hidden > - {/* Energetic blue gradient: bright top → deep blue bottom */} - + - {/* Shadow layer - deep blue */} - - {/* Main 3D body */} - - {/* Top bevel (inner 3D) */} - - {/* FP text */} + {/* Shadow */} + + {/* Body */} + + + {/* Links motif inside: small chain links in corners, clear center for FP */} + + + + + + + + + + + {/* FP text – top-right so it doesn’t overlap the links */} = ({ height = 44, className = "" }) => { FP - {/* Wordmark: FP-MTMS + subtitle — strong, energetic */} -
+ {/* Wordmark: fixed typography so login and main page match */} +
FP-MTMS Food Production From 3236f144cddd75a64569c3ad0e06fe30de6bb00e Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Mon, 23 Feb 2026 18:28:35 +0800 Subject: [PATCH 19/22] fix the /ps overlap problem --- src/app/(main)/ps/page.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index bca0a75..675380c 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -287,10 +287,10 @@ export default function ProductionSchedulePage() {
- {/* Detail Modal */} + {/* Detail Modal – z-index above sidebar drawer (1200) so they don't overlap on small windows */} {isDetailOpen && (
@@ -503,7 +503,7 @@ export default function ProductionSchedulePage() { {/* Export Dialog */} {isExportDialogOpen && (
From 42ee4a6d922816e4781e74dae1ada05236986c9d Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Tue, 24 Feb 2026 10:50:27 +0800 Subject: [PATCH 20/22] update --- src/app/api/stockIssue/actions.ts | 3 ++ src/components/General/LoadingComponent.tsx | 2 +- .../InputDataGrid/InputDataGrid.tsx | 2 +- .../Jodetail/FInishedJobOrderRecord.tsx | 2 + .../Jodetail/JobPickExecutionForm.tsx | 7 ++-- .../Jodetail/newJobPickExecution.tsx | 2 +- .../ProductionProcessList.tsx | 2 +- src/components/Qc/QcForm.tsx | 2 +- src/components/StockIssue/SearchPage.tsx | 13 +++++++ src/components/StockIssue/SubmitIssueForm.tsx | 39 +++++++++++++++++-- src/i18n/zh/common.json | 2 +- src/i18n/zh/inventory.json | 7 ++++ src/i18n/zh/jo.json | 2 +- 13 files changed, 70 insertions(+), 15 deletions(-) diff --git a/src/app/api/stockIssue/actions.ts b/src/app/api/stockIssue/actions.ts index 790c4fe..5d52d32 100644 --- a/src/app/api/stockIssue/actions.ts +++ b/src/app/api/stockIssue/actions.ts @@ -25,6 +25,7 @@ export interface StockIssueResult { handleStatus: string; handleDate: string | null; handledBy: number | null; + uomDesc: string | null; } export interface ExpiryItemResult { id: number; @@ -178,6 +179,8 @@ export async function submitMissItem(issueId: number, handler: number) { itemDescription: string | null; storeLocation: string | null; issues: IssueDetailItem[]; + bookQty: number; + uomDesc: string | null; } export interface IssueDetailItem { diff --git a/src/components/General/LoadingComponent.tsx b/src/components/General/LoadingComponent.tsx index fc802b2..868d187 100644 --- a/src/components/General/LoadingComponent.tsx +++ b/src/components/General/LoadingComponent.tsx @@ -8,7 +8,7 @@ export const LoadingComponent: React.FC = () => { display="flex" justifyContent="center" alignItems="center" - // autoheight="true" + > diff --git a/src/components/InputDataGrid/InputDataGrid.tsx b/src/components/InputDataGrid/InputDataGrid.tsx index 51ebecf..bab891d 100644 --- a/src/components/InputDataGrid/InputDataGrid.tsx +++ b/src/components/InputDataGrid/InputDataGrid.tsx @@ -370,7 +370,7 @@ function InputDataGrid({ // columns={!checkboxSelection ? _columns : columns} columns={needActions ? _columns : columns} editMode="row" - // autoHeight + sx={{ height: "30vh", "--DataGrid-overlayHeight": "100px", diff --git a/src/components/Jodetail/FInishedJobOrderRecord.tsx b/src/components/Jodetail/FInishedJobOrderRecord.tsx index e076661..5f5a0a8 100644 --- a/src/components/Jodetail/FInishedJobOrderRecord.tsx +++ b/src/components/Jodetail/FInishedJobOrderRecord.tsx @@ -509,9 +509,11 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { size="small" sx={{ mb: 1 }} /> + {/* {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")} + */} { // 增加 badPackageQty 判断,确保有坏包装会走 issue 流程 const badPackageQty = Number((formData as any).badPackageQty) || 0; - const isNormalPick = verifiedQty > 0 - && formData.missQty == 0 - && formData.badItemQty == 0 - && badPackageQty == 0; + const isNormalPick = (formData.missQty == null || formData.missQty === 0) + && (formData.badItemQty == null || formData.badItemQty === 0) + && (badPackageQty === 0); if (isNormalPick) { if (onNormalPickSubmit) { diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 4549061..3b9a9d8 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -1822,7 +1822,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { }, [handleSubmitPickQtyWithQty]); const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => - lot.stockOutLineStatus === 'checked' + lot.stockOutLineStatus === 'checked' || lot.stockOutLineStatus === 'partially_completed' ); if (scannedLots.length === 0) { diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index 2190567..956bdbd 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -306,7 +306,7 @@ const ProductProcessList: React.FC = ({ onSelectProcess )} {statusLower === "completed" && ( - )} diff --git a/src/components/Qc/QcForm.tsx b/src/components/Qc/QcForm.tsx index 46a33a4..8fac0e4 100644 --- a/src/components/Qc/QcForm.tsx +++ b/src/components/Qc/QcForm.tsx @@ -232,7 +232,7 @@ const QcForm: React.FC = ({ rows, disabled = false }) => { return ( <> - // autoHeight + = ({ dataList }) => { { name: "itemDescription", label: t("Item") }, { name: "lotNo", label: t("Lot No.") }, { name: "storeLocation", label: t("Location") }, + { + name: "bookQty", + label: t("Book Qty"), + renderCell: (item) => ( + <>{item.bookQty?.toFixed(2) ?? "0"} {item.uomDesc ?? ""} + ), + }, { name: "issueQty", label: t("Miss Qty") }, + { name: "uomDesc", label: t("UoM"), renderCell: (item) => ( + <>{item.uomDesc ?? ""} + ) }, { name: "id", label: t("Action"), @@ -196,6 +206,9 @@ const SearchPage: React.FC = ({ dataList }) => { { name: "lotNo", label: t("Lot No.") }, { name: "storeLocation", label: t("Location") }, { name: "issueQty", label: t("Defective Qty") }, + { name: "uomDesc", label: t("UoM"), renderCell: (item) => ( + <>{item.uomDesc ?? ""} + ) }, { name: "id", label: t("Action"), diff --git a/src/components/StockIssue/SubmitIssueForm.tsx b/src/components/StockIssue/SubmitIssueForm.tsx index 37d6bab..1a891fe 100644 --- a/src/components/StockIssue/SubmitIssueForm.tsx +++ b/src/components/StockIssue/SubmitIssueForm.tsx @@ -49,7 +49,10 @@ const SubmitIssueForm: React.FC = ({ const [submitting, setSubmitting] = useState(false); const [details, setDetails] = useState(null); const [submitQty, setSubmitQty] = useState(""); - + const bookQty = details?.bookQty ?? 0; + const submitQtyNum = parseFloat(submitQty); + const submitQtyValid = !Number.isNaN(submitQtyNum) && submitQtyNum >= 0; + const remainAvailable = submitQtyValid ? Math.max(0, bookQty - submitQtyNum) : bookQty; useEffect(() => { if (open && lotId) { loadDetails(); @@ -121,9 +124,17 @@ const SubmitIssueForm: React.FC = ({ {t("Lot No.")}: {details.lotNo} - + {t("Location")}: {details.storeLocation} + + {t("Book Qty")}:{" "} + {details.bookQty} + + + {t("UoM")}:{" "} + {details.uomDesc ?? ""} + @@ -146,8 +157,8 @@ const SubmitIssueForm: React.FC = ({ {issue.pickerName || "-"} {issueType === "miss" - ? issue.missQty?.toFixed(2) || "0" - : issue.issueQty?.toFixed(2) || "0"} + ? issue.missQty?.toFixed(0) || "0" + : issue.issueQty?.toFixed(0) || "0"} {issue.pickOrderCode} {issue.doOrderCode || "-"} @@ -168,6 +179,26 @@ const SubmitIssueForm: React.FC = ({ inputProps={{ min: 0, step: 0.01 }} sx={{ mt: 2 }} /> + { + const raw = e.target.value; + if (raw === "") { + setSubmitQty(""); + return; + } + const remain = parseFloat(raw); + if (!Number.isNaN(remain) && remain >= 0) { + const newSubmit = Math.max(0, bookQty - remain); + setSubmitQty(newSubmit.toFixed(0)); + } + }} + inputProps={{ min: 0, step: 0.01, readOnly: false }} + sx={{ mt: 2 }} + />