|
|
@@ -11,30 +11,17 @@ import { |
|
|
Button, |
|
|
Button, |
|
|
Grid, |
|
|
Grid, |
|
|
Divider, |
|
|
Divider, |
|
|
Dialog, |
|
|
|
|
|
DialogTitle, |
|
|
|
|
|
DialogContent, |
|
|
|
|
|
DialogActions, |
|
|
|
|
|
Table, |
|
|
|
|
|
TableBody, |
|
|
|
|
|
TableCell, |
|
|
|
|
|
TableContainer, |
|
|
|
|
|
TableHead, |
|
|
|
|
|
TableRow, |
|
|
|
|
|
Paper, |
|
|
|
|
|
Chip, |
|
|
Chip, |
|
|
Autocomplete |
|
|
Autocomplete |
|
|
} from '@mui/material'; |
|
|
} from '@mui/material'; |
|
|
import PrintIcon from '@mui/icons-material/Print'; |
|
|
import PrintIcon from '@mui/icons-material/Print'; |
|
|
import { REPORTS, ReportDefinition } from '@/config/reportConfig'; |
|
|
import { REPORTS, ReportDefinition } from '@/config/reportConfig'; |
|
|
import { getSession } from "next-auth/react"; |
|
|
|
|
|
import { NEXT_PUBLIC_API_URL } from '@/config/api'; |
|
|
import { NEXT_PUBLIC_API_URL } from '@/config/api'; |
|
|
|
|
|
|
|
|
interface ItemCodeWithCategory { |
|
|
|
|
|
code: string; |
|
|
|
|
|
category: string; |
|
|
|
|
|
name?: string; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; |
|
|
|
|
|
import { |
|
|
|
|
|
fetchSemiFGItemCodes, |
|
|
|
|
|
fetchSemiFGItemCodesWithCategory |
|
|
|
|
|
} from './semiFGProductionAnalysisApi'; |
|
|
|
|
|
|
|
|
interface ItemCodeWithName { |
|
|
interface ItemCodeWithName { |
|
|
code: string; |
|
|
code: string; |
|
|
@@ -46,9 +33,6 @@ export default function ReportPage() { |
|
|
const [criteria, setCriteria] = useState<Record<string, string>>({}); |
|
|
const [criteria, setCriteria] = useState<Record<string, string>>({}); |
|
|
const [loading, setLoading] = useState(false); |
|
|
const [loading, setLoading] = useState(false); |
|
|
const [dynamicOptions, setDynamicOptions] = useState<Record<string, { label: string; value: string }[]>>({}); |
|
|
const [dynamicOptions, setDynamicOptions] = useState<Record<string, { label: string; value: string }[]>>({}); |
|
|
const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({}); |
|
|
|
|
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false); |
|
|
|
|
|
const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState<ItemCodeWithCategory[]>([]); |
|
|
|
|
|
|
|
|
|
|
|
// Find the configuration for the currently selected report |
|
|
// Find the configuration for the currently selected report |
|
|
const currentReport = useMemo(() => |
|
|
const currentReport = useMemo(() => |
|
|
@@ -77,6 +61,36 @@ export default function ReportPage() { |
|
|
if (!field.dynamicOptionsEndpoint) return; |
|
|
if (!field.dynamicOptionsEndpoint) return; |
|
|
|
|
|
|
|
|
try { |
|
|
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; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Create options with code and name format: "PP1162 瑞士汁(1磅/包)" |
|
|
|
|
|
const options = itemCodesWithName.map(item => { |
|
|
|
|
|
const code = item.code; |
|
|
|
|
|
const name = item.name || ''; |
|
|
|
|
|
const category = categoryMap[code]?.category || ''; |
|
|
|
|
|
|
|
|
|
|
|
// Format: "PP1162 瑞士汁(1磅/包)" or "PP1162 瑞士汁(1磅/包) (FG)" |
|
|
|
|
|
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 |
|
|
const token = localStorage.getItem("accessToken"); |
|
|
const token = localStorage.getItem("accessToken"); |
|
|
|
|
|
|
|
|
// Handle multiple stockCategory values (comma-separated) |
|
|
// Handle multiple stockCategory values (comma-separated) |
|
|
@@ -98,45 +112,12 @@ export default function ReportPage() { |
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); |
|
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); |
|
|
|
|
|
|
|
|
const itemCodesWithName: ItemCodeWithName[] = await response.json(); |
|
|
|
|
|
|
|
|
|
|
|
// Fetch item codes with category to show labels |
|
|
|
|
|
const categoryUrl = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes-with-category${paramValue && paramValue !== 'All' && !paramValue.includes('All') ? `?stockCategory=${paramValue}` : ''}`; |
|
|
|
|
|
const categoryResponse = await fetch(categoryUrl, { |
|
|
|
|
|
method: 'GET', |
|
|
|
|
|
headers: { |
|
|
|
|
|
'Authorization': `Bearer ${token}`, |
|
|
|
|
|
'Content-Type': 'application/json', |
|
|
|
|
|
}, |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
let categoryMap: Record<string, ItemCodeWithCategory> = {}; |
|
|
|
|
|
if (categoryResponse.ok) { |
|
|
|
|
|
const itemsWithCategory: ItemCodeWithCategory[] = await categoryResponse.json(); |
|
|
|
|
|
itemsWithCategory.forEach(item => { |
|
|
|
|
|
categoryMap[item.code] = item; |
|
|
|
|
|
}); |
|
|
|
|
|
setItemCodesWithCategory((prev) => ({ ...prev, ...categoryMap })); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Create options with code and name format: "PP1162 瑞士汁(1磅/包)" |
|
|
|
|
|
const options = itemCodesWithName.map(item => { |
|
|
|
|
|
const code = item.code; |
|
|
|
|
|
const name = item.name || ''; |
|
|
|
|
|
const category = categoryMap[code]?.category || ''; |
|
|
|
|
|
|
|
|
|
|
|
// Format: "PP1162 瑞士汁(1磅/包)" or "PP1162 瑞士汁(1磅/包) (FG)" |
|
|
|
|
|
let label = name ? `${code} ${name}` : code; |
|
|
|
|
|
if (category) { |
|
|
|
|
|
label = `${label} (${category})`; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return { label, value: code }; |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
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 })); |
|
|
setDynamicOptions((prev) => ({ ...prev, [field.name]: options })); |
|
|
|
|
|
|
|
|
// Do NOT clear itemCode when stockCategory changes - preserve user's selection |
|
|
|
|
|
} catch (error) { |
|
|
} catch (error) { |
|
|
console.error("Failed to fetch dynamic options:", error); |
|
|
console.error("Failed to fetch dynamic options:", error); |
|
|
setDynamicOptions((prev) => ({ ...prev, [field.name]: [] })); |
|
|
setDynamicOptions((prev) => ({ ...prev, [field.name]: [] })); |
|
|
@@ -170,25 +151,11 @@ export default function ReportPage() { |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (currentReport.id === 'rep-005' && criteria.itemCode) { |
|
|
|
|
|
const selectedCodes = criteria.itemCode.split(',').filter(code => code.trim()); |
|
|
|
|
|
const itemCodesInfo: ItemCodeWithCategory[] = selectedCodes.map(code => { |
|
|
|
|
|
const codeTrimmed = code.trim(); |
|
|
|
|
|
const categoryInfo = itemCodesWithCategory[codeTrimmed]; |
|
|
|
|
|
return { |
|
|
|
|
|
code: codeTrimmed, |
|
|
|
|
|
category: categoryInfo?.category || 'Unknown', |
|
|
|
|
|
name: categoryInfo?.name || '' |
|
|
|
|
|
}; |
|
|
|
|
|
}); |
|
|
|
|
|
setSelectedItemCodesInfo(itemCodesInfo); |
|
|
|
|
|
setShowConfirmDialog(true); |
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// Direct print for other reports |
|
|
|
|
|
await executePrint(); |
|
|
|
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
const executePrint = async () => { |
|
|
const executePrint = async () => { |
|
|
@@ -457,79 +424,30 @@ export default function ReportPage() { |
|
|
</Grid> |
|
|
</Grid> |
|
|
|
|
|
|
|
|
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'flex-end' }}> |
|
|
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'flex-end' }}> |
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
size="large" |
|
|
|
|
|
startIcon={<PrintIcon />} |
|
|
|
|
|
onClick={handlePrint} |
|
|
|
|
|
disabled={loading} |
|
|
|
|
|
sx={{ px: 4 }} |
|
|
|
|
|
> |
|
|
|
|
|
{loading ? "生成報告..." : "列印報告"} |
|
|
|
|
|
</Button> |
|
|
|
|
|
|
|
|
{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> |
|
|
</Box> |
|
|
</CardContent> |
|
|
</CardContent> |
|
|
</Card> |
|
|
</Card> |
|
|
)} |
|
|
)} |
|
|
|
|
|
|
|
|
{/* Confirmation Dialog for 成品/半成品生產分析報告 */} |
|
|
|
|
|
<Dialog |
|
|
|
|
|
open={showConfirmDialog} |
|
|
|
|
|
onClose={() => setShowConfirmDialog(false)} |
|
|
|
|
|
maxWidth="md" |
|
|
|
|
|
fullWidth |
|
|
|
|
|
> |
|
|
|
|
|
<DialogTitle> |
|
|
|
|
|
<Typography variant="h6" fontWeight="bold"> |
|
|
|
|
|
已選擇的物料編號以及列印成品/半成品生產分析報告 |
|
|
|
|
|
</Typography> |
|
|
|
|
|
</DialogTitle> |
|
|
|
|
|
<DialogContent> |
|
|
|
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> |
|
|
|
|
|
請確認以下已選擇的物料編號及其類別: |
|
|
|
|
|
</Typography> |
|
|
|
|
|
<TableContainer component={Paper} variant="outlined"> |
|
|
|
|
|
<Table> |
|
|
|
|
|
<TableHead> |
|
|
|
|
|
<TableRow> |
|
|
|
|
|
<TableCell><strong>物料編號及名稱</strong></TableCell> |
|
|
|
|
|
<TableCell><strong>類別</strong></TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
</TableHead> |
|
|
|
|
|
<TableBody> |
|
|
|
|
|
{selectedItemCodesInfo.map((item, index) => { |
|
|
|
|
|
const displayName = item.name ? `${item.code} ${item.name}` : item.code; |
|
|
|
|
|
return ( |
|
|
|
|
|
<TableRow key={index}> |
|
|
|
|
|
<TableCell>{displayName}</TableCell> |
|
|
|
|
|
<TableCell> |
|
|
|
|
|
<Chip |
|
|
|
|
|
label={item.category || 'Unknown'} |
|
|
|
|
|
color={item.category === 'FG' ? 'primary' : item.category === 'WIP' ? 'secondary' : 'default'} |
|
|
|
|
|
size="small" |
|
|
|
|
|
/> |
|
|
|
|
|
</TableCell> |
|
|
|
|
|
</TableRow> |
|
|
|
|
|
); |
|
|
|
|
|
})} |
|
|
|
|
|
</TableBody> |
|
|
|
|
|
</Table> |
|
|
|
|
|
</TableContainer> |
|
|
|
|
|
</DialogContent> |
|
|
|
|
|
<DialogActions sx={{ p: 2 }}> |
|
|
|
|
|
<Button onClick={() => setShowConfirmDialog(false)}> |
|
|
|
|
|
取消 |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
variant="contained" |
|
|
|
|
|
onClick={executePrint} |
|
|
|
|
|
disabled={loading} |
|
|
|
|
|
startIcon={<PrintIcon />} |
|
|
|
|
|
> |
|
|
|
|
|
{loading ? "生成報告..." : "確認列印報告"} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</DialogActions> |
|
|
|
|
|
</Dialog> |
|
|
|
|
|
</Box> |
|
|
</Box> |
|
|
); |
|
|
); |
|
|
} |
|
|
} |