From 329ccc22bdb477930f07f248db58aa4b50037f49 Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Fri, 6 Feb 2026 20:10:38 +0800 Subject: [PATCH] FG/SemiFG Production Analysis Report Update --- .../report/SemiFGProductionAnalysisReport.tsx | 181 +++++++++++++++ src/app/(main)/report/page.tsx | 208 ++++++------------ .../report/semiFGProductionAnalysisApi.ts | 116 ++++++++++ 3 files changed, 360 insertions(+), 145 deletions(-) create mode 100644 src/app/(main)/report/SemiFGProductionAnalysisReport.tsx create mode 100644 src/app/(main)/report/semiFGProductionAnalysisApi.ts diff --git a/src/app/(main)/report/SemiFGProductionAnalysisReport.tsx b/src/app/(main)/report/SemiFGProductionAnalysisReport.tsx new file mode 100644 index 0000000..ca839dd --- /dev/null +++ b/src/app/(main)/report/SemiFGProductionAnalysisReport.tsx @@ -0,0 +1,181 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Chip, + Typography, +} from '@mui/material'; +import PrintIcon from '@mui/icons-material/Print'; +import { + fetchSemiFGItemCodes, + fetchSemiFGItemCodesWithCategory, + generateSemiFGProductionAnalysisReport, + ItemCodeWithCategory, +} from './semiFGProductionAnalysisApi'; + +interface SemiFGProductionAnalysisReportProps { + criteria: Record; + requiredFieldLabels: string[]; + loading: boolean; + setLoading: (loading: boolean) => void; + reportTitle?: string; +} + +export default function SemiFGProductionAnalysisReport({ + criteria, + requiredFieldLabels, + loading, + setLoading, + reportTitle = '成品/半成品生產分析報告', +}: SemiFGProductionAnalysisReportProps) { + const [showConfirmDialog, setShowConfirmDialog] = useState(false); + const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState([]); + const [itemCodesWithCategory, setItemCodesWithCategory] = useState>({}); + + // Fetch item codes with category when stockCategory changes + useEffect(() => { + const stockCategory = criteria.stockCategory || ''; + if (stockCategory) { + fetchSemiFGItemCodesWithCategory(stockCategory) + .then((items) => { + const categoryMap: Record = {}; + items.forEach((item) => { + categoryMap[item.code] = item; + }); + setItemCodesWithCategory((prev) => ({ ...prev, ...categoryMap })); + }) + .catch((error) => { + console.error('Failed to fetch item codes with category:', error); + }); + } + }, [criteria.stockCategory]); + + const handlePrintClick = async () => { + // Validate required fields + if (requiredFieldLabels.length > 0) { + alert(`缺少必填條件:\n- ${requiredFieldLabels.join('\n- ')}`); + return; + } + + // If no itemCode is selected, print directly without confirmation + if (!criteria.itemCode) { + await executePrint(); + return; + } + + // If itemCode is selected, show confirmation dialog + 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); + }; + + const executePrint = async () => { + setLoading(true); + try { + await generateSemiFGProductionAnalysisReport(criteria, reportTitle); + 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 ( + <> + + + {/* Confirmation Dialog for 成品/半成品生產分析報告 */} + setShowConfirmDialog(false)} + maxWidth="md" + fullWidth + > + + + 已選擇的物料編號以及列印成品/半成品生產分析報告 + + + + + 請確認以下已選擇的物料編號及其類別: + + + + + + + 物料編號及名稱 + + + 類別 + + + + + {selectedItemCodesInfo.map((item, index) => { + const displayName = item.name ? `${item.code} ${item.name}` : item.code; + return ( + + {displayName} + + + + + ); + })} + +
+
+
+ + + + +
+ + ); +} diff --git a/src/app/(main)/report/page.tsx b/src/app/(main)/report/page.tsx index 3259c74..18774af 100644 --- a/src/app/(main)/report/page.tsx +++ b/src/app/(main)/report/page.tsx @@ -11,30 +11,17 @@ import { Button, Grid, Divider, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Paper, Chip, Autocomplete } from '@mui/material'; import PrintIcon from '@mui/icons-material/Print'; import { REPORTS, ReportDefinition } from '@/config/reportConfig'; -import { getSession } from "next-auth/react"; 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 { code: string; @@ -46,9 +33,6 @@ export default function ReportPage() { const [criteria, setCriteria] = useState>({}); const [loading, setLoading] = useState(false); const [dynamicOptions, setDynamicOptions] = useState>({}); - const [itemCodesWithCategory, setItemCodesWithCategory] = useState>({}); - const [showConfirmDialog, setShowConfirmDialog] = useState(false); - const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState([]); // Find the configuration for the currently selected report const currentReport = useMemo(() => @@ -77,6 +61,36 @@ export default function ReportPage() { 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; + }); + + // 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"); // 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}`); - 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 = {}; - 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 })); - - // Do NOT clear itemCode when stockCategory changes - preserve user's selection } catch (error) { console.error("Failed to fetch dynamic options:", error); setDynamicOptions((prev) => ({ ...prev, [field.name]: [] })); @@ -170,25 +151,11 @@ export default function ReportPage() { 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 () => { @@ -457,79 +424,30 @@ export default function ReportPage() { - + {currentReport.id === 'rep-005' ? ( + f.required && !criteria[f.name]).map(f => f.label)} + loading={loading} + setLoading={setLoading} + reportTitle={currentReport.title} + /> + ) : ( + + )} )} - - {/* Confirmation Dialog for 成品/半成品生產分析報告 */} - setShowConfirmDialog(false)} - maxWidth="md" - fullWidth - > - - - 已選擇的物料編號以及列印成品/半成品生產分析報告 - - - - - 請確認以下已選擇的物料編號及其類別: - - - - - - 物料編號及名稱 - 類別 - - - - {selectedItemCodesInfo.map((item, index) => { - const displayName = item.name ? `${item.code} ${item.name}` : item.code; - return ( - - {displayName} - - - - - ); - })} - -
-
-
- - - - -
); } \ No newline at end of file diff --git a/src/app/(main)/report/semiFGProductionAnalysisApi.ts b/src/app/(main)/report/semiFGProductionAnalysisApi.ts new file mode 100644 index 0000000..04cfc8a --- /dev/null +++ b/src/app/(main)/report/semiFGProductionAnalysisApi.ts @@ -0,0 +1,116 @@ +import { NEXT_PUBLIC_API_URL } from '@/config/api'; + +export interface ItemCodeWithName { + code: string; + name: string; +} + +export interface ItemCodeWithCategory { + code: string; + category: string; + name?: string; +} + +/** + * Fetch item codes for SemiFG Production Analysis Report + * @param stockCategory - Comma-separated stock categories (e.g., "FG,WIP") or empty string for all + * @returns Array of item codes with names + */ +export const fetchSemiFGItemCodes = async ( + stockCategory: string = '' +): Promise => { + const token = localStorage.getItem("accessToken"); + + let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`; + if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { + url = `${url}?stockCategory=${stockCategory}`; + } + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); +}; + +/** + * Fetch item codes with category information for SemiFG Production Analysis Report + * @param stockCategory - Comma-separated stock categories (e.g., "FG,WIP") or empty string for all + * @returns Array of item codes with category and name + */ +export const fetchSemiFGItemCodesWithCategory = async ( + stockCategory: string = '' +): Promise => { + const token = localStorage.getItem("accessToken"); + + let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes-with-category`; + if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { + url = `${url}?stockCategory=${stockCategory}`; + } + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + return await response.json(); +}; + +/** + * Generate and download the SemiFG Production Analysis Report PDF + * @param criteria - Report criteria parameters + * @param reportTitle - Title of the report for filename + * @returns Promise that resolves when download is complete + */ +export const generateSemiFGProductionAnalysisReport = async ( + criteria: Record, + reportTitle: string = '成品/半成品生產分析報告' +): Promise => { + const token = localStorage.getItem("accessToken"); + const queryParams = new URLSearchParams(criteria).toString(); + const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis?${queryParams}`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/pdf', + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + 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 = `${reportTitle}.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); +};