|
- "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<string>('');
- const [criteria, setCriteria] = useState<Record<string, string>>({});
- const [loading, setLoading] = useState(false);
- const [dynamicOptions, setDynamicOptions] = useState<Record<string, { label: string; value: string }[]>>({});
- 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<HTMLInputElement>) => {
- 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<string, { code: string; category: string; name?: string }> = {};
- 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 (
- <Box sx={{ p: 4, maxWidth: 1000, margin: '0 auto' }}>
- <Typography variant="h4" gutterBottom fontWeight="bold">
- 報告管理
- </Typography>
-
- <Card sx={{ mb: 4, boxShadow: 3 }}>
- <CardContent>
- <Typography variant="h6" gutterBottom>
- 選擇報告
- </Typography>
- <TextField
- select
- fullWidth
- label="報告列表"
- value={selectedReportId}
- onChange={handleReportChange}
- helperText="選擇報告"
- >
- {REPORTS.map((report) => (
- <MenuItem key={report.id} value={report.id}>
- {report.title}
- </MenuItem>
- ))}
- </TextField>
- </CardContent>
- </Card>
-
- {currentReport && (
- <Card sx={{ boxShadow: 3, animation: 'fadeIn 0.5s' }}>
- <CardContent>
- <Typography variant="h6" color="primary" gutterBottom>
- 搜尋條件: {currentReport.title}
- </Typography>
- <Divider sx={{ mb: 3 }} />
-
- <Grid container spacing={3}>
- {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 (
- <Grid item {...gridSize} key={field.name}>
- <Autocomplete
- multiple={field.multiple || false}
- freeSolo
- options={options.map(opt => 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) => (
- <TextField
- {...params}
- fullWidth
- label={field.label}
- placeholder={field.placeholder || "選擇或輸入物料編號"}
- sx={currentReport.id === 'rep-005' ? {
- '& .MuiOutlinedInput-root': {
- minHeight: '64px',
- fontSize: '1rem'
- },
- '& .MuiInputLabel-root': {
- fontSize: '1rem'
- }
- } : {}}
- />
- )}
- 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 (
- <Chip
- variant="outlined"
- label={displayLabel}
- {...getTagProps({ index })}
- key={`${option}-${index}`}
- />
- );
- })
- }
- 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);
- }}
- />
- </Grid>
- );
- }
-
- // Regular TextField for other fields
- return (
- <Grid item {...gridSize} key={field.name}>
- <TextField
- fullWidth
- label={field.label}
- type={field.type}
- placeholder={field.placeholder}
- InputLabelProps={field.type === 'date' ? { shrink: true } : {}}
- sx={currentReport.id === 'rep-005' ? {
- '& .MuiOutlinedInput-root': {
- minHeight: '64px',
- fontSize: '1rem'
- },
- '& .MuiInputLabel-root': {
- fontSize: '1rem'
- }
- } : {}}
- onChange={(e) => {
- 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) => (
- <MenuItem key={opt.value} value={opt.value}>
- {opt.label}
- </MenuItem>
- ))}
- </TextField>
- </Grid>
- );
- })}
- </Grid>
-
- <Box sx={{ mt: 4, display: 'flex', justifyContent: 'flex-end' }}>
- {currentReport.id === 'rep-005' ? (
- <SemiFGProductionAnalysisReport
- criteria={criteria}
- requiredFieldLabels={currentReport.fields.filter(f => f.required && !criteria[f.name]).map(f => f.label)}
- loading={loading}
- setLoading={setLoading}
- reportTitle={currentReport.title}
- />
- ) : (
- <Button
- variant="contained"
- size="large"
- startIcon={<PrintIcon />}
- onClick={handlePrint}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading ? "生成報告..." : "列印報告"}
- </Button>
- )}
- </Box>
- </CardContent>
- </Card>
- )}
- </Box>
- );
- }
|