"use client"; import React, { useState, useMemo, useEffect } from 'react'; import { Box, Card, CardContent, Typography, MenuItem, TextField, Button, Grid, Divider, Chip, Autocomplete } from '@mui/material'; 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, fetchSemiFGItemCodesWithCategory } from './semiFGProductionAnalysisApi'; interface ItemCodeWithName { code: string; name: string; } export default function ReportPage() { const [selectedReportId, setSelectedReportId] = useState(''); const [criteria, setCriteria] = useState>({}); const [loading, setLoading] = useState(false); const [dynamicOptions, setDynamicOptions] = useState>({}); const [showConfirmDialog, setShowConfirmDialog] = useState(false); // Find the configuration for the currently selected report const currentReport = useMemo(() => REPORTS.find((r) => r.id === selectedReportId), [selectedReportId]); const handleReportChange = (event: React.ChangeEvent) => { setSelectedReportId(event.target.value); setCriteria({}); // Clear criteria when switching reports }; const handleFieldChange = (name: string, value: string | string[]) => { const stringValue = Array.isArray(value) ? value.join(',') : value; setCriteria((prev) => ({ ...prev, [name]: stringValue })); // If this is stockCategory and there's a field that depends on it, fetch dynamic options if (name === 'stockCategory' && currentReport) { const itemCodeField = currentReport.fields.find(f => f.name === 'itemCode' && f.dynamicOptions); if (itemCodeField && itemCodeField.dynamicOptionsEndpoint) { fetchDynamicOptions(itemCodeField, stringValue); } } }; const fetchDynamicOptions = async (field: any, paramValue: string) => { if (!field.dynamicOptionsEndpoint) return; try { // Use API service for SemiFG Production Analysis Report (rep-005) if (currentReport?.id === 'rep-005' && field.name === 'itemCode') { const itemCodesWithName = await fetchSemiFGItemCodes(paramValue); const itemsWithCategory = await fetchSemiFGItemCodesWithCategory(paramValue); const categoryMap: Record = {}; itemsWithCategory.forEach(item => { categoryMap[item.code] = item; }); const options = itemCodesWithName.map(item => { const code = item.code; const name = item.name || ''; const category = categoryMap[code]?.category || ''; let label = name ? `${code} ${name}` : code; if (category) { label = `${label} (${category})`; } return { label, value: code }; }); setDynamicOptions((prev) => ({ ...prev, [field.name]: options })); return; } // Handle other reports with dynamic options let url = field.dynamicOptionsEndpoint; if (paramValue && paramValue !== 'All' && !paramValue.includes('All')) { url = `${field.dynamicOptionsEndpoint}?${field.dynamicOptionsParam}=${paramValue}`; } const response = await clientAuthFetch(url, { method: 'GET', 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(); const options = Array.isArray(data) ? data.map((item: any) => ({ label: item.label || item.name || item.code || String(item), value: item.value || item.code || String(item) })) : []; setDynamicOptions((prev) => ({ ...prev, [field.name]: options })); } catch (error) { console.error("Failed to fetch dynamic options:", error); setDynamicOptions((prev) => ({ ...prev, [field.name]: [] })); } }; // Load initial options when report is selected useEffect(() => { if (currentReport) { currentReport.fields.forEach(field => { if (field.dynamicOptions && field.dynamicOptionsEndpoint) { // Load all options initially fetchDynamicOptions(field, ''); } }); } // Clear dynamic options when report changes setDynamicOptions({}); }, [selectedReportId]); const handlePrint = async () => { if (!currentReport) return; // 1. Mandatory Field Validation const missingFields = currentReport.fields .filter(field => field.required && !criteria[field.name]) .map(field => field.label); if (missingFields.length > 0) { alert(`缺少必填條件:\n- ${missingFields.join('\n- ')}`); return; } // For rep-005, the print logic is handled by SemiFGProductionAnalysisReport component // For other reports, execute print directly if (currentReport.id !== 'rep-005') { await executePrint(); } }; const executePrint = async () => { if (!currentReport) return; setLoading(true); try { const queryParams = new URLSearchParams(criteria).toString(); const url = `${currentReport.apiEndpoint}?${queryParams}`; const response = await clientAuthFetch(url, { method: 'GET', 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); throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`); } const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = downloadUrl; const contentDisposition = response.headers.get('Content-Disposition'); let fileName = `${currentReport.title}.pdf`; if (contentDisposition?.includes('filename=')) { fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); } link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(downloadUrl); setShowConfirmDialog(false); } catch (error) { console.error("Failed to generate report:", error); alert("An error occurred while generating the report. Please try again."); } finally { setLoading(false); } }; return ( 報告管理 選擇報告 {REPORTS.map((report) => ( {report.title} ))} {currentReport && ( 搜尋條件: {currentReport.title} {currentReport.fields.map((field) => { const options = field.dynamicOptions ? (dynamicOptions[field.name] || []) : (field.options || []); const currentValue = criteria[field.name] || ''; const valueForSelect = field.multiple ? (currentValue ? currentValue.split(',').map(v => v.trim()).filter(v => v) : []) : currentValue; // Use larger grid size for 成品/半成品生產分析報告 const gridSize = currentReport.id === 'rep-005' ? { xs: 12, sm: 12, md: 6 } : { xs: 12, sm: 6 }; // Use Autocomplete for fields that allow input if (field.type === 'select' && field.allowInput) { const autocompleteValue = field.multiple ? (Array.isArray(valueForSelect) ? valueForSelect : []) : (valueForSelect || null); return ( opt.value)} value={autocompleteValue} onChange={(event, newValue, reason) => { if (field.multiple) { // Handle multiple selection - newValue is an array let values: string[] = []; if (Array.isArray(newValue)) { values = newValue .map(v => typeof v === 'string' ? v.trim() : String(v).trim()) .filter(v => v !== ''); } handleFieldChange(field.name, values); } else { // Handle single selection - newValue can be string or null const value = typeof newValue === 'string' ? newValue.trim() : (newValue || ''); handleFieldChange(field.name, value); } }} onKeyDown={(event) => { // Allow Enter key to add custom value in multiple mode if (field.multiple && event.key === 'Enter') { const target = event.target as HTMLInputElement; if (target && target.value && target.value.trim()) { const currentValues = Array.isArray(autocompleteValue) ? autocompleteValue : []; const newValue = target.value.trim(); if (!currentValues.includes(newValue)) { handleFieldChange(field.name, [...currentValues, newValue]); // Clear the input setTimeout(() => { if (target) target.value = ''; }, 0); } } } }} renderInput={(params) => ( )} renderTags={(value, getTagProps) => value.map((option, index) => { // Find the label for the option if it exists in options const optionObj = options.find(opt => opt.value === option); const displayLabel = optionObj ? optionObj.label : String(option); return ( ); }) } getOptionLabel={(option) => { // Find the label for the option if it exists in options const optionObj = options.find(opt => opt.value === option); return optionObj ? optionObj.label : String(option); }} /> ); } // Regular TextField for other fields return ( { if (field.multiple) { const value = typeof e.target.value === 'string' ? e.target.value.split(',') : e.target.value; // Special handling for stockCategory if (field.name === 'stockCategory' && Array.isArray(value)) { const currentValues = (criteria[field.name] || '').split(',').map(v => v.trim()).filter(v => v); const newValues = value.map(v => String(v).trim()).filter(v => v); const wasOnlyAll = currentValues.length === 1 && currentValues[0] === 'All'; const hasAll = newValues.includes('All'); const hasOthers = newValues.some(v => v !== 'All'); if (hasAll && hasOthers) { // User selected "All" along with other options // If previously only "All" was selected, user is trying to switch - remove "All" and keep others if (wasOnlyAll) { const filteredValue = newValues.filter(v => v !== 'All'); handleFieldChange(field.name, filteredValue); } else { // User added "All" to existing selections - keep only "All" handleFieldChange(field.name, ['All']); } } else if (hasAll && !hasOthers) { // Only "All" is selected handleFieldChange(field.name, ['All']); } else if (!hasAll && hasOthers) { // Other options selected without "All" handleFieldChange(field.name, newValues); } else { // Empty selection handleFieldChange(field.name, []); } } else { handleFieldChange(field.name, value); } } else { handleFieldChange(field.name, e.target.value); } }} value={valueForSelect} select={field.type === 'select'} SelectProps={field.multiple ? { multiple: true, renderValue: (selected: any) => { if (Array.isArray(selected)) { return selected.join(', '); } return selected; } } : {}} > {field.type === 'select' && options.map((opt) => ( {opt.label} ))} ); })} {currentReport.id === 'rep-005' ? ( f.required && !criteria[f.name]).map(f => f.label)} loading={loading} setLoading={setLoading} reportTitle={currentReport.title} /> ) : ( )} )} ); }