Преглед изворни кода

FG/SemiFG Production Analysis Report Update

MergeProblem1^2
B.E.N.S.O.N пре 23 часа
родитељ
комит
329ccc22bd
3 измењених фајлова са 360 додато и 145 уклоњено
  1. +181
    -0
      src/app/(main)/report/SemiFGProductionAnalysisReport.tsx
  2. +63
    -145
      src/app/(main)/report/page.tsx
  3. +116
    -0
      src/app/(main)/report/semiFGProductionAnalysisApi.ts

+ 181
- 0
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<string, string>;
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<ItemCodeWithCategory[]>([]);
const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({});

// Fetch item codes with category when stockCategory changes
useEffect(() => {
const stockCategory = criteria.stockCategory || '';
if (stockCategory) {
fetchSemiFGItemCodesWithCategory(stockCategory)
.then((items) => {
const categoryMap: Record<string, ItemCodeWithCategory> = {};
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 (
<>
<Button
variant="contained"
size="large"
startIcon={<PrintIcon />}
onClick={handlePrintClick}
disabled={loading}
sx={{ px: 4 }}
>
{loading ? '生成報告...' : '列印報告'}
</Button>

{/* 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>
</>
);
}

+ 63
- 145
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<Record<string, string>>({});
const [loading, setLoading] = useState(false);
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
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<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");
// 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<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 }));
// 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() {
</Grid>

<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>
</CardContent>
</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>
);
}

+ 116
- 0
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<ItemCodeWithName[]> => {
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<ItemCodeWithCategory[]> => {
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<string, string>,
reportTitle: string = '成品/半成品生產分析報告'
): Promise<void> => {
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);
};

Loading…
Откажи
Сачувај