|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- "use client";
-
- import React, { useEffect, useRef, useState } from 'react';
- import { useTranslation } from "react-i18next";
- import {
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
- Button,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- Chip,
- Typography,
- } from '@mui/material';
- import DownloadIcon from '@mui/icons-material/Download';
- import dayjs from 'dayjs';
- import {
- fetchSemiFGItemCodesWithCategory,
- generateSemiFGProductionAnalysisReport,
- generateSemiFGProductionAnalysisReportExcel,
- generateSemiFGProductionAnalysisReportExcelDaily,
- ItemCodeWithCategory,
- } from './semiFGProductionAnalysisApi';
- import { parseItemCodeTokens } from './parseItemCodeTokens';
-
- type ExportFormat = 'pdf' | 'excel' | 'excel-daily';
-
- interface SemiFGProductionAnalysisReportProps {
- criteria: Record<string, string>;
- prepareCriteria?: () => Record<string, string>;
- requiredFieldLabels: string[];
- loading: boolean;
- setLoading: (loading: boolean) => void;
- reportTitle?: string;
- onExportSuccess?: (format: "pdf" | "excel") => void;
- }
-
- export default function SemiFGProductionAnalysisReport({
- criteria,
- prepareCriteria,
- requiredFieldLabels,
- loading,
- setLoading,
- reportTitle = '成品/半成品生產分析報告',
- onExportSuccess,
- }: SemiFGProductionAnalysisReportProps) {
- const { t } = useTranslation("report");
- const [showConfirmDialog, setShowConfirmDialog] = useState(false);
- const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState<ItemCodeWithCategory[]>([]);
- const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({});
- const [exportFormat, setExportFormat] = useState<ExportFormat>('pdf');
- const inFlightRef = useRef(false);
-
- // 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 dailyRangeError = (activeCriteria: Record<string, string>): string | null => {
- const start = (activeCriteria.lastOutDateStart || '').trim();
- const end = (activeCriteria.lastOutDateEnd || '').trim();
- if (!start || !end) return t('semiFgDailyRangeRequired');
- const startDate = dayjs(start);
- const endDate = dayjs(end);
- if (!startDate.isValid() || !endDate.isValid()) return t('semiFgDailyRangeRequired');
- if (endDate.isBefore(startDate, 'day')) return t('semiFgDailyRangeInvalid');
- if (endDate.diff(startDate, 'day') + 1 > 366) return t('semiFgDailyRangeTooLong');
- return null;
- };
-
- const handleExportClick = async (format: ExportFormat) => {
- if (inFlightRef.current) return;
- const activeCriteria = prepareCriteria?.() ?? criteria;
- setExportFormat(format);
- if (requiredFieldLabels.length > 0) {
- alert(t('missingRequired', { fields: requiredFieldLabels.join('\n- ') }));
- return;
- }
- if (format === 'excel-daily') {
- const rangeError = dailyRangeError(activeCriteria);
- if (rangeError) {
- alert(rangeError);
- return;
- }
- }
-
- const selectedCodes = parseItemCodeTokens(
- [activeCriteria.itemCode, activeCriteria.itemCodePaste].filter(Boolean).join(' '),
- );
- if (selectedCodes.length === 0) {
- await executeExport(format, activeCriteria);
- return;
- }
-
- const itemCodesInfo: ItemCodeWithCategory[] = selectedCodes.map((codeTrimmed) => {
- const categoryInfo = itemCodesWithCategory[codeTrimmed];
- return {
- code: codeTrimmed,
- category: categoryInfo?.category || 'Unknown',
- name: categoryInfo?.name || '',
- };
- });
- setSelectedItemCodesInfo(itemCodesInfo);
- setShowConfirmDialog(true);
- };
-
- const executeExport = async (
- format: ExportFormat = exportFormat,
- activeCriteria: Record<string, string> = prepareCriteria?.() ?? criteria,
- ) => {
- if (inFlightRef.current) return;
- inFlightRef.current = true;
- setLoading(true);
- try {
- if (format === 'excel-daily') {
- await generateSemiFGProductionAnalysisReportExcelDaily(activeCriteria, reportTitle);
- onExportSuccess?.('excel');
- } else if (format === 'excel') {
- await generateSemiFGProductionAnalysisReportExcel(activeCriteria, reportTitle);
- onExportSuccess?.('excel');
- } else {
- await generateSemiFGProductionAnalysisReport(activeCriteria, reportTitle);
- onExportSuccess?.('pdf');
- }
- setShowConfirmDialog(false);
- } catch (error) {
- console.error('Failed to generate report:', error);
- const message = error instanceof Error ? error.message.trim() : '';
- alert(message && !message.startsWith('HTTP error') ? message : t('generateError'));
- } finally {
- setLoading(false);
- inFlightRef.current = false;
- }
- };
-
- const confirmLabel = loading
- ? exportFormat === 'excel-daily'
- ? t('semiFgGeneratingDailyExcel')
- : exportFormat === 'excel'
- ? t('generatingExcel')
- : t('generatingPdf')
- : exportFormat === 'excel-daily'
- ? t('semiFgConfirmDailyExcel')
- : exportFormat === 'excel'
- ? t('confirmDownloadExcel')
- : t('confirmDownloadPdf');
-
- return (
- <>
- <div style={{ display: 'flex', gap: 16 }}>
- <Button
- variant="contained"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={() => handleExportClick('pdf')}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading && exportFormat === 'pdf' ? t('generatingPdf') : t('downloadPdf')}
- </Button>
- <Button
- variant="outlined"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={() => handleExportClick('excel')}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading && exportFormat === 'excel' ? t('generatingExcel') : t('downloadExcel')}
- </Button>
- <Button
- variant="outlined"
- color="success"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={() => handleExportClick('excel-daily')}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading && exportFormat === 'excel-daily'
- ? t('semiFgGeneratingDailyExcel')
- : t('semiFgDownloadDailyExcel')}
- </Button>
- </div>
-
- {/* Confirmation Dialog for 成品/半成品生產分析報告 */}
- <Dialog
- open={showConfirmDialog}
- onClose={() => setShowConfirmDialog(false)}
- maxWidth="md"
- fullWidth
- >
- <DialogTitle>
- <Typography variant="h6" fontWeight="bold">
- {t('semiFgConfirmTitle')}
- </Typography>
- </DialogTitle>
- <DialogContent>
- <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
- {t('semiFgConfirmHint')}
- </Typography>
- <TableContainer component={Paper} variant="outlined">
- <Table>
- <TableHead>
- <TableRow>
- <TableCell>
- <strong>{t('semiFgColItem')}</strong>
- </TableCell>
- <TableCell>
- <strong>{t('semiFgColCategory')}</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)}>{t('cancel')}</Button>
- <Button
- variant="contained"
- onClick={() => executeExport()}
- disabled={loading}
- startIcon={<DownloadIcon />}
- >
- {confirmLabel}
- </Button>
- </DialogActions>
- </Dialog>
- </>
- );
- }
|