From 6a28a8e1e9abb70e3efa225ba658999e1abbf013 Mon Sep 17 00:00:00 2001 From: "PC-20260115JRSN\\Administrator" Date: Fri, 18 Sep 2026 00:33:23 +0800 Subject: [PATCH] added search criteria for pasting item code for stock balance report --- src/app/(main)/report/page.tsx | 100 ++++++++++++------- src/app/(main)/report/parseItemCodeTokens.ts | 47 +++++++++ src/app/(main)/report/reportI18n.ts | 8 +- src/config/reportConfig.ts | 12 +++ src/i18n/en/report.json | 6 +- src/i18n/zh/report.json | 6 +- 6 files changed, 138 insertions(+), 41 deletions(-) create mode 100644 src/app/(main)/report/parseItemCodeTokens.ts diff --git a/src/app/(main)/report/page.tsx b/src/app/(main)/report/page.tsx index 6dec1c10..dce43d73 100644 --- a/src/app/(main)/report/page.tsx +++ b/src/app/(main)/report/page.tsx @@ -26,6 +26,7 @@ import { import DownloadIcon from '@mui/icons-material/Download'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import { REPORTS } from '@/config/reportConfig'; +import { mergePastedItemCodes, buildStockBalanceRequestBody } from './parseItemCodeTokens'; import { NEXT_PUBLIC_API_URL } from '@/config/api'; import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; @@ -80,7 +81,7 @@ const FIELD_ERROR_SX = { /** FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.0 | 2026-09-10 */ export default function ReportPage() { const { data: session } = useSession() as { data: SessionWithTokens | null }; - const { t, i18n, reportTitle, fieldLabel, optionLabel } = useReportLabels(); + const { t, i18n, reportTitle, fieldLabel, fieldHint, optionLabel } = useReportLabels(); const isZh = (i18n.language || 'zh').startsWith('zh'); const dateDisplayFormat = isZh ? 'DD/MM/YYYY' : 'DD/MM/YYYY'; const includeGrnFinancialColumns = @@ -358,6 +359,22 @@ export default function ReportPage() { return p.toString(); }; + const buildCriteriaQueryString = (): string => { + if (!currentReport) return ''; + if (currentReport.id === 'rep-012') return buildRep012QueryString(); + if (currentReport.id === 'rep-010') return buildRep010QueryString(); + const merged = mergePastedItemCodes(criteria); + const p = new URLSearchParams(merged); + if (currentReport.id === 'rep-016') { + const day = (merged.dateStart || '').trim(); + if (day) { + p.set('dateStart', day); + p.set('dateEnd', day); + } + } + return p.toString(); + }; + const handlePrint = async () => { if (!currentReport) return; if (!validateRequiredFields()) return; @@ -399,28 +416,24 @@ export default function ReportPage() { await generateShopOrderReplenishmentReportExcel(criteria, reportTitle(currentReport), t); } else { // Backend returns actual .xlsx bytes for this Excel endpoint. - let queryParams = - currentReport.id === 'rep-012' - ? buildRep012QueryString() - : currentReport.id === 'rep-010' - ? buildRep010QueryString() - : new URLSearchParams(criteria).toString(); - // rep-016: single-day UI — mirror dateStart to dateEnd for backend API. - if (currentReport.id === 'rep-016') { - const p = new URLSearchParams(criteria); - const day = (criteria.dateStart || '').trim(); - if (day) { - p.set('dateStart', day); - p.set('dateEnd', day); - } - queryParams = p.toString(); - } - const excelUrl = `${currentReport.apiEndpoint}-excel?${queryParams}`; - - const response = await clientAuthFetch(excelUrl, { - method: 'GET', - headers: { Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, - }); + const isStockBalance = currentReport.id === 'rep-007'; + const excelUrl = isStockBalance + ? `${currentReport.apiEndpoint}-excel` + : `${currentReport.apiEndpoint}-excel?${buildCriteriaQueryString()}`; + + const response = await clientAuthFetch(excelUrl, isStockBalance + ? { + method: 'POST', + headers: { + Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(buildStockBalanceRequestBody(criteria)), + } + : { + method: 'GET', + headers: { Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, + }); if (response.status === 401 || response.status === 403) return; if (response.status === 204) { @@ -472,18 +485,24 @@ export default function ReportPage() { setLoading(true); try { - let queryParams = - currentReport.id === 'rep-012' - ? buildRep012QueryString() - : currentReport.id === 'rep-010' - ? buildRep010QueryString() - : new URLSearchParams(criteria).toString(); - const url = `${currentReport.apiEndpoint}?${queryParams}`; - - const response = await clientAuthFetch(url, { - method: 'GET', - headers: { 'Accept': 'application/pdf' }, - }); + const isStockBalance = currentReport.id === 'rep-007'; + const url = isStockBalance + ? currentReport.apiEndpoint + : `${currentReport.apiEndpoint}?${buildCriteriaQueryString()}`; + + const response = await clientAuthFetch(url, isStockBalance + ? { + method: 'POST', + headers: { + Accept: 'application/pdf', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(buildStockBalanceRequestBody(criteria)), + } + : { + method: 'GET', + headers: { Accept: 'application/pdf' }, + }); if (response.status === 401 || response.status === 403) return; if (!response.ok) { @@ -574,7 +593,10 @@ export default function ReportPage() { : currentValue; // Use larger grid size for 成品/半成品生產分析報告 - const gridSize = currentReport.id === 'rep-005' ? { xs: 12, sm: 12, md: 6 } : { xs: 12, sm: 6 }; + const gridSize = field.multiline + ? { xs: 12 } + : currentReport.id === 'rep-005' ? { xs: 12, sm: 12, md: 6 } : { xs: 12, sm: 6 }; + const hintText = fieldHint(currentReport.id, field.name); const disabledByCheckedCheckbox = currentReport.fields.some((f) => { if (f.type !== 'checkbox' || criteria[f.name] !== 'true') return false; @@ -762,9 +784,11 @@ export default function ReportPage() { fullWidth required={field.required} error={Boolean(fieldErrors[field.name])} - helperText={fieldErrors[field.name] || undefined} + helperText={fieldErrors[field.name] || hintText || undefined} label={translatedLabel} - type={field.type} + type={field.multiline ? 'text' : field.type} + multiline={field.multiline} + minRows={field.multiline ? (field.minRows ?? 4) : undefined} placeholder={field.placeholder} disabled={disabledByCheckedCheckbox || disabledRep012Status || !!field.disabled} sx={{ diff --git a/src/app/(main)/report/parseItemCodeTokens.ts b/src/app/(main)/report/parseItemCodeTokens.ts new file mode 100644 index 00000000..148a08e2 --- /dev/null +++ b/src/app/(main)/report/parseItemCodeTokens.ts @@ -0,0 +1,47 @@ +/** Split pasted / typed item codes from Excel or a text field. */ +const ITEM_CODE_SEPARATORS = /[\s,;,、\u00A0\u3000]+/; + +export function parseItemCodeTokens(raw: string | undefined | null): string[] { + if (!raw) return []; + const seen = new Set(); + const tokens: string[] = []; + for (const part of raw.split(ITEM_CODE_SEPARATORS)) { + const token = part.trim(); + if (!token) continue; + const key = token.toUpperCase(); + if (seen.has(key)) continue; + seen.add(key); + tokens.push(token); + } + return tokens; +} + +/** Merge multi-select `itemCode` with pasted `itemCodePaste`; drop the paste field from API params. */ +export function mergePastedItemCodes(criteria: Record): Record { + const next: Record = { ...criteria }; + delete next.itemCodePaste; + const merged = parseItemCodeTokens( + [criteria.itemCode, criteria.itemCodePaste].filter(Boolean).join(" "), + ); + if (merged.length > 0) { + next.itemCode = merged.join(","); + } else { + delete next.itemCode; + } + return next; +} + +/** POST body for 庫存結餘報告 so pasted codes are not limited by URL length. */ +export function buildStockBalanceRequestBody(criteria: Record): { + stockDate?: string; + itemCodes?: string[]; +} { + const itemCodes = parseItemCodeTokens( + [criteria.itemCode, 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; + return body; +} diff --git a/src/app/(main)/report/reportI18n.ts b/src/app/(main)/report/reportI18n.ts index 7e8be629..7031a3af 100644 --- a/src/app/(main)/report/reportI18n.ts +++ b/src/app/(main)/report/reportI18n.ts @@ -19,6 +19,12 @@ export function useReportLabels() { defaultValue: field.label, }); + const fieldHint = (reportId: string, fieldName: string, fallback = "") => { + const key = `reports.${reportId}.fieldHints.${fieldName}`; + if (!i18n.exists(key, { ns: "report" })) return fallback; + return String(t(key, { defaultValue: fallback })); + }; + const optionLabel = ( reportId: string, fieldName: string, @@ -35,7 +41,7 @@ export function useReportLabels() { const categoryTitle = (id: string, fallback: string) => t(`categories.${id}`, { defaultValue: fallback }); - return { t, i18n, reportTitle, fieldLabel, optionLabel, categoryTitle }; + return { t, i18n, reportTitle, fieldLabel, fieldHint, optionLabel, categoryTitle }; } export function reportExcelT( diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts index c6ed8b5b..f34bcd93 100644 --- a/src/config/reportConfig.ts +++ b/src/config/reportConfig.ts @@ -25,6 +25,10 @@ export interface ReportField { minDate?: 'today'; /** Disable the input (e.g. date locked to today) */ disabled?: boolean; + /** Render a multiline text area (for pasting many values) */ + multiline?: boolean; + /** Rows for multiline text areas. Default 4. */ + minRows?: number; } export type ReportResponseType = 'pdf' | 'excel'; @@ -340,6 +344,14 @@ export const REPORTS: ReportDefinition[] = [ fields: [ { label: "庫存日期: Stock Date", name: "stockDate", type: "date", required: true }, asyncItemCodeField(), + { + label: "貼上貨品編號 Paste Item Codes", + name: "itemCodePaste", + type: "text", + required: false, + multiline: true, + minRows: 4, + }, ] }, diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json index 17af3c26..eb761d48 100644 --- a/src/i18n/en/report.json +++ b/src/i18n/en/report.json @@ -122,7 +122,11 @@ "title": "Stock Balance Report", "fields": { "stockDate": "Stock Date", - "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-014": { diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json index 52a2a638..c0de47f6 100644 --- a/src/i18n/zh/report.json +++ b/src/i18n/zh/report.json @@ -122,7 +122,11 @@ "title": "庫存結餘報告", "fields": { "stockDate": "庫存日期", - "itemCode": "貨品編號" + "itemCode": "貨品編號", + "itemCodePaste": "貼上貨品編號" + }, + "fieldHints": { + "itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔" } }, "rep-014": {