|
|
|
@@ -26,7 +26,7 @@ import { |
|
|
|
import DownloadIcon from '@mui/icons-material/Download'; |
|
|
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; |
|
|
|
import { REPORTS } from '@/config/reportConfig'; |
|
|
|
import { mergePastedItemCodes, buildItemCodePasteRequestBody } from './parseItemCodeTokens'; |
|
|
|
import { mergePastedItemCodes, buildItemCodePasteRequestBody, parseItemCodeTokens } from './parseItemCodeTokens'; |
|
|
|
import { NEXT_PUBLIC_API_URL } from '@/config/api'; |
|
|
|
import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; |
|
|
|
import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; |
|
|
|
@@ -86,8 +86,8 @@ const ITEM_CODE_PASTE_POST_REPORTS = new Set([ |
|
|
|
|
|
|
|
/** 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. 69 | v1.0.2 | 2026-09-10 */ |
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.0 | 2026-09-10 */ |
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 69 | v1.0.3 | 2026-09-28 */ |
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.1 | 2026-09-28 */ |
|
|
|
export default function ReportPage() { |
|
|
|
const { data: session } = useSession() as { data: SessionWithTokens | null }; |
|
|
|
const { t, i18n, reportTitle, fieldLabel, fieldHint, optionLabel } = useReportLabels(); |
|
|
|
@@ -98,6 +98,37 @@ export default function ReportPage() { |
|
|
|
|
|
|
|
const [selectedReportId, setSelectedReportId] = useState<string>(''); |
|
|
|
const [criteria, setCriteria] = useState<Record<string, string>>({}); |
|
|
|
const criteriaRef = useRef<Record<string, string>>({}); |
|
|
|
const pendingItemCodeInputRef = useRef<Record<string, string>>({}); |
|
|
|
const updateCriteria = ( |
|
|
|
updater: Record<string, string> | ((prev: Record<string, string>) => Record<string, string>), |
|
|
|
) => { |
|
|
|
const next = typeof updater === 'function' ? updater(criteriaRef.current) : updater; |
|
|
|
criteriaRef.current = next; |
|
|
|
setCriteria(next); |
|
|
|
}; |
|
|
|
/** Typed item codes are not in criteria until a dropdown option is chosen. Merge them in before the request. */ |
|
|
|
const flushPendingItemCodeInputs = (): Record<string, string> => { |
|
|
|
const pendingEntries = Object.entries(pendingItemCodeInputRef.current).filter(([, raw]) => raw.trim()); |
|
|
|
if (pendingEntries.length === 0) return criteriaRef.current; |
|
|
|
|
|
|
|
const next = { ...criteriaRef.current }; |
|
|
|
let changed = false; |
|
|
|
for (const [field, raw] of pendingEntries) { |
|
|
|
pendingItemCodeInputRef.current[field] = ''; |
|
|
|
const merged = parseItemCodeTokens([next[field], raw].filter(Boolean).join(' ')); |
|
|
|
const joined = merged.join(','); |
|
|
|
if (joined && joined !== (next[field] || '')) { |
|
|
|
next[field] = joined; |
|
|
|
changed = true; |
|
|
|
} |
|
|
|
} |
|
|
|
if (changed) { |
|
|
|
criteriaRef.current = next; |
|
|
|
setCriteria(next); |
|
|
|
} |
|
|
|
return criteriaRef.current; |
|
|
|
}; |
|
|
|
const [loading, setLoading] = useState(false); |
|
|
|
const excelInFlightRef = useRef(false); |
|
|
|
const [dynamicOptions, setDynamicOptions] = useState<Record<string, { label: string; value: string }[]>>({}); |
|
|
|
@@ -124,14 +155,15 @@ export default function ReportPage() { |
|
|
|
if (reportId === selectedReportId) return; |
|
|
|
setSelectedReportId(reportId); |
|
|
|
setFieldErrors({}); |
|
|
|
pendingItemCodeInputRef.current = {}; |
|
|
|
if (reportId === 'rep-010') { |
|
|
|
setCriteria({ qcType: 'all', qcItemScope: 'all' }); |
|
|
|
updateCriteria({ qcType: 'all', qcItemScope: 'all' }); |
|
|
|
} else if (reportId === 'rep-004') { |
|
|
|
setCriteria({ storeId: 'All', poPrefix: 'All' }); |
|
|
|
updateCriteria({ storeId: 'All', poPrefix: 'All' }); |
|
|
|
} else if (reportId === 'rep-021') { |
|
|
|
setCriteria({ storeId: 'All', stockTakeSectionDescription: 'All', lotOrigin: 'All' }); |
|
|
|
updateCriteria({ storeId: 'All', stockTakeSectionDescription: 'All', lotOrigin: 'All' }); |
|
|
|
} else { |
|
|
|
setCriteria({}); |
|
|
|
updateCriteria({}); |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
@@ -143,7 +175,7 @@ export default function ReportPage() { |
|
|
|
delete next[name]; |
|
|
|
return next; |
|
|
|
}); |
|
|
|
setCriteria((prev) => { |
|
|
|
updateCriteria((prev) => { |
|
|
|
const next = { ...prev, [name]: stringValue }; |
|
|
|
if (currentReport?.id === 'rep-021' && name === 'warehouse') { |
|
|
|
const m = stringValue.trim().match(/^w(\d)/i); |
|
|
|
@@ -252,7 +284,7 @@ export default function ReportPage() { |
|
|
|
|
|
|
|
// Default "All" (no filter) for stock take variance report conditions. |
|
|
|
if (selectedReportId === 'rep-012') { |
|
|
|
setCriteria({ |
|
|
|
updateCriteria({ |
|
|
|
store_id: 'All', |
|
|
|
status: 'All', |
|
|
|
type: 'All', |
|
|
|
@@ -264,7 +296,7 @@ export default function ReportPage() { |
|
|
|
useEffect(() => { |
|
|
|
if (selectedReportId !== 'rep-012' || !rep012MultiRound) return; |
|
|
|
if (criteria.status === 'completed') return; |
|
|
|
setCriteria((prev) => ({ ...prev, status: 'completed' })); |
|
|
|
updateCriteria((prev) => ({ ...prev, status: 'completed' })); |
|
|
|
}, [selectedReportId, rep012MultiRound, criteria.status]); |
|
|
|
|
|
|
|
// React 18 Strict Mode (dev) mounts → unmounts → remounts, so effects with [] run twice. |
|
|
|
@@ -329,19 +361,20 @@ export default function ReportPage() { |
|
|
|
|
|
|
|
/** rep-012:單輪送 status;多輪送 stockTakeRoundId 清單且 status=completed */ |
|
|
|
const buildRep012QueryString = (): string => { |
|
|
|
const active = criteriaRef.current; |
|
|
|
const p = new URLSearchParams(); |
|
|
|
p.set('stockTakeRoundId', rep012RoundIds.join(',')); |
|
|
|
const code = criteria.itemCode?.trim(); |
|
|
|
const code = active.itemCode?.trim(); |
|
|
|
if (code) p.set('itemCode', code); |
|
|
|
const store = criteria.store_id?.trim(); |
|
|
|
const store = active.store_id?.trim(); |
|
|
|
if (store && store !== 'All') p.set('store_id', store); |
|
|
|
if (rep012MultiRound) { |
|
|
|
p.set('status', 'completed'); |
|
|
|
} else { |
|
|
|
const status = criteria.status?.trim(); |
|
|
|
const status = active.status?.trim(); |
|
|
|
if (status && status !== 'All') p.set('status', status); |
|
|
|
} |
|
|
|
const lotType = criteria.type?.trim(); |
|
|
|
const lotType = active.type?.trim(); |
|
|
|
if (lotType && lotType !== 'All') p.set('type', lotType); |
|
|
|
return p.toString(); |
|
|
|
}; |
|
|
|
@@ -349,15 +382,16 @@ export default function ReportPage() { |
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 64 | v1.0.0 | 2026-08-11 */ |
|
|
|
/** rep-010:qcItemScope → includeMeasurable / includeOther;qcType=all 不傳篩選 */ |
|
|
|
const buildRep010QueryString = (): string => { |
|
|
|
const active = criteriaRef.current; |
|
|
|
const p = new URLSearchParams(); |
|
|
|
Object.entries(criteria).forEach(([key, value]) => { |
|
|
|
Object.entries(active).forEach(([key, value]) => { |
|
|
|
if (key === 'qcItemScope') return; |
|
|
|
if (key === 'qcType' && String(value).trim().toLowerCase() === 'all') return; |
|
|
|
if (value != null && String(value).trim() !== '') { |
|
|
|
p.set(key, String(value)); |
|
|
|
} |
|
|
|
}); |
|
|
|
const scope = (criteria.qcItemScope || 'all').trim().toLowerCase(); |
|
|
|
const scope = (active.qcItemScope || 'all').trim().toLowerCase(); |
|
|
|
if (scope === 'measurable') { |
|
|
|
p.set('includeMeasurable', 'true'); |
|
|
|
p.set('includeOther', 'false'); |
|
|
|
@@ -380,7 +414,7 @@ export default function ReportPage() { |
|
|
|
if (!currentReport) return ''; |
|
|
|
if (currentReport.id === 'rep-012') return buildRep012QueryString(); |
|
|
|
if (currentReport.id === 'rep-010') return buildRep010QueryString(); |
|
|
|
const merged = mergePastedItemCodes(criteria, itemCodeFieldName()); |
|
|
|
const merged = mergePastedItemCodes(criteriaRef.current, itemCodeFieldName()); |
|
|
|
const p = new URLSearchParams(merged); |
|
|
|
if (currentReport.id === 'rep-016') { |
|
|
|
const day = (merged.dateStart || '').trim(); |
|
|
|
@@ -393,6 +427,7 @@ export default function ReportPage() { |
|
|
|
}; |
|
|
|
|
|
|
|
const handlePrint = async () => { |
|
|
|
flushPendingItemCodeInputs(); |
|
|
|
if (!currentReport) return; |
|
|
|
if (!validateRequiredFields()) return; |
|
|
|
|
|
|
|
@@ -409,6 +444,7 @@ export default function ReportPage() { |
|
|
|
}; |
|
|
|
|
|
|
|
const handleExcelPrint = async () => { |
|
|
|
flushPendingItemCodeInputs(); |
|
|
|
if (!currentReport) return; |
|
|
|
if (!validateRequiredFields()) return; |
|
|
|
await executeExcelReport(); |
|
|
|
@@ -420,17 +456,18 @@ export default function ReportPage() { |
|
|
|
excelInFlightRef.current = true; |
|
|
|
setLoading(true); |
|
|
|
try { |
|
|
|
const activeCriteria = criteriaRef.current; |
|
|
|
if (currentReport.id === 'rep-014') { |
|
|
|
await generateGrnReportExcel( |
|
|
|
criteria, |
|
|
|
activeCriteria, |
|
|
|
reportTitle(currentReport), |
|
|
|
includeGrnFinancialColumns, |
|
|
|
t, |
|
|
|
); |
|
|
|
} else if (currentReport.id === 'rep-015') { |
|
|
|
await generateBomShopSyncReportExcel(criteria, reportTitle(currentReport), t); |
|
|
|
await generateBomShopSyncReportExcel(activeCriteria, reportTitle(currentReport), t); |
|
|
|
} else if (currentReport.id === 'rep-017') { |
|
|
|
await generateShopOrderReplenishmentReportExcel(criteria, reportTitle(currentReport), t); |
|
|
|
await generateShopOrderReplenishmentReportExcel(activeCriteria, reportTitle(currentReport), t); |
|
|
|
} else { |
|
|
|
// Backend returns actual .xlsx bytes for this Excel endpoint. |
|
|
|
const usesItemCodePastePost = isItemCodePastePostReport(); |
|
|
|
@@ -445,7 +482,7 @@ export default function ReportPage() { |
|
|
|
Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', |
|
|
|
'Content-Type': 'application/json', |
|
|
|
}, |
|
|
|
body: JSON.stringify(buildItemCodePasteRequestBody(criteria, itemCodeFieldName())), |
|
|
|
body: JSON.stringify(buildItemCodePasteRequestBody(criteriaRef.current, itemCodeFieldName())), |
|
|
|
} |
|
|
|
: { |
|
|
|
method: 'GET', |
|
|
|
@@ -514,7 +551,7 @@ export default function ReportPage() { |
|
|
|
Accept: 'application/pdf', |
|
|
|
'Content-Type': 'application/json', |
|
|
|
}, |
|
|
|
body: JSON.stringify(buildItemCodePasteRequestBody(criteria, itemCodeFieldName())), |
|
|
|
body: JSON.stringify(buildItemCodePasteRequestBody(criteriaRef.current, itemCodeFieldName())), |
|
|
|
} |
|
|
|
: { |
|
|
|
method: 'GET', |
|
|
|
@@ -692,7 +729,13 @@ export default function ReportPage() { |
|
|
|
label={translatedLabel} |
|
|
|
placeholder={field.placeholder || "e.g. FA0591"} |
|
|
|
value={selectedCodes} |
|
|
|
onChange={(codes) => handleFieldChange(field.name, codes)} |
|
|
|
onInputValueChange={(text) => { |
|
|
|
pendingItemCodeInputRef.current[field.name] = text; |
|
|
|
}} |
|
|
|
onChange={(codes) => { |
|
|
|
pendingItemCodeInputRef.current[field.name] = ""; |
|
|
|
handleFieldChange(field.name, codes); |
|
|
|
}} |
|
|
|
minChars={field.asyncSearchMinChars ?? 2} |
|
|
|
disabled={disabledByCheckedCheckbox || disabledRep012Status || !!field.disabled} |
|
|
|
/> |
|
|
|
@@ -711,8 +754,20 @@ export default function ReportPage() { |
|
|
|
<Autocomplete |
|
|
|
multiple={field.multiple || false} |
|
|
|
freeSolo |
|
|
|
filterSelectedOptions |
|
|
|
options={options.map(opt => opt.value)} |
|
|
|
value={autocompleteValue} |
|
|
|
isOptionEqualToValue={(option, selected) => |
|
|
|
String(option).toUpperCase() === String(selected).toUpperCase() |
|
|
|
} |
|
|
|
onInputChange={(_, input, reason) => { |
|
|
|
if (reason === "reset" || reason === "clear") { |
|
|
|
pendingItemCodeInputRef.current[field.name] = ""; |
|
|
|
return; |
|
|
|
} |
|
|
|
if (reason === "blur") return; |
|
|
|
pendingItemCodeInputRef.current[field.name] = input; |
|
|
|
}} |
|
|
|
onChange={(event, newValue, reason) => { |
|
|
|
if (field.multiple) { |
|
|
|
// Handle multiple selection - newValue is an array |
|
|
|
@@ -895,6 +950,7 @@ export default function ReportPage() { |
|
|
|
{currentReport.id === 'rep-005' ? ( |
|
|
|
<SemiFGProductionAnalysisReport |
|
|
|
criteria={criteria} |
|
|
|
prepareCriteria={flushPendingItemCodeInputs} |
|
|
|
requiredFieldLabels={currentReport.fields.filter(f => f.required && !criteria[f.name]).map(f => fieldLabel(currentReport.id, f))} |
|
|
|
loading={loading} |
|
|
|
setLoading={setLoading} |
|
|
|
|