Selaa lähdekoodia

added search criteria for pasting item code for stock balance report

production
PC-20260115JRSN\Administrator 2 päivää sitten
vanhempi
commit
6a28a8e1e9
6 muutettua tiedostoa jossa 138 lisäystä ja 41 poistoa
  1. +62
    -38
      src/app/(main)/report/page.tsx
  2. +47
    -0
      src/app/(main)/report/parseItemCodeTokens.ts
  3. +7
    -1
      src/app/(main)/report/reportI18n.ts
  4. +12
    -0
      src/config/reportConfig.ts
  5. +5
    -1
      src/i18n/en/report.json
  6. +5
    -1
      src/i18n/zh/report.json

+ 62
- 38
src/app/(main)/report/page.tsx Näytä tiedosto

@@ -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={{


+ 47
- 0
src/app/(main)/report/parseItemCodeTokens.ts Näytä tiedosto

@@ -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<string>();
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<string, string>): Record<string, string> {
const next: Record<string, string> = { ...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<string, string>): {
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;
}

+ 7
- 1
src/app/(main)/report/reportI18n.ts Näytä tiedosto

@@ -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(


+ 12
- 0
src/config/reportConfig.ts Näytä tiedosto

@@ -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,
},
]
},


+ 5
- 1
src/i18n/en/report.json Näytä tiedosto

@@ -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": {


+ 5
- 1
src/i18n/zh/report.json Näytä tiedosto

@@ -122,7 +122,11 @@
"title": "庫存結餘報告",
"fields": {
"stockDate": "庫存日期",
"itemCode": "貨品編號"
"itemCode": "貨品編號",
"itemCodePaste": "貼上貨品編號"
},
"fieldHints": {
"itemCodePaste": "可從 Excel 貼上多個貨品編號,以空格、逗號或換行分隔"
}
},
"rep-014": {


Ladataan…
Peruuta
Tallenna