FPSMS-frontend
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SemiFGProductionAnalysisReport.tsx 8.8 KiB

​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
6 months ago
​
​
​
​
​
​
6 months ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. "use client";
  2. import React, { useEffect, useRef, useState } from 'react';
  3. import { useTranslation } from "react-i18next";
  4. import {
  5. Dialog,
  6. DialogTitle,
  7. DialogContent,
  8. DialogActions,
  9. Button,
  10. Table,
  11. TableBody,
  12. TableCell,
  13. TableContainer,
  14. TableHead,
  15. TableRow,
  16. Paper,
  17. Chip,
  18. Typography,
  19. } from '@mui/material';
  20. import DownloadIcon from '@mui/icons-material/Download';
  21. import dayjs from 'dayjs';
  22. import {
  23. fetchSemiFGItemCodesWithCategory,
  24. generateSemiFGProductionAnalysisReport,
  25. generateSemiFGProductionAnalysisReportExcel,
  26. generateSemiFGProductionAnalysisReportExcelDaily,
  27. ItemCodeWithCategory,
  28. } from './semiFGProductionAnalysisApi';
  29. import { parseItemCodeTokens } from './parseItemCodeTokens';
  30. type ExportFormat = 'pdf' | 'excel' | 'excel-daily';
  31. interface SemiFGProductionAnalysisReportProps {
  32. criteria: Record<string, string>;
  33. prepareCriteria?: () => Record<string, string>;
  34. requiredFieldLabels: string[];
  35. loading: boolean;
  36. setLoading: (loading: boolean) => void;
  37. reportTitle?: string;
  38. onExportSuccess?: (format: "pdf" | "excel") => void;
  39. }
  40. export default function SemiFGProductionAnalysisReport({
  41. criteria,
  42. prepareCriteria,
  43. requiredFieldLabels,
  44. loading,
  45. setLoading,
  46. reportTitle = '成品/半成品生產分析報告',
  47. onExportSuccess,
  48. }: SemiFGProductionAnalysisReportProps) {
  49. const { t } = useTranslation("report");
  50. const [showConfirmDialog, setShowConfirmDialog] = useState(false);
  51. const [selectedItemCodesInfo, setSelectedItemCodesInfo] = useState<ItemCodeWithCategory[]>([]);
  52. const [itemCodesWithCategory, setItemCodesWithCategory] = useState<Record<string, ItemCodeWithCategory>>({});
  53. const [exportFormat, setExportFormat] = useState<ExportFormat>('pdf');
  54. const inFlightRef = useRef(false);
  55. // Fetch item codes with category when stockCategory changes
  56. useEffect(() => {
  57. const stockCategory = criteria.stockCategory || '';
  58. if (stockCategory) {
  59. fetchSemiFGItemCodesWithCategory(stockCategory)
  60. .then((items) => {
  61. const categoryMap: Record<string, ItemCodeWithCategory> = {};
  62. items.forEach((item) => {
  63. categoryMap[item.code] = item;
  64. });
  65. setItemCodesWithCategory((prev) => ({ ...prev, ...categoryMap }));
  66. })
  67. .catch((error) => {
  68. console.error('Failed to fetch item codes with category:', error);
  69. });
  70. }
  71. }, [criteria.stockCategory]);
  72. const dailyRangeError = (activeCriteria: Record<string, string>): string | null => {
  73. const start = (activeCriteria.lastOutDateStart || '').trim();
  74. const end = (activeCriteria.lastOutDateEnd || '').trim();
  75. if (!start || !end) return t('semiFgDailyRangeRequired');
  76. const startDate = dayjs(start);
  77. const endDate = dayjs(end);
  78. if (!startDate.isValid() || !endDate.isValid()) return t('semiFgDailyRangeRequired');
  79. if (endDate.isBefore(startDate, 'day')) return t('semiFgDailyRangeInvalid');
  80. if (endDate.diff(startDate, 'day') + 1 > 366) return t('semiFgDailyRangeTooLong');
  81. return null;
  82. };
  83. const handleExportClick = async (format: ExportFormat) => {
  84. if (inFlightRef.current) return;
  85. const activeCriteria = prepareCriteria?.() ?? criteria;
  86. setExportFormat(format);
  87. if (requiredFieldLabels.length > 0) {
  88. alert(t('missingRequired', { fields: requiredFieldLabels.join('\n- ') }));
  89. return;
  90. }
  91. if (format === 'excel-daily') {
  92. const rangeError = dailyRangeError(activeCriteria);
  93. if (rangeError) {
  94. alert(rangeError);
  95. return;
  96. }
  97. }
  98. const selectedCodes = parseItemCodeTokens(
  99. [activeCriteria.itemCode, activeCriteria.itemCodePaste].filter(Boolean).join(' '),
  100. );
  101. if (selectedCodes.length === 0) {
  102. await executeExport(format, activeCriteria);
  103. return;
  104. }
  105. const itemCodesInfo: ItemCodeWithCategory[] = selectedCodes.map((codeTrimmed) => {
  106. const categoryInfo = itemCodesWithCategory[codeTrimmed];
  107. return {
  108. code: codeTrimmed,
  109. category: categoryInfo?.category || 'Unknown',
  110. name: categoryInfo?.name || '',
  111. };
  112. });
  113. setSelectedItemCodesInfo(itemCodesInfo);
  114. setShowConfirmDialog(true);
  115. };
  116. const executeExport = async (
  117. format: ExportFormat = exportFormat,
  118. activeCriteria: Record<string, string> = prepareCriteria?.() ?? criteria,
  119. ) => {
  120. if (inFlightRef.current) return;
  121. inFlightRef.current = true;
  122. setLoading(true);
  123. try {
  124. if (format === 'excel-daily') {
  125. await generateSemiFGProductionAnalysisReportExcelDaily(activeCriteria, reportTitle);
  126. onExportSuccess?.('excel');
  127. } else if (format === 'excel') {
  128. await generateSemiFGProductionAnalysisReportExcel(activeCriteria, reportTitle);
  129. onExportSuccess?.('excel');
  130. } else {
  131. await generateSemiFGProductionAnalysisReport(activeCriteria, reportTitle);
  132. onExportSuccess?.('pdf');
  133. }
  134. setShowConfirmDialog(false);
  135. } catch (error) {
  136. console.error('Failed to generate report:', error);
  137. const message = error instanceof Error ? error.message.trim() : '';
  138. alert(message && !message.startsWith('HTTP error') ? message : t('generateError'));
  139. } finally {
  140. setLoading(false);
  141. inFlightRef.current = false;
  142. }
  143. };
  144. const confirmLabel = loading
  145. ? exportFormat === 'excel-daily'
  146. ? t('semiFgGeneratingDailyExcel')
  147. : exportFormat === 'excel'
  148. ? t('generatingExcel')
  149. : t('generatingPdf')
  150. : exportFormat === 'excel-daily'
  151. ? t('semiFgConfirmDailyExcel')
  152. : exportFormat === 'excel'
  153. ? t('confirmDownloadExcel')
  154. : t('confirmDownloadPdf');
  155. return (
  156. <>
  157. <div style={{ display: 'flex', gap: 16 }}>
  158. <Button
  159. variant="contained"
  160. size="large"
  161. startIcon={<DownloadIcon />}
  162. onClick={() => handleExportClick('pdf')}
  163. disabled={loading}
  164. sx={{ px: 4 }}
  165. >
  166. {loading && exportFormat === 'pdf' ? t('generatingPdf') : t('downloadPdf')}
  167. </Button>
  168. <Button
  169. variant="outlined"
  170. size="large"
  171. startIcon={<DownloadIcon />}
  172. onClick={() => handleExportClick('excel')}
  173. disabled={loading}
  174. sx={{ px: 4 }}
  175. >
  176. {loading && exportFormat === 'excel' ? t('generatingExcel') : t('downloadExcel')}
  177. </Button>
  178. <Button
  179. variant="outlined"
  180. color="success"
  181. size="large"
  182. startIcon={<DownloadIcon />}
  183. onClick={() => handleExportClick('excel-daily')}
  184. disabled={loading}
  185. sx={{ px: 4 }}
  186. >
  187. {loading && exportFormat === 'excel-daily'
  188. ? t('semiFgGeneratingDailyExcel')
  189. : t('semiFgDownloadDailyExcel')}
  190. </Button>
  191. </div>
  192. {/* Confirmation Dialog for 成品/半成品生產分析報告 */}
  193. <Dialog
  194. open={showConfirmDialog}
  195. onClose={() => setShowConfirmDialog(false)}
  196. maxWidth="md"
  197. fullWidth
  198. >
  199. <DialogTitle>
  200. <Typography variant="h6" fontWeight="bold">
  201. {t('semiFgConfirmTitle')}
  202. </Typography>
  203. </DialogTitle>
  204. <DialogContent>
  205. <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
  206. {t('semiFgConfirmHint')}
  207. </Typography>
  208. <TableContainer component={Paper} variant="outlined">
  209. <Table>
  210. <TableHead>
  211. <TableRow>
  212. <TableCell>
  213. <strong>{t('semiFgColItem')}</strong>
  214. </TableCell>
  215. <TableCell>
  216. <strong>{t('semiFgColCategory')}</strong>
  217. </TableCell>
  218. </TableRow>
  219. </TableHead>
  220. <TableBody>
  221. {selectedItemCodesInfo.map((item, index) => {
  222. const displayName = item.name ? `${item.code} ${item.name}` : item.code;
  223. return (
  224. <TableRow key={index}>
  225. <TableCell>{displayName}</TableCell>
  226. <TableCell>
  227. <Chip
  228. label={item.category || 'Unknown'}
  229. color={item.category === 'FG' ? 'primary' : item.category === 'WIP' ? 'secondary' : 'default'}
  230. size="small"
  231. />
  232. </TableCell>
  233. </TableRow>
  234. );
  235. })}
  236. </TableBody>
  237. </Table>
  238. </TableContainer>
  239. </DialogContent>
  240. <DialogActions sx={{ p: 2 }}>
  241. <Button onClick={() => setShowConfirmDialog(false)}>{t('cancel')}</Button>
  242. <Button
  243. variant="contained"
  244. onClick={() => executeExport()}
  245. disabled={loading}
  246. startIcon={<DownloadIcon />}
  247. >
  248. {confirmLabel}
  249. </Button>
  250. </DialogActions>
  251. </Dialog>
  252. </>
  253. );
  254. }