FPSMS-frontend
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 

725 строки
28 KiB

  1. "use client";
  2. import React, { useState, useMemo, useEffect } from 'react';
  3. import { useSession } from "next-auth/react";
  4. import { SessionWithTokens } from "@/config/authConfig";
  5. import { AUTH } from "@/authorities";
  6. import {
  7. Box,
  8. Card,
  9. CardContent,
  10. Typography,
  11. MenuItem,
  12. TextField,
  13. Button,
  14. Grid,
  15. Divider,
  16. Chip,
  17. Autocomplete,
  18. Checkbox,
  19. FormControlLabel,
  20. } from '@mui/material';
  21. import DownloadIcon from '@mui/icons-material/Download';
  22. import { REPORTS, ReportDefinition } from '@/config/reportConfig';
  23. import { NEXT_PUBLIC_API_URL } from '@/config/api';
  24. import { clientAuthFetch } from '@/app/utils/clientAuthFetch';
  25. import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport';
  26. import ReportSelectionDashboard from './ReportSelectionDashboard';
  27. import {
  28. fetchSemiFGItemCodes,
  29. fetchSemiFGItemCodesWithCategory
  30. } from './semiFGProductionAnalysisApi';
  31. import { generateGrnReportExcel } from './grnReportApi';
  32. import { generateBomShopSyncReportExcel } from './bomShopSyncReportApi';
  33. import {
  34. FEATURE_USAGE,
  35. FEATURE_USAGE_ACTION,
  36. logFeatureUsage,
  37. } from '@/lib/featureUsageLog';
  38. interface ItemCodeWithName {
  39. code: string;
  40. name: string;
  41. }
  42. export default function ReportPage() {
  43. const { data: session } = useSession() as { data: SessionWithTokens | null };
  44. const includeGrnFinancialColumns =
  45. session?.abilities?.includes(AUTH.ADMIN) ?? false;
  46. const [selectedReportId, setSelectedReportId] = useState<string>('');
  47. const [criteria, setCriteria] = useState<Record<string, string>>({});
  48. const [loading, setLoading] = useState(false);
  49. const [dynamicOptions, setDynamicOptions] = useState<Record<string, { label: string; value: string }[]>>({});
  50. const [showConfirmDialog, setShowConfirmDialog] = useState(false);
  51. // Find the configuration for the currently selected report
  52. const rep012RoundIds = useMemo(() => {
  53. if (selectedReportId !== 'rep-012') return [] as string[];
  54. return (criteria.stockTakeRoundId || '')
  55. .split(',')
  56. .map((s) => s.trim())
  57. .filter(Boolean);
  58. }, [selectedReportId, criteria.stockTakeRoundId]);
  59. const rep012MultiRound = rep012RoundIds.length > 1;
  60. const currentReport = useMemo(() =>
  61. REPORTS.find((r) => r.id === selectedReportId),
  62. [selectedReportId]);
  63. const handleSelectReport = (reportId: string) => {
  64. if (reportId === selectedReportId) return;
  65. setSelectedReportId(reportId);
  66. setCriteria({});
  67. };
  68. const handleFieldChange = (name: string, value: string | string[]) => {
  69. const stringValue = Array.isArray(value) ? value.join(',') : value;
  70. setCriteria((prev) => ({ ...prev, [name]: stringValue }));
  71. // If this is stockCategory and there's a field that depends on it, fetch dynamic options
  72. if (name === 'stockCategory' && currentReport) {
  73. const itemCodeField = currentReport.fields.find(f => f.name === 'itemCode' && f.dynamicOptions);
  74. if (itemCodeField && itemCodeField.dynamicOptionsEndpoint) {
  75. fetchDynamicOptions(itemCodeField, stringValue);
  76. }
  77. }
  78. };
  79. const fetchDynamicOptions = async (field: any, paramValue: string) => {
  80. if (!field.dynamicOptionsEndpoint) return;
  81. try {
  82. // Use API service for SemiFG Production Analysis Report (rep-005)
  83. if (currentReport?.id === 'rep-005' && field.name === 'itemCode') {
  84. const itemCodesWithName = await fetchSemiFGItemCodes(paramValue);
  85. const itemsWithCategory = await fetchSemiFGItemCodesWithCategory(paramValue);
  86. const categoryMap: Record<string, { code: string; category: string; name?: string }> = {};
  87. itemsWithCategory.forEach(item => {
  88. categoryMap[item.code] = item;
  89. });
  90. const options = itemCodesWithName.map(item => {
  91. const code = item.code;
  92. const name = item.name || '';
  93. const category = categoryMap[code]?.category || '';
  94. let label = name ? `${code} ${name}` : code;
  95. if (category) {
  96. label = `${label} (${category})`;
  97. }
  98. return { label, value: code };
  99. });
  100. setDynamicOptions((prev) => ({ ...prev, [field.name]: options }));
  101. return;
  102. }
  103. // Handle other reports with dynamic options
  104. let url = field.dynamicOptionsEndpoint;
  105. if (paramValue && paramValue !== 'All' && !paramValue.includes('All')) {
  106. url = `${field.dynamicOptionsEndpoint}?${field.dynamicOptionsParam}=${paramValue}`;
  107. }
  108. const response = await clientAuthFetch(url, {
  109. method: 'GET',
  110. headers: { 'Content-Type': 'application/json' },
  111. });
  112. if (response.status === 401 || response.status === 403) return;
  113. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  114. const data = await response.json();
  115. const options = Array.isArray(data)
  116. ? data.map((item: any) => ({ label: item.label || item.name || item.code || String(item), value: item.value || item.code || String(item) }))
  117. : [];
  118. setDynamicOptions((prev) => ({ ...prev, [field.name]: options }));
  119. } catch (error) {
  120. console.error("Failed to fetch dynamic options:", error);
  121. setDynamicOptions((prev) => ({ ...prev, [field.name]: [] }));
  122. }
  123. };
  124. // Load initial options when report is selected
  125. useEffect(() => {
  126. if (currentReport) {
  127. currentReport.fields.forEach(field => {
  128. if (field.dynamicOptions && field.dynamicOptionsEndpoint) {
  129. // Load all options initially
  130. fetchDynamicOptions(field, '');
  131. }
  132. });
  133. }
  134. // Clear dynamic options when report changes
  135. setDynamicOptions({});
  136. // Default "All" (no filter) for stock take variance report conditions.
  137. if (selectedReportId === 'rep-012') {
  138. setCriteria({
  139. store_id: 'All',
  140. status: 'All',
  141. });
  142. }
  143. }, [selectedReportId]);
  144. /** rep-012:多選輪次時狀態固定為已審核 */
  145. useEffect(() => {
  146. if (selectedReportId !== 'rep-012' || !rep012MultiRound) return;
  147. if (criteria.status === 'completed') return;
  148. setCriteria((prev) => ({ ...prev, status: 'completed' }));
  149. }, [selectedReportId, rep012MultiRound, criteria.status]);
  150. // React 18 Strict Mode (dev) mounts → unmounts → remounts, so effects with [] run twice.
  151. // Dedupe PAGE_VIEW within a short window so 進入頁面次數 is +1 per real visit.
  152. useEffect(() => {
  153. if (typeof window === "undefined") return;
  154. const w = window as Window & { __fpsmsReportPageViewLoggedAt?: number };
  155. const now = Date.now();
  156. if (w.__fpsmsReportPageViewLoggedAt != null && now - w.__fpsmsReportPageViewLoggedAt < 2000) {
  157. return;
  158. }
  159. w.__fpsmsReportPageViewLoggedAt = now;
  160. logFeatureUsage(FEATURE_USAGE.REPORT_MANAGEMENT, FEATURE_USAGE_ACTION.PAGE_VIEW);
  161. }, []);
  162. const validateRequiredFields = () => {
  163. if (!currentReport) return true;
  164. if (currentReport.id === 'rep-012') {
  165. if (rep012RoundIds.length === 0) {
  166. alert('缺少必填條件:\n- 盤點輪次');
  167. return false;
  168. }
  169. return true;
  170. }
  171. // Mandatory Field Validation
  172. const missingFields = currentReport.fields
  173. .filter((field) => {
  174. if (!field.required) return false;
  175. return !criteria[field.name];
  176. })
  177. .map(field => field.label);
  178. if (missingFields.length > 0) {
  179. alert(`缺少必填條件:\n- ${missingFields.join('\n- ')}`);
  180. return false;
  181. }
  182. return true;
  183. };
  184. /** rep-012:單輪送 status;多輪送 stockTakeRoundId 清單且 status=completed */
  185. const buildRep012QueryString = (): string => {
  186. const p = new URLSearchParams();
  187. p.set('stockTakeRoundId', rep012RoundIds.join(','));
  188. const code = criteria.itemCode?.trim();
  189. if (code) p.set('itemCode', code);
  190. const store = criteria.store_id?.trim();
  191. if (store && store !== 'All') p.set('store_id', store);
  192. if (rep012MultiRound) {
  193. p.set('status', 'completed');
  194. } else {
  195. const status = criteria.status?.trim();
  196. if (status && status !== 'All') p.set('status', status);
  197. }
  198. return p.toString();
  199. };
  200. const handlePrint = async () => {
  201. if (!currentReport) return;
  202. if (!validateRequiredFields()) return;
  203. // For rep-005, the print logic is handled by SemiFGProductionAnalysisReport component
  204. if (currentReport.id === 'rep-005') return;
  205. // For Excel reports (e.g. GRN), fetch JSON and download as .xlsx
  206. if (currentReport.responseType === 'excel') {
  207. await executeExcelReport();
  208. return;
  209. }
  210. await executePrint();
  211. };
  212. const handleExcelPrint = async () => {
  213. if (!currentReport) return;
  214. if (!validateRequiredFields()) return;
  215. await executeExcelReport();
  216. };
  217. const executeExcelReport = async () => {
  218. if (!currentReport) return;
  219. setLoading(true);
  220. try {
  221. if (currentReport.id === 'rep-014') {
  222. await generateGrnReportExcel(
  223. criteria,
  224. currentReport.title,
  225. includeGrnFinancialColumns
  226. );
  227. } else if (currentReport.id === 'rep-015') {
  228. await generateBomShopSyncReportExcel(criteria, currentReport.title);
  229. } else {
  230. // Backend returns actual .xlsx bytes for this Excel endpoint.
  231. const queryParams =
  232. currentReport.id === 'rep-012'
  233. ? buildRep012QueryString()
  234. : new URLSearchParams(criteria).toString();
  235. const excelUrl = `${currentReport.apiEndpoint}-excel?${queryParams}`;
  236. const response = await clientAuthFetch(excelUrl, {
  237. method: 'GET',
  238. headers: { Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
  239. });
  240. if (response.status === 401 || response.status === 403) return;
  241. if (!response.ok) {
  242. const errorText = await response.text();
  243. console.error("Response error:", errorText);
  244. throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
  245. }
  246. const blob = await response.blob();
  247. const downloadUrl = window.URL.createObjectURL(blob);
  248. const link = document.createElement('a');
  249. link.href = downloadUrl;
  250. const contentDisposition = response.headers.get('Content-Disposition');
  251. let fileName = `${currentReport.title}.xlsx`;
  252. if (contentDisposition?.includes('filename=')) {
  253. fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, '');
  254. }
  255. link.setAttribute('download', fileName);
  256. document.body.appendChild(link);
  257. link.click();
  258. link.remove();
  259. window.URL.revokeObjectURL(downloadUrl);
  260. }
  261. if (currentReport) {
  262. logFeatureUsage(
  263. FEATURE_USAGE.REPORT_MANAGEMENT,
  264. FEATURE_USAGE_ACTION.DOWNLOAD,
  265. `${currentReport.id}:excel`,
  266. );
  267. }
  268. setShowConfirmDialog(false);
  269. } catch (error) {
  270. console.error("Failed to generate Excel report:", error);
  271. alert("An error occurred while generating the report. Please try again.");
  272. } finally {
  273. setLoading(false);
  274. }
  275. };
  276. const executePrint = async () => {
  277. if (!currentReport) return;
  278. setLoading(true);
  279. try {
  280. const queryParams =
  281. currentReport.id === 'rep-012'
  282. ? buildRep012QueryString()
  283. : new URLSearchParams(criteria).toString();
  284. const url = `${currentReport.apiEndpoint}?${queryParams}`;
  285. const response = await clientAuthFetch(url, {
  286. method: 'GET',
  287. headers: { 'Accept': 'application/pdf' },
  288. });
  289. if (response.status === 401 || response.status === 403) return;
  290. if (!response.ok) {
  291. const errorText = await response.text();
  292. console.error("Response error:", errorText);
  293. throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
  294. }
  295. const blob = await response.blob();
  296. const downloadUrl = window.URL.createObjectURL(blob);
  297. const link = document.createElement('a');
  298. link.href = downloadUrl;
  299. const contentDisposition = response.headers.get('Content-Disposition');
  300. let fileName = `${currentReport.title}.pdf`;
  301. if (contentDisposition?.includes('filename=')) {
  302. fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, '');
  303. }
  304. link.setAttribute('download', fileName);
  305. document.body.appendChild(link);
  306. link.click();
  307. link.remove();
  308. window.URL.revokeObjectURL(downloadUrl);
  309. logFeatureUsage(
  310. FEATURE_USAGE.REPORT_MANAGEMENT,
  311. FEATURE_USAGE_ACTION.DOWNLOAD,
  312. `${currentReport.id}:pdf`,
  313. );
  314. setShowConfirmDialog(false);
  315. } catch (error) {
  316. console.error("Failed to generate report:", error);
  317. alert("An error occurred while generating the report. Please try again.");
  318. } finally {
  319. setLoading(false);
  320. }
  321. };
  322. return (
  323. <Box sx={{ p: 4, maxWidth: 1280, margin: '0 auto' }}>
  324. <Typography variant="h4" gutterBottom fontWeight="bold">
  325. 報告管理
  326. </Typography>
  327. <ReportSelectionDashboard
  328. selectedReportId={selectedReportId}
  329. onSelectReport={handleSelectReport}
  330. />
  331. {currentReport && (
  332. <Card sx={{ boxShadow: 3, animation: 'fadeIn 0.5s' }}>
  333. <CardContent>
  334. <Typography variant="h6" color="primary" gutterBottom>
  335. 搜索條件: {currentReport.title}
  336. </Typography>
  337. <Divider sx={{ mb: 3 }} />
  338. <Grid container spacing={3}>
  339. {currentReport.fields.map((field) => {
  340. const options = field.dynamicOptions
  341. ? (dynamicOptions[field.name] || [])
  342. : (field.options || []);
  343. const currentValue = criteria[field.name] || '';
  344. const valueForSelect = field.multiple
  345. ? (currentValue ? currentValue.split(',').map(v => v.trim()).filter(v => v) : [])
  346. : currentValue;
  347. // Use larger grid size for 成品/半成品生產分析報告
  348. const gridSize = currentReport.id === 'rep-005' ? { xs: 12, sm: 12, md: 6 } : { xs: 12, sm: 6 };
  349. const disabledByCheckedCheckbox = currentReport.fields.some((f) => {
  350. if (f.type !== 'checkbox' || criteria[f.name] !== 'true') return false;
  351. return f.disablesFieldsWhenChecked?.includes(field.name) ?? false;
  352. });
  353. const disabledRep012Status =
  354. currentReport.id === 'rep-012' &&
  355. field.name === 'status' &&
  356. rep012MultiRound;
  357. if (field.type === 'checkbox') {
  358. return (
  359. <Grid item {...gridSize} key={field.name}>
  360. <FormControlLabel
  361. control={
  362. <Checkbox
  363. checked={criteria[field.name] === 'true'}
  364. onChange={(e) =>
  365. handleFieldChange(field.name, e.target.checked ? 'true' : '')
  366. }
  367. />
  368. }
  369. label={field.label}
  370. />
  371. </Grid>
  372. );
  373. }
  374. // Use Autocomplete for fields that allow input
  375. if (field.type === 'select' && field.allowInput) {
  376. const autocompleteValue = field.multiple
  377. ? (Array.isArray(valueForSelect) ? valueForSelect : [])
  378. : (valueForSelect || null);
  379. return (
  380. <Grid item {...gridSize} key={field.name}>
  381. <Autocomplete
  382. multiple={field.multiple || false}
  383. freeSolo
  384. options={options.map(opt => opt.value)}
  385. value={autocompleteValue}
  386. onChange={(event, newValue, reason) => {
  387. if (field.multiple) {
  388. // Handle multiple selection - newValue is an array
  389. let values: string[] = [];
  390. if (Array.isArray(newValue)) {
  391. values = newValue
  392. .map(v => typeof v === 'string' ? v.trim() : String(v).trim())
  393. .filter(v => v !== '');
  394. }
  395. handleFieldChange(field.name, values);
  396. } else {
  397. // Handle single selection - newValue can be string or null
  398. const value = typeof newValue === 'string' ? newValue.trim() : (newValue || '');
  399. handleFieldChange(field.name, value);
  400. }
  401. }}
  402. onKeyDown={(event) => {
  403. // Allow Enter key to add custom value in multiple mode
  404. if (field.multiple && event.key === 'Enter') {
  405. const target = event.target as HTMLInputElement;
  406. if (target && target.value && target.value.trim()) {
  407. const currentValues = Array.isArray(autocompleteValue) ? autocompleteValue : [];
  408. const newValue = target.value.trim();
  409. if (!currentValues.includes(newValue)) {
  410. handleFieldChange(field.name, [...currentValues, newValue]);
  411. // Clear the input
  412. setTimeout(() => {
  413. if (target) target.value = '';
  414. }, 0);
  415. }
  416. }
  417. }
  418. }}
  419. renderInput={(params) => (
  420. <TextField
  421. {...params}
  422. fullWidth
  423. label={field.label}
  424. placeholder={field.placeholder || "選擇或輸入物料編號"}
  425. sx={currentReport.id === 'rep-005' ? {
  426. '& .MuiOutlinedInput-root': {
  427. minHeight: '64px',
  428. fontSize: '1rem'
  429. },
  430. '& .MuiInputLabel-root': {
  431. fontSize: '1rem'
  432. }
  433. } : {}}
  434. />
  435. )}
  436. renderTags={(value, getTagProps) =>
  437. value.map((option, index) => {
  438. // Find the label for the option if it exists in options
  439. const optionObj = options.find(opt => opt.value === option);
  440. const displayLabel = optionObj ? optionObj.label : String(option);
  441. return (
  442. <Chip
  443. variant="outlined"
  444. label={displayLabel}
  445. {...getTagProps({ index })}
  446. key={`${option}-${index}`}
  447. />
  448. );
  449. })
  450. }
  451. getOptionLabel={(option) => {
  452. // Find the label for the option if it exists in options
  453. const optionObj = options.find(opt => opt.value === option);
  454. return optionObj ? optionObj.label : String(option);
  455. }}
  456. />
  457. </Grid>
  458. );
  459. }
  460. // Regular TextField for other fields
  461. return (
  462. <Grid item {...gridSize} key={field.name}>
  463. <TextField
  464. fullWidth
  465. label={field.label}
  466. type={field.type}
  467. placeholder={field.placeholder}
  468. disabled={disabledByCheckedCheckbox || disabledRep012Status}
  469. InputLabelProps={field.type === 'date' ? { shrink: true } : {}}
  470. sx={currentReport.id === 'rep-005' ? {
  471. '& .MuiOutlinedInput-root': {
  472. minHeight: '64px',
  473. fontSize: '1rem'
  474. },
  475. '& .MuiInputLabel-root': {
  476. fontSize: '1rem'
  477. }
  478. } : {}}
  479. onChange={(e) => {
  480. if (field.multiple) {
  481. const value = typeof e.target.value === 'string'
  482. ? e.target.value.split(',')
  483. : e.target.value;
  484. // Special handling for stockCategory
  485. if (field.name === 'stockCategory' && Array.isArray(value)) {
  486. const currentValues = (criteria[field.name] || '').split(',').map(v => v.trim()).filter(v => v);
  487. const newValues = value.map(v => String(v).trim()).filter(v => v);
  488. const wasOnlyAll = currentValues.length === 1 && currentValues[0] === 'All';
  489. const hasAll = newValues.includes('All');
  490. const hasOthers = newValues.some(v => v !== 'All');
  491. if (hasAll && hasOthers) {
  492. // User selected "All" along with other options
  493. // If previously only "All" was selected, user is trying to switch - remove "All" and keep others
  494. if (wasOnlyAll) {
  495. const filteredValue = newValues.filter(v => v !== 'All');
  496. handleFieldChange(field.name, filteredValue);
  497. } else {
  498. // User added "All" to existing selections - keep only "All"
  499. handleFieldChange(field.name, ['All']);
  500. }
  501. } else if (hasAll && !hasOthers) {
  502. // Only "All" is selected
  503. handleFieldChange(field.name, ['All']);
  504. } else if (!hasAll && hasOthers) {
  505. // Other options selected without "All"
  506. handleFieldChange(field.name, newValues);
  507. } else {
  508. // Empty selection
  509. handleFieldChange(field.name, []);
  510. }
  511. } else {
  512. handleFieldChange(field.name, value);
  513. }
  514. } else {
  515. handleFieldChange(field.name, e.target.value);
  516. }
  517. }}
  518. value={valueForSelect}
  519. select={field.type === 'select'}
  520. SelectProps={field.multiple ? {
  521. multiple: true,
  522. renderValue: (selected: any) => {
  523. if (Array.isArray(selected)) {
  524. return selected
  525. .map((v) => {
  526. const opt = options.find((o) => o.value === v);
  527. return opt?.label ?? String(v);
  528. })
  529. .join(', ');
  530. }
  531. return selected;
  532. }
  533. } : {}}
  534. >
  535. {field.type === 'select' && options.map((opt) => (
  536. <MenuItem key={opt.value} value={opt.value}>
  537. {opt.label}
  538. </MenuItem>
  539. ))}
  540. </TextField>
  541. </Grid>
  542. );
  543. })}
  544. </Grid>
  545. <Box sx={{ mt: 4, display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
  546. {currentReport.id === 'rep-005' ? (
  547. <SemiFGProductionAnalysisReport
  548. criteria={criteria}
  549. requiredFieldLabels={currentReport.fields.filter(f => f.required && !criteria[f.name]).map(f => f.label)}
  550. loading={loading}
  551. setLoading={setLoading}
  552. reportTitle={currentReport.title}
  553. onExportSuccess={(format) => {
  554. logFeatureUsage(
  555. FEATURE_USAGE.REPORT_MANAGEMENT,
  556. FEATURE_USAGE_ACTION.DOWNLOAD,
  557. `${currentReport.id}:${format}`,
  558. );
  559. }}
  560. />
  561. ) : currentReport.id === 'rep-013' || currentReport.id === 'rep-009' || currentReport.id === 'rep-012' || currentReport.id === 'rep-004' || currentReport.id === 'rep-007' || currentReport.id === 'rep-008' || currentReport.id === 'rep-011' ? (
  562. <>
  563. <Button
  564. variant="contained"
  565. size="large"
  566. startIcon={<DownloadIcon />}
  567. onClick={handlePrint}
  568. disabled={loading}
  569. sx={{ px: 4 }}
  570. >
  571. {loading ? "生成 PDF..." : "下載報告 (PDF)"}
  572. </Button>
  573. <Button
  574. variant="outlined"
  575. size="large"
  576. startIcon={<DownloadIcon />}
  577. onClick={handleExcelPrint}
  578. disabled={loading}
  579. sx={{ px: 4 }}
  580. >
  581. {loading ? "生成 Excel..." : "下載報告 (Excel)"}
  582. </Button>
  583. </>
  584. ) : currentReport.id === 'rep-006' ? (
  585. <>
  586. <Button
  587. variant="contained"
  588. size="large"
  589. startIcon={<DownloadIcon />}
  590. onClick={handlePrint}
  591. disabled={loading}
  592. sx={{ px: 4 }}
  593. >
  594. {loading ? "生成 PDF..." : "下載報告 (PDF)"}
  595. </Button>
  596. <Button
  597. variant="outlined"
  598. size="large"
  599. startIcon={<DownloadIcon />}
  600. onClick={handleExcelPrint}
  601. disabled={loading}
  602. sx={{ px: 4 }}
  603. >
  604. {loading ? "生成 Excel..." : "下載報告 (Excel)"}
  605. </Button>
  606. </>
  607. ) : currentReport.id === 'rep-010' ? (
  608. <>
  609. <Button
  610. variant="contained"
  611. size="large"
  612. startIcon={<DownloadIcon />}
  613. onClick={handlePrint}
  614. disabled={loading}
  615. sx={{ px: 4 }}
  616. >
  617. {loading ? "生成 PDF..." : "下載報告 (PDF)"}
  618. </Button>
  619. <Button
  620. variant="outlined"
  621. size="large"
  622. startIcon={<DownloadIcon />}
  623. onClick={handleExcelPrint}
  624. disabled={loading}
  625. sx={{ px: 4 }}
  626. >
  627. {loading ? "生成 Excel..." : "下載報告 (Excel)"}
  628. </Button>
  629. </>
  630. ) : currentReport.responseType === 'excel' ? (
  631. <Button
  632. variant="contained"
  633. size="large"
  634. startIcon={<DownloadIcon />}
  635. onClick={handlePrint}
  636. disabled={loading}
  637. sx={{ px: 4 }}
  638. >
  639. {loading ? "生成 Excel..." : "下載報告 (Excel)"}
  640. </Button>
  641. ) : (
  642. <Button
  643. variant="contained"
  644. size="large"
  645. startIcon={<DownloadIcon />}
  646. onClick={handlePrint}
  647. disabled={loading}
  648. sx={{ px: 4 }}
  649. >
  650. {loading ? "生成報告..." : "下載報告 (PDF)"}
  651. </Button>
  652. )}
  653. </Box>
  654. </CardContent>
  655. </Card>
  656. )}
  657. </Box>
  658. );
  659. }