| @@ -26,6 +26,7 @@ import { | |||||
| generateSemiFGProductionAnalysisReportExcel, | generateSemiFGProductionAnalysisReportExcel, | ||||
| ItemCodeWithCategory, | ItemCodeWithCategory, | ||||
| } from './semiFGProductionAnalysisApi'; | } from './semiFGProductionAnalysisApi'; | ||||
| import { parseItemCodeTokens } from './parseItemCodeTokens'; | |||||
| interface SemiFGProductionAnalysisReportProps { | interface SemiFGProductionAnalysisReportProps { | ||||
| criteria: Record<string, string>; | criteria: Record<string, string>; | ||||
| @@ -76,16 +77,15 @@ export default function SemiFGProductionAnalysisReport({ | |||||
| return; | return; | ||||
| } | } | ||||
| // If no itemCode is selected, export directly without confirmation | |||||
| if (!criteria.itemCode) { | |||||
| const selectedCodes = parseItemCodeTokens( | |||||
| [criteria.itemCode, criteria.itemCodePaste].filter(Boolean).join(' '), | |||||
| ); | |||||
| if (selectedCodes.length === 0) { | |||||
| await executeExport(format); | await executeExport(format); | ||||
| return; | return; | ||||
| } | } | ||||
| // If itemCode is selected, show confirmation dialog | |||||
| const selectedCodes = criteria.itemCode.split(',').filter((code) => code.trim()); | |||||
| const itemCodesInfo: ItemCodeWithCategory[] = selectedCodes.map((code) => { | |||||
| const codeTrimmed = code.trim(); | |||||
| const itemCodesInfo: ItemCodeWithCategory[] = selectedCodes.map((codeTrimmed) => { | |||||
| const categoryInfo = itemCodesWithCategory[codeTrimmed]; | const categoryInfo = itemCodesWithCategory[codeTrimmed]; | ||||
| return { | return { | ||||
| code: codeTrimmed, | code: codeTrimmed, | ||||
| @@ -2,6 +2,7 @@ | |||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | ||||
| import { buildItemCodePasteRequestBody } from "./parseItemCodeTokens"; | |||||
| import { | import { | ||||
| exportMultiSheetToXlsx, | exportMultiSheetToXlsx, | ||||
| } from "@/app/(main)/chart/_components/exportChartToXlsx"; | } from "@/app/(main)/chart/_components/exportChartToXlsx"; | ||||
| @@ -124,12 +125,15 @@ function emptyMaterialSheetRow( | |||||
| export async function fetchBomShopSyncReportData( | export async function fetchBomShopSyncReportData( | ||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| ): Promise<BomShopSyncReportResponse> { | ): Promise<BomShopSyncReportResponse> { | ||||
| const queryParams = new URLSearchParams(criteria).toString(); | |||||
| const url = `${NEXT_PUBLIC_API_URL}/report/bom-shop-sync-history?${queryParams}`; | |||||
| const url = `${NEXT_PUBLIC_API_URL}/report/bom-shop-sync-history`; | |||||
| const response = await clientAuthFetch(url, { | const response = await clientAuthFetch(url, { | ||||
| method: "GET", | |||||
| headers: { Accept: "application/json" }, | |||||
| method: "POST", | |||||
| headers: { | |||||
| Accept: "application/json", | |||||
| "Content-Type": "application/json", | |||||
| }, | |||||
| body: JSON.stringify(buildItemCodePasteRequestBody(criteria, "finishedItemCode")), | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) | if (response.status === 401 || response.status === 403) | ||||
| @@ -1,6 +1,7 @@ | |||||
| "use client"; | "use client"; | ||||
| import { NEXT_PUBLIC_API_URL } from "@/config/api"; | import { NEXT_PUBLIC_API_URL } from "@/config/api"; | ||||
| import { buildItemCodePasteRequestBody } from './parseItemCodeTokens'; | |||||
| import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; | ||||
| import { | import { | ||||
| exportChartToXlsx, | exportChartToXlsx, | ||||
| @@ -64,17 +65,20 @@ export interface GrnReportResponse { | |||||
| /** | /** | ||||
| * Fetch GRN (Goods Received Note) report data by date range. | * Fetch GRN (Goods Received Note) report data by date range. | ||||
| * Backend: GET /report/grn-report?receiptDateStart=&receiptDateEnd=&itemCode= | |||||
| * Backend: POST /report/grn-report { receiptDateStart, receiptDateEnd, itemCodes } | |||||
| */ | */ | ||||
| export async function fetchGrnReportData( | export async function fetchGrnReportData( | ||||
| criteria: Record<string, string> | criteria: Record<string, string> | ||||
| ): Promise<{ rows: GrnReportRow[]; listedPoAmounts?: ListedPoAmounts }> { | ): Promise<{ rows: GrnReportRow[]; listedPoAmounts?: ListedPoAmounts }> { | ||||
| const queryParams = new URLSearchParams(criteria).toString(); | |||||
| const url = `${NEXT_PUBLIC_API_URL}/report/grn-report?${queryParams}`; | |||||
| const url = `${NEXT_PUBLIC_API_URL}/report/grn-report`; | |||||
| const response = await clientAuthFetch(url, { | const response = await clientAuthFetch(url, { | ||||
| method: "GET", | |||||
| headers: { Accept: "application/json" }, | |||||
| method: "POST", | |||||
| headers: { | |||||
| Accept: "application/json", | |||||
| "Content-Type": "application/json", | |||||
| }, | |||||
| body: JSON.stringify(buildItemCodePasteRequestBody(criteria, "itemCode")), | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) | if (response.status === 401 || response.status === 403) | ||||
| @@ -26,7 +26,7 @@ import { | |||||
| import DownloadIcon from '@mui/icons-material/Download'; | import DownloadIcon from '@mui/icons-material/Download'; | ||||
| import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; | ||||
| import { REPORTS } from '@/config/reportConfig'; | import { REPORTS } from '@/config/reportConfig'; | ||||
| import { mergePastedItemCodes, buildStockBalanceRequestBody } from './parseItemCodeTokens'; | |||||
| import { mergePastedItemCodes, buildItemCodePasteRequestBody } from './parseItemCodeTokens'; | |||||
| import { NEXT_PUBLIC_API_URL } from '@/config/api'; | import { NEXT_PUBLIC_API_URL } from '@/config/api'; | ||||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | ||||
| import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; | import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; | ||||
| @@ -75,6 +75,15 @@ const FIELD_ERROR_SX = { | |||||
| }, | }, | ||||
| }; | }; | ||||
| const ITEM_CODE_PASTE_POST_REPORTS = new Set([ | |||||
| 'rep-004', | |||||
| 'rep-006', | |||||
| 'rep-007', | |||||
| 'rep-008', | |||||
| 'rep-011', | |||||
| 'rep-013', | |||||
| ]); | |||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.3 | 2026-09-10 */ | /** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.3 | 2026-09-10 */ | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.1 | 2026-08-11 */ | /** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.1 | 2026-08-11 */ | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 69 | v1.0.2 | 2026-09-10 */ | /** FP-MTMS Version Checklist | Functions Ref. No. 69 | v1.0.2 | 2026-09-10 */ | ||||
| @@ -359,11 +368,19 @@ export default function ReportPage() { | |||||
| return p.toString(); | return p.toString(); | ||||
| }; | }; | ||||
| const itemCodeFieldName = (): string => | |||||
| currentReport?.fields.some((f) => f.name === 'finishedItemCode') | |||||
| ? 'finishedItemCode' | |||||
| : 'itemCode'; | |||||
| const isItemCodePastePostReport = (): boolean => | |||||
| !!currentReport && ITEM_CODE_PASTE_POST_REPORTS.has(currentReport.id); | |||||
| const buildCriteriaQueryString = (): string => { | const buildCriteriaQueryString = (): string => { | ||||
| if (!currentReport) return ''; | if (!currentReport) return ''; | ||||
| if (currentReport.id === 'rep-012') return buildRep012QueryString(); | if (currentReport.id === 'rep-012') return buildRep012QueryString(); | ||||
| if (currentReport.id === 'rep-010') return buildRep010QueryString(); | if (currentReport.id === 'rep-010') return buildRep010QueryString(); | ||||
| const merged = mergePastedItemCodes(criteria); | |||||
| const merged = mergePastedItemCodes(criteria, itemCodeFieldName()); | |||||
| const p = new URLSearchParams(merged); | const p = new URLSearchParams(merged); | ||||
| if (currentReport.id === 'rep-016') { | if (currentReport.id === 'rep-016') { | ||||
| const day = (merged.dateStart || '').trim(); | const day = (merged.dateStart || '').trim(); | ||||
| @@ -416,19 +433,19 @@ export default function ReportPage() { | |||||
| await generateShopOrderReplenishmentReportExcel(criteria, reportTitle(currentReport), t); | await generateShopOrderReplenishmentReportExcel(criteria, reportTitle(currentReport), t); | ||||
| } else { | } else { | ||||
| // Backend returns actual .xlsx bytes for this Excel endpoint. | // Backend returns actual .xlsx bytes for this Excel endpoint. | ||||
| const isStockBalance = currentReport.id === 'rep-007'; | |||||
| const excelUrl = isStockBalance | |||||
| const usesItemCodePastePost = isItemCodePastePostReport(); | |||||
| const excelUrl = usesItemCodePastePost | |||||
| ? `${currentReport.apiEndpoint}-excel` | ? `${currentReport.apiEndpoint}-excel` | ||||
| : `${currentReport.apiEndpoint}-excel?${buildCriteriaQueryString()}`; | : `${currentReport.apiEndpoint}-excel?${buildCriteriaQueryString()}`; | ||||
| const response = await clientAuthFetch(excelUrl, isStockBalance | |||||
| const response = await clientAuthFetch(excelUrl, usesItemCodePastePost | |||||
| ? { | ? { | ||||
| method: 'POST', | method: 'POST', | ||||
| headers: { | headers: { | ||||
| Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | ||||
| 'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||||
| }, | }, | ||||
| body: JSON.stringify(buildStockBalanceRequestBody(criteria)), | |||||
| body: JSON.stringify(buildItemCodePasteRequestBody(criteria, itemCodeFieldName())), | |||||
| } | } | ||||
| : { | : { | ||||
| method: 'GET', | method: 'GET', | ||||
| @@ -485,19 +502,19 @@ export default function ReportPage() { | |||||
| setLoading(true); | setLoading(true); | ||||
| try { | try { | ||||
| const isStockBalance = currentReport.id === 'rep-007'; | |||||
| const url = isStockBalance | |||||
| const usesItemCodePastePost = isItemCodePastePostReport(); | |||||
| const url = usesItemCodePastePost | |||||
| ? currentReport.apiEndpoint | ? currentReport.apiEndpoint | ||||
| : `${currentReport.apiEndpoint}?${buildCriteriaQueryString()}`; | : `${currentReport.apiEndpoint}?${buildCriteriaQueryString()}`; | ||||
| const response = await clientAuthFetch(url, isStockBalance | |||||
| const response = await clientAuthFetch(url, usesItemCodePastePost | |||||
| ? { | ? { | ||||
| method: 'POST', | method: 'POST', | ||||
| headers: { | headers: { | ||||
| Accept: 'application/pdf', | Accept: 'application/pdf', | ||||
| 'Content-Type': 'application/json', | 'Content-Type': 'application/json', | ||||
| }, | }, | ||||
| body: JSON.stringify(buildStockBalanceRequestBody(criteria)), | |||||
| body: JSON.stringify(buildItemCodePasteRequestBody(criteria, itemCodeFieldName())), | |||||
| } | } | ||||
| : { | : { | ||||
| method: 'GET', | method: 'GET', | ||||
| @@ -16,32 +16,47 @@ export function parseItemCodeTokens(raw: string | undefined | null): string[] { | |||||
| return tokens; | return tokens; | ||||
| } | } | ||||
| /** Merge multi-select `itemCode` with pasted `itemCodePaste`; drop the paste field from API params. */ | |||||
| export function mergePastedItemCodes(criteria: Record<string, string>): Record<string, string> { | |||||
| export function resolveItemCodeFieldName(criteria: Record<string, string>): string { | |||||
| return Object.prototype.hasOwnProperty.call(criteria, "finishedItemCode") | |||||
| ? "finishedItemCode" | |||||
| : "itemCode"; | |||||
| } | |||||
| /** Merge multi-select item codes with pasted `itemCodePaste`; drop the paste field from API params. */ | |||||
| export function mergePastedItemCodes( | |||||
| criteria: Record<string, string>, | |||||
| codeField = resolveItemCodeFieldName(criteria), | |||||
| ): Record<string, string> { | |||||
| const next: Record<string, string> = { ...criteria }; | const next: Record<string, string> = { ...criteria }; | ||||
| delete next.itemCodePaste; | delete next.itemCodePaste; | ||||
| const merged = parseItemCodeTokens( | const merged = parseItemCodeTokens( | ||||
| [criteria.itemCode, criteria.itemCodePaste].filter(Boolean).join(" "), | |||||
| [criteria[codeField], criteria.itemCodePaste].filter(Boolean).join(" "), | |||||
| ); | ); | ||||
| if (merged.length > 0) { | if (merged.length > 0) { | ||||
| next.itemCode = merged.join(","); | |||||
| next[codeField] = merged.join(","); | |||||
| } else { | } else { | ||||
| delete next.itemCode; | |||||
| delete next[codeField]; | |||||
| } | } | ||||
| return next; | return next; | ||||
| } | } | ||||
| /** POST body for 庫存結餘報告 so pasted codes are not limited by URL length. */ | |||||
| export function buildStockBalanceRequestBody(criteria: Record<string, string>): { | |||||
| stockDate?: string; | |||||
| itemCodes?: string[]; | |||||
| } { | |||||
| /** POST body so pasted codes are not limited by URL length. */ | |||||
| export function buildItemCodePasteRequestBody( | |||||
| criteria: Record<string, string>, | |||||
| codeField = resolveItemCodeFieldName(criteria), | |||||
| ): Record<string, unknown> { | |||||
| const itemCodes = parseItemCodeTokens( | const itemCodes = parseItemCodeTokens( | ||||
| [criteria.itemCode, criteria.itemCodePaste].filter(Boolean).join(" "), | |||||
| [criteria[codeField], criteria.itemCodePaste].filter(Boolean).join(" "), | |||||
| ); | ); | ||||
| const body: { stockDate?: string; itemCodes?: string[] } = {}; | |||||
| const stockDate = criteria.stockDate?.trim(); | |||||
| if (stockDate) body.stockDate = stockDate; | |||||
| if (itemCodes.length) body.itemCodes = itemCodes; | |||||
| const body: Record<string, unknown> = {}; | |||||
| for (const [key, value] of Object.entries(criteria)) { | |||||
| if (key === "itemCodePaste" || key === codeField) continue; | |||||
| const trimmed = value?.trim(); | |||||
| if (trimmed) body[key] = trimmed; | |||||
| } | |||||
| if (itemCodes.length) { | |||||
| body[codeField] = itemCodes.join(","); | |||||
| body.itemCodes = itemCodes; | |||||
| } | |||||
| return body; | return body; | ||||
| } | } | ||||
| @@ -2,6 +2,7 @@ | |||||
| import { NEXT_PUBLIC_API_URL } from '@/config/api'; | import { NEXT_PUBLIC_API_URL } from '@/config/api'; | ||||
| import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; | ||||
| import { buildItemCodePasteRequestBody } from './parseItemCodeTokens'; | |||||
| export interface ItemCodeWithName { | export interface ItemCodeWithName { | ||||
| code: string; | code: string; | ||||
| @@ -72,12 +73,15 @@ export const generateSemiFGProductionAnalysisReport = async ( | |||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = '成品/半成品生產分析報告' | reportTitle: string = '成品/半成品生產分析報告' | ||||
| ): Promise<void> => { | ): Promise<void> => { | ||||
| const queryParams = new URLSearchParams(criteria).toString(); | |||||
| const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis?${queryParams}`; | |||||
| const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis`; | |||||
| const response = await clientAuthFetch(url, { | const response = await clientAuthFetch(url, { | ||||
| method: 'GET', | |||||
| headers: { Accept: 'application/pdf' }, | |||||
| method: 'POST', | |||||
| headers: { | |||||
| Accept: 'application/pdf', | |||||
| 'Content-Type': 'application/json', | |||||
| }, | |||||
| body: JSON.stringify(buildItemCodePasteRequestBody(criteria, 'itemCode')), | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); | if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); | ||||
| @@ -111,12 +115,15 @@ export const generateSemiFGProductionAnalysisReportExcel = async ( | |||||
| criteria: Record<string, string>, | criteria: Record<string, string>, | ||||
| reportTitle: string = '成品/半成品生產分析報告' | reportTitle: string = '成品/半成品生產分析報告' | ||||
| ): Promise<void> => { | ): Promise<void> => { | ||||
| const queryParams = new URLSearchParams(criteria).toString(); | |||||
| const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis-excel?${queryParams}`; | |||||
| const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis-excel`; | |||||
| const response = await clientAuthFetch(url, { | const response = await clientAuthFetch(url, { | ||||
| method: 'GET', | |||||
| headers: { Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, | |||||
| method: 'POST', | |||||
| headers: { | |||||
| Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', | |||||
| 'Content-Type': 'application/json', | |||||
| }, | |||||
| body: JSON.stringify(buildItemCodePasteRequestBody(criteria, 'itemCode')), | |||||
| }); | }); | ||||
| if (response.status === 401 || response.status === 403) throw new Error('Unauthorized'); | if (response.status === 401 || response.status === 403) throw new Error('Unauthorized'); | ||||
| @@ -305,34 +305,45 @@ const PoSearch: React.FC<Props> = ({ | |||||
| const res = await baseListResp.json(); | const res = await baseListResp.json(); | ||||
| if (!res) return; | if (!res) return; | ||||
| if (res.records && res.records.length > 0) { | |||||
| setFilteredPo(res.records); | |||||
| setTotalCount(res.total); | |||||
| return; | |||||
| } | |||||
| const records: PoResult[] = res.records ?? []; | |||||
| const searchedCodeRaw = (filterArgs as any)?.code; | const searchedCodeRaw = (filterArgs as any)?.code; | ||||
| const searchedCode = | const searchedCode = | ||||
| typeof searchedCodeRaw === "string" ? searchedCodeRaw.trim() : ""; | typeof searchedCodeRaw === "string" ? searchedCodeRaw.trim() : ""; | ||||
| const shouldAutoSyncFromM18 = | |||||
| const isM18PoCode = | |||||
| searchedCode.length > 14 && | searchedCode.length > 14 && | ||||
| (searchedCode.startsWith("PP") || searchedCode.startsWith("PF")); | (searchedCode.startsWith("PP") || searchedCode.startsWith("PF")); | ||||
| const hasLocalPending = records.some( | |||||
| (row) => String(row.status ?? "").toLowerCase() === "pending", | |||||
| ); | |||||
| const shouldLookupMissing = records.length === 0 && isM18PoCode; | |||||
| const shouldRefreshPending = hasLocalPending && isM18PoCode; | |||||
| if (!shouldAutoSyncFromM18 || autoSyncInProgressRef.current) { | |||||
| setFilteredPo(res.records); | |||||
| if ( | |||||
| (!shouldLookupMissing && !shouldRefreshPending) || | |||||
| autoSyncInProgressRef.current | |||||
| ) { | |||||
| setFilteredPo(records); | |||||
| setTotalCount(res.total); | setTotalCount(res.total); | ||||
| return; | return; | ||||
| } | } | ||||
| if (shouldRefreshPending) { | |||||
| setFilteredPo(records); | |||||
| setTotalCount(res.total); | |||||
| } | |||||
| try { | try { | ||||
| autoSyncInProgressRef.current = true; | autoSyncInProgressRef.current = true; | ||||
| setIsM18LookupLoading(true); | |||||
| setAutoSyncStatus("正在從M18找尋PO..."); | |||||
| setIsM18LookupLoading(shouldLookupMissing); | |||||
| setAutoSyncStatus( | |||||
| shouldLookupMissing | |||||
| ? "正在從M18找尋PO..." | |||||
| : "正在檢查M18是否有更新...", | |||||
| ); | |||||
| const syncResp = await clientAuthFetch( | const syncResp = await clientAuthFetch( | ||||
| `${NEXT_PUBLIC_API_URL}/m18/test/po-by-code?code=${encodeURIComponent( | `${NEXT_PUBLIC_API_URL}/m18/test/po-by-code?code=${encodeURIComponent( | ||||
| searchedCode, | searchedCode, | ||||
| )}`, | |||||
| )}&ifNewer=true`, | |||||
| { method: "GET" }, | { method: "GET" }, | ||||
| ); | ); | ||||
| @@ -350,8 +361,13 @@ const PoSearch: React.FC<Props> = ({ | |||||
| } | } | ||||
| const syncOk = Boolean(syncJson?.totalSuccess && syncJson.totalSuccess > 0); | const syncOk = Boolean(syncJson?.totalSuccess && syncJson.totalSuccess > 0); | ||||
| const skippedIfNewer = String(syncJson?.query ?? "").includes( | |||||
| "skipped (ifNewer)", | |||||
| ); | |||||
| if (syncOk) { | if (syncOk) { | ||||
| setAutoSyncStatus("成功找到PO"); | |||||
| setAutoSyncStatus( | |||||
| shouldLookupMissing ? "成功找到PO" : "已從M18更新PO", | |||||
| ); | |||||
| const listResp = await clientAuthFetch( | const listResp = await clientAuthFetch( | ||||
| `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams( | `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams( | ||||
| @@ -363,21 +379,24 @@ const PoSearch: React.FC<Props> = ({ | |||||
| const listJson = await listResp.json(); | const listJson = await listResp.json(); | ||||
| setFilteredPo(listJson.records ?? []); | setFilteredPo(listJson.records ?? []); | ||||
| setTotalCount(listJson.total ?? 0); | setTotalCount(listJson.total ?? 0); | ||||
| setAutoSyncStatus("成功找到PO"); | |||||
| setAutoSyncStatus( | |||||
| shouldLookupMissing ? "成功找到PO" : "已從M18更新PO", | |||||
| ); | |||||
| return; | return; | ||||
| } | } | ||||
| setAutoSyncStatus("找不到PO"); | |||||
| setAutoSyncStatus(shouldLookupMissing ? "找不到PO" : null); | |||||
| } else if (skippedIfNewer || shouldRefreshPending) { | |||||
| setAutoSyncStatus(null); | |||||
| } else { | } else { | ||||
| setAutoSyncStatus("找不到PO"); | setAutoSyncStatus("找不到PO"); | ||||
| } | } | ||||
| // Ensure UI updates even if sync didn't change results | |||||
| setFilteredPo(res.records); | |||||
| setFilteredPo(records); | |||||
| setTotalCount(res.total ?? 0); | setTotalCount(res.total ?? 0); | ||||
| } catch (e) { | } catch (e) { | ||||
| console.error("Auto sync error:", e); | console.error("Auto sync error:", e); | ||||
| setAutoSyncStatus("找不到PO"); | |||||
| setFilteredPo(res.records); | |||||
| setAutoSyncStatus(shouldLookupMissing ? "找不到PO" : null); | |||||
| setFilteredPo(records); | |||||
| setTotalCount(res.total ?? 0); | setTotalCount(res.total ?? 0); | ||||
| } finally { | } finally { | ||||
| setIsM18LookupLoading(false); | setIsM18LookupLoading(false); | ||||
| @@ -481,7 +500,9 @@ const PoSearch: React.FC<Props> = ({ | |||||
| sx={{ color: "#fff", zIndex: (theme) => theme.zIndex.modal + 1, flexDirection: "column", gap: 1 }} | sx={{ color: "#fff", zIndex: (theme) => theme.zIndex.modal + 1, flexDirection: "column", gap: 1 }} | ||||
| > | > | ||||
| <CircularProgress color="inherit" /> | <CircularProgress color="inherit" /> | ||||
| <Typography variant="body1">正在從M18找尋PO...</Typography> | |||||
| <Typography variant="body1"> | |||||
| {autoSyncStatus || "正在從M18找尋PO..."} | |||||
| </Typography> | |||||
| </Backdrop> | </Backdrop> | ||||
| </> | </> | ||||
| </> | </> | ||||
| @@ -57,6 +57,15 @@ const asyncItemCodeField = ( | |||||
| placeholder: "e.g. FA0591", | placeholder: "e.g. FA0591", | ||||
| }); | }); | ||||
| const pasteItemCodeField = (): ReportField => ({ | |||||
| label: "貼上貨品編號 Paste Item Codes", | |||||
| name: "itemCodePaste", | |||||
| type: "text", | |||||
| required: false, | |||||
| multiline: true, | |||||
| minRows: 4, | |||||
| }); | |||||
| export const REPORTS: ReportDefinition[] = [ | export const REPORTS: ReportDefinition[] = [ | ||||
| //{ | //{ | ||||
| // id: "rep-001", | // id: "rep-001", | ||||
| @@ -109,6 +118,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "入倉日期:由 Last In Date Start", name: "lastInDateStart", type: "date", 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: "入倉日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, | ||||
| asyncItemCodeField(), | asyncItemCodeField(), | ||||
| pasteItemCodeField(), | |||||
| { | { | ||||
| label: "樓層 Store ID", | label: "樓層 Store ID", | ||||
| name: "storeId", | name: "storeId", | ||||
| @@ -148,6 +158,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "出貨日期:至 Last Out Date End", name: "lastOutDateEnd", 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: "年份 Year", name: "year", type: "text", required: false, placeholder: "e.g. 2026" }, | ||||
| asyncItemCodeField(), | asyncItemCodeField(), | ||||
| pasteItemCodeField(), | |||||
| ] | ] | ||||
| }, | }, | ||||
| /* | /* | ||||
| @@ -302,6 +313,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "庫存日期:由 Last In Date Start", name: "lastInDateStart", type: "date", 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: "庫存日期:至 Last In Date End", name: "lastInDateEnd", type: "date", required: false }, | ||||
| asyncItemCodeField(), | asyncItemCodeField(), | ||||
| pasteItemCodeField(), | |||||
| ] | ] | ||||
| }, | }, | ||||
| /* Hidden for now: 庫存流水帳報告 (rep-020) | /* Hidden for now: 庫存流水帳報告 (rep-020) | ||||
| @@ -344,14 +356,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| fields: [ | fields: [ | ||||
| { label: "庫存日期: Stock Date", name: "stockDate", type: "date", required: true }, | { label: "庫存日期: Stock Date", name: "stockDate", type: "date", required: true }, | ||||
| asyncItemCodeField(), | asyncItemCodeField(), | ||||
| { | |||||
| label: "貼上貨品編號 Paste Item Codes", | |||||
| name: "itemCodePaste", | |||||
| type: "text", | |||||
| required: false, | |||||
| multiline: true, | |||||
| minRows: 4, | |||||
| }, | |||||
| pasteItemCodeField(), | |||||
| ] | ] | ||||
| }, | }, | ||||
| @@ -365,6 +370,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "收貨日期:由 Receipt Date Start", name: "receiptDateStart", type: "date", required: false }, | { label: "收貨日期:由 Receipt Date Start", name: "receiptDateStart", type: "date", required: false }, | ||||
| { label: "收貨日期:至 Receipt Date End", name: "receiptDateEnd", type: "date", required: false }, | { label: "收貨日期:至 Receipt Date End", name: "receiptDateEnd", type: "date", required: false }, | ||||
| asyncItemCodeField(), | asyncItemCodeField(), | ||||
| pasteItemCodeField(), | |||||
| ], | ], | ||||
| }, | }, | ||||
| @@ -417,6 +423,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "出倉日期:由 Last Out Date Start", name: "lastOutDateStart", 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: "出倉日期:至 Last Out Date End", name: "lastOutDateEnd", type: "date", required: false }, | ||||
| asyncItemCodeField(), | asyncItemCodeField(), | ||||
| pasteItemCodeField(), | |||||
| { label: "提料人 Handler", name: "handler", type: "select", required: false, | { label: "提料人 Handler", name: "handler", type: "select", required: false, | ||||
| multiple: true, | multiple: true, | ||||
| dynamicOptions: true, | dynamicOptions: true, | ||||
| @@ -449,6 +456,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-item-code-prefixes`, | dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/stock-item-code-prefixes`, | ||||
| dynamicOptionsParam: "stockCategory", | dynamicOptionsParam: "stockCategory", | ||||
| options: [] }, | options: [] }, | ||||
| pasteItemCodeField(), | |||||
| ] | ] | ||||
| }, | }, | ||||
| @@ -475,6 +483,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`, | dynamicOptionsEndpoint: `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`, | ||||
| dynamicOptionsParam: "stockCategory", | dynamicOptionsParam: "stockCategory", | ||||
| options: [] }, | options: [] }, | ||||
| pasteItemCodeField(), | |||||
| ] | ] | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -486,6 +495,7 @@ export const REPORTS: ReportDefinition[] = [ | |||||
| { label: "同步日期:由 Sync Date Start", name: "syncDateStart", type: "date", required: false }, | { label: "同步日期:由 Sync Date Start", name: "syncDateStart", type: "date", required: false }, | ||||
| { label: "同步日期:至 Sync Date End", name: "syncDateEnd", type: "date", required: false }, | { label: "同步日期:至 Sync Date End", name: "syncDateEnd", type: "date", required: false }, | ||||
| asyncItemCodeField("成品貨號 Finished Item Code", "finishedItemCode"), | asyncItemCodeField("成品貨號 Finished Item Code", "finishedItemCode"), | ||||
| pasteItemCodeField(), | |||||
| { | { | ||||
| label: "同步狀態 Sync Status", | label: "同步狀態 Sync Status", | ||||
| name: "syncStatus", | name: "syncStatus", | ||||
| @@ -52,6 +52,7 @@ | |||||
| "lastInDateStart": "Last In Date Start", | "lastInDateStart": "Last In Date Start", | ||||
| "lastInDateEnd": "Last In Date End", | "lastInDateEnd": "Last In Date End", | ||||
| "itemCode": "Item Code", | "itemCode": "Item Code", | ||||
| "itemCodePaste": "Paste Item Codes", | |||||
| "storeId": "Floor", | "storeId": "Floor", | ||||
| "warehouse": "Warehouse", | "warehouse": "Warehouse", | ||||
| "area": "Area", | "area": "Area", | ||||
| @@ -68,6 +69,9 @@ | |||||
| "PP": "PP", | "PP": "PP", | ||||
| "PF": "PF" | "PF": "PF" | ||||
| } | } | ||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-008": { | "rep-008": { | ||||
| @@ -76,7 +80,11 @@ | |||||
| "lastOutDateStart": "Last Out Date Start", | "lastOutDateStart": "Last Out Date Start", | ||||
| "lastOutDateEnd": "Last Out Date End", | "lastOutDateEnd": "Last Out Date End", | ||||
| "year": "Year", | "year": "Year", | ||||
| "itemCode": "Item Code" | |||||
| "itemCode": "Item Code", | |||||
| "itemCodePaste": "Paste Item Codes" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-012": { | "rep-012": { | ||||
| @@ -115,7 +123,11 @@ | |||||
| "fields": { | "fields": { | ||||
| "lastInDateStart": "Stock Date Start", | "lastInDateStart": "Stock Date Start", | ||||
| "lastInDateEnd": "Stock Date End", | "lastInDateEnd": "Stock Date End", | ||||
| "itemCode": "Item Code" | |||||
| "itemCode": "Item Code", | |||||
| "itemCodePaste": "Paste Item Codes" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-007": { | "rep-007": { | ||||
| @@ -134,7 +146,11 @@ | |||||
| "fields": { | "fields": { | ||||
| "receiptDateStart": "Receipt Date Start", | "receiptDateStart": "Receipt Date Start", | ||||
| "receiptDateEnd": "Receipt Date End", | "receiptDateEnd": "Receipt Date End", | ||||
| "itemCode": "Item Code" | |||||
| "itemCode": "Item Code", | |||||
| "itemCodePaste": "Paste Item Codes" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-009": { | "rep-009": { | ||||
| @@ -173,7 +189,11 @@ | |||||
| "lastOutDateStart": "Last Out Date Start", | "lastOutDateStart": "Last Out Date Start", | ||||
| "lastOutDateEnd": "Last Out Date End", | "lastOutDateEnd": "Last Out Date End", | ||||
| "itemCode": "Item Code", | "itemCode": "Item Code", | ||||
| "itemCodePaste": "Paste Item Codes", | |||||
| "handler": "Handler" | "handler": "Handler" | ||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-006": { | "rep-006": { | ||||
| @@ -183,7 +203,11 @@ | |||||
| "lastOutDateEnd": "Consumption Date End", | "lastOutDateEnd": "Consumption Date End", | ||||
| "year": "Year", | "year": "Year", | ||||
| "stockCategory": "Category", | "stockCategory": "Category", | ||||
| "itemCode": "Item Code" | |||||
| "itemCode": "Item Code", | |||||
| "itemCodePaste": "Paste Item Codes" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-005": { | "rep-005": { | ||||
| @@ -193,7 +217,11 @@ | |||||
| "lastOutDateEnd": "Production Complete Date End", | "lastOutDateEnd": "Production Complete Date End", | ||||
| "year": "Year", | "year": "Year", | ||||
| "stockCategory": "Category", | "stockCategory": "Category", | ||||
| "itemCode": "Item Code" | |||||
| "itemCode": "Item Code", | |||||
| "itemCodePaste": "Paste Item Codes" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-015": { | "rep-015": { | ||||
| @@ -202,8 +230,12 @@ | |||||
| "syncDateStart": "Sync Date Start", | "syncDateStart": "Sync Date Start", | ||||
| "syncDateEnd": "Sync Date End", | "syncDateEnd": "Sync Date End", | ||||
| "finishedItemCode": "Finished Item Code", | "finishedItemCode": "Finished Item Code", | ||||
| "itemCodePaste": "Paste Item Codes", | |||||
| "syncStatus": "Sync Status" | "syncStatus": "Sync Status" | ||||
| }, | }, | ||||
| "fieldHints": { | |||||
| "itemCodePaste": "Paste item codes from Excel. Separate with spaces, commas, or new lines" | |||||
| }, | |||||
| "options": { | "options": { | ||||
| "syncStatus": { | "syncStatus": { | ||||
| "all": "All", | "all": "All", | ||||
| @@ -52,6 +52,7 @@ | |||||
| "lastInDateStart": "入倉日期:由", | "lastInDateStart": "入倉日期:由", | ||||
| "lastInDateEnd": "入倉日期:至", | "lastInDateEnd": "入倉日期:至", | ||||
| "itemCode": "貨品編號", | "itemCode": "貨品編號", | ||||
| "itemCodePaste": "貼上貨品編號", | |||||
| "storeId": "樓層", | "storeId": "樓層", | ||||
| "warehouse": "倉庫", | "warehouse": "倉庫", | ||||
| "area": "區域", | "area": "區域", | ||||
| @@ -68,6 +69,9 @@ | |||||
| "PP": "PP", | "PP": "PP", | ||||
| "PF": "PF" | "PF": "PF" | ||||
| } | } | ||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-008": { | "rep-008": { | ||||
| @@ -76,7 +80,11 @@ | |||||
| "lastOutDateStart": "出貨日期:由", | "lastOutDateStart": "出貨日期:由", | ||||
| "lastOutDateEnd": "出貨日期:至", | "lastOutDateEnd": "出貨日期:至", | ||||
| "year": "年份", | "year": "年份", | ||||
| "itemCode": "貨品編號" | |||||
| "itemCode": "貨品編號", | |||||
| "itemCodePaste": "貼上貨品編號" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-012": { | "rep-012": { | ||||
| @@ -115,7 +123,11 @@ | |||||
| "fields": { | "fields": { | ||||
| "lastInDateStart": "庫存日期:由", | "lastInDateStart": "庫存日期:由", | ||||
| "lastInDateEnd": "庫存日期:至", | "lastInDateEnd": "庫存日期:至", | ||||
| "itemCode": "貨品編號" | |||||
| "itemCode": "貨品編號", | |||||
| "itemCodePaste": "貼上貨品編號" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-007": { | "rep-007": { | ||||
| @@ -134,7 +146,11 @@ | |||||
| "fields": { | "fields": { | ||||
| "receiptDateStart": "收貨日期:由", | "receiptDateStart": "收貨日期:由", | ||||
| "receiptDateEnd": "收貨日期:至", | "receiptDateEnd": "收貨日期:至", | ||||
| "itemCode": "貨品編號" | |||||
| "itemCode": "貨品編號", | |||||
| "itemCodePaste": "貼上貨品編號" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-009": { | "rep-009": { | ||||
| @@ -173,7 +189,11 @@ | |||||
| "lastOutDateStart": "出倉日期:由", | "lastOutDateStart": "出倉日期:由", | ||||
| "lastOutDateEnd": "出倉日期:至", | "lastOutDateEnd": "出倉日期:至", | ||||
| "itemCode": "貨品編號", | "itemCode": "貨品編號", | ||||
| "itemCodePaste": "貼上貨品編號", | |||||
| "handler": "提料人" | "handler": "提料人" | ||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-006": { | "rep-006": { | ||||
| @@ -183,7 +203,11 @@ | |||||
| "lastOutDateEnd": "材料消耗日期:至", | "lastOutDateEnd": "材料消耗日期:至", | ||||
| "year": "年份", | "year": "年份", | ||||
| "stockCategory": "類別", | "stockCategory": "類別", | ||||
| "itemCode": "貨品編號" | |||||
| "itemCode": "貨品編號", | |||||
| "itemCodePaste": "貼上貨品編號" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-005": { | "rep-005": { | ||||
| @@ -193,7 +217,11 @@ | |||||
| "lastOutDateEnd": "完成生產日期:至", | "lastOutDateEnd": "完成生產日期:至", | ||||
| "year": "年份", | "year": "年份", | ||||
| "stockCategory": "類別", | "stockCategory": "類別", | ||||
| "itemCode": "貨品編號" | |||||
| "itemCode": "貨品編號", | |||||
| "itemCodePaste": "貼上貨品編號" | |||||
| }, | |||||
| "fieldHints": { | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| } | } | ||||
| }, | }, | ||||
| "rep-015": { | "rep-015": { | ||||
| @@ -202,8 +230,12 @@ | |||||
| "syncDateStart": "同步日期:由", | "syncDateStart": "同步日期:由", | ||||
| "syncDateEnd": "同步日期:至", | "syncDateEnd": "同步日期:至", | ||||
| "finishedItemCode": "成品貨號", | "finishedItemCode": "成品貨號", | ||||
| "itemCodePaste": "貼上貨品編號", | |||||
| "syncStatus": "同步狀態" | "syncStatus": "同步狀態" | ||||
| }, | }, | ||||
| "fieldHints": { | |||||
| "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" | |||||
| }, | |||||
| "options": { | "options": { | ||||
| "syncStatus": { | "syncStatus": { | ||||
| "all": "全部", | "all": "全部", | ||||