FPSMS-frontend
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 

601 linhas
18 KiB

  1. "use client";
  2. import SearchBox, { Criterion } from "../SearchBox";
  3. import { useCallback, useMemo, useState, useEffect } from "react";
  4. import { useTranslation } from "react-i18next";
  5. import SearchResults, { Column } from "../SearchResults";
  6. import { useRouter } from "next/navigation";
  7. import { successDialog } from "../Swal/CustomAlerts";
  8. import useUploadContext from "../UploadProvider/useUploadContext";
  9. import { downloadFile } from "@/app/utils/commonUtil";
  10. import { EquipmentDetailResult } from "@/app/api/settings/equipmentDetail";
  11. import { exportEquipmentQrCode, printEquipmentQrCode } from "@/app/api/settings/equipmentDetail/client";
  12. import {
  13. Checkbox,
  14. Box,
  15. Button,
  16. TextField,
  17. Stack,
  18. Autocomplete,
  19. Modal,
  20. Card,
  21. IconButton,
  22. Table,
  23. TableBody,
  24. TableCell,
  25. TableContainer,
  26. TableHead,
  27. TableRow,
  28. Paper,
  29. Typography
  30. } from "@mui/material";
  31. import DownloadIcon from "@mui/icons-material/Download";
  32. import PrintIcon from "@mui/icons-material/Print";
  33. import CloseIcon from "@mui/icons-material/Close";
  34. import { PrinterCombo } from "@/app/api/settings/printer";
  35. import axiosInstance from "@/app/(main)/axios/axiosInstance";
  36. import { NEXT_PUBLIC_API_URL } from "@/config/api";
  37. interface Props {
  38. equipmentDetails: EquipmentDetailResult[];
  39. printerCombo: PrinterCombo[];
  40. }
  41. type SearchQuery = Partial<Omit<EquipmentDetailResult, "id">>;
  42. type SearchParamNames = keyof SearchQuery;
  43. const QrCodeHandleEquipmentSearch: React.FC<Props> = ({ equipmentDetails, printerCombo }) => {
  44. const { t } = useTranslation("common");
  45. const [filteredEquipmentDetails, setFilteredEquipmentDetails] = useState<EquipmentDetailResult[]>([]);
  46. const router = useRouter();
  47. const { setIsUploading } = useUploadContext();
  48. const [pagingController, setPagingController] = useState({
  49. pageNum: 1,
  50. pageSize: 10,
  51. });
  52. const [filterObj, setFilterObj] = useState({});
  53. const [totalCount, setTotalCount] = useState(0);
  54. const [checkboxIds, setCheckboxIds] = useState<(string | number)[]>([]);
  55. const [selectedEquipmentDetailsMap, setSelectedEquipmentDetailsMap] = useState<Map<string | number, EquipmentDetailResult>>(new Map());
  56. const [selectAll, setSelectAll] = useState(false);
  57. const [printQty, setPrintQty] = useState(1);
  58. const [isSearching, setIsSearching] = useState(false);
  59. const [previewOpen, setPreviewOpen] = useState(false);
  60. const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  61. const [selectedEquipmentDetailsModalOpen, setSelectedEquipmentDetailsModalOpen] = useState(false);
  62. const filteredPrinters = useMemo(() => {
  63. return printerCombo.filter((printer) => {
  64. return printer.type === "A4";
  65. });
  66. }, [printerCombo]);
  67. const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
  68. filteredPrinters.length > 0 ? filteredPrinters[0] : undefined
  69. );
  70. useEffect(() => {
  71. if (!selectedPrinter || !filteredPrinters.find(p => p.id === selectedPrinter.id)) {
  72. setSelectedPrinter(filteredPrinters.length > 0 ? filteredPrinters[0] : undefined);
  73. }
  74. }, [filteredPrinters, selectedPrinter]);
  75. const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
  76. () => [
  77. {
  78. label: "設備名稱",
  79. paramName: "code",
  80. type: "text",
  81. },
  82. {
  83. label: "設備編號",
  84. paramName: "equipmentCode",
  85. type: "text",
  86. },
  87. ],
  88. [],
  89. );
  90. interface ApiResponse<T> {
  91. records: T[];
  92. total: number;
  93. }
  94. const refetchData = useCallback(
  95. async (filterObj: SearchQuery, pageNum: number, pageSize: number) => {
  96. const authHeader = axiosInstance.defaults.headers["Authorization"];
  97. if (!authHeader) {
  98. setTimeout(() => {
  99. refetchData(filterObj, pageNum, pageSize);
  100. }, 10);
  101. return;
  102. }
  103. const params = {
  104. pageNum: pageNum,
  105. pageSize: pageSize,
  106. ...filterObj,
  107. };
  108. try {
  109. const response = await axiosInstance.get<ApiResponse<EquipmentDetailResult>>(
  110. `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage`,
  111. { params },
  112. );
  113. if (response.status == 200) {
  114. setFilteredEquipmentDetails(response.data.records);
  115. setTotalCount(response.data.total);
  116. return response;
  117. } else {
  118. throw "400";
  119. }
  120. } catch (error) {
  121. console.error("Error fetching equipment details:", error);
  122. throw error;
  123. }
  124. },
  125. [],
  126. );
  127. useEffect(() => {
  128. refetchData(filterObj, pagingController.pageNum, pagingController.pageSize);
  129. }, [filterObj, pagingController.pageNum, pagingController.pageSize]);
  130. useEffect(() => {
  131. if (filteredEquipmentDetails.length > 0) {
  132. const allCurrentPageSelected = filteredEquipmentDetails.every(ed => checkboxIds.includes(ed.id));
  133. setSelectAll(allCurrentPageSelected);
  134. } else {
  135. setSelectAll(false);
  136. }
  137. }, [filteredEquipmentDetails, checkboxIds]);
  138. const handleSelectEquipmentDetail = useCallback((equipmentDetailId: string | number, checked: boolean) => {
  139. if (checked) {
  140. const equipmentDetail = filteredEquipmentDetails.find(ed => ed.id === equipmentDetailId);
  141. if (equipmentDetail) {
  142. setCheckboxIds(prev => [...prev, equipmentDetailId]);
  143. setSelectedEquipmentDetailsMap(prev => {
  144. const newMap = new Map(prev);
  145. newMap.set(equipmentDetailId, equipmentDetail);
  146. return newMap;
  147. });
  148. }
  149. } else {
  150. setCheckboxIds(prev => prev.filter(id => id !== equipmentDetailId));
  151. setSelectedEquipmentDetailsMap(prev => {
  152. const newMap = new Map(prev);
  153. newMap.delete(equipmentDetailId);
  154. return newMap;
  155. });
  156. setSelectAll(false);
  157. }
  158. }, [filteredEquipmentDetails]);
  159. const fetchAllMatchingEquipmentDetails = useCallback(async (): Promise<EquipmentDetailResult[]> => {
  160. const authHeader = axiosInstance.defaults.headers["Authorization"];
  161. if (!authHeader) {
  162. return [];
  163. }
  164. if (totalCount === 0) {
  165. return [];
  166. }
  167. const params = {
  168. pageNum: 1,
  169. pageSize: totalCount > 0 ? totalCount : 10000,
  170. ...filterObj,
  171. };
  172. try {
  173. const response = await axiosInstance.get<ApiResponse<EquipmentDetailResult>>(
  174. `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage`,
  175. { params },
  176. );
  177. if (response.status == 200) {
  178. return response.data.records;
  179. }
  180. return [];
  181. } catch (error) {
  182. console.error("Error fetching all equipment details:", error);
  183. return [];
  184. }
  185. }, [filterObj, totalCount]);
  186. const handleSelectAll = useCallback(async (checked: boolean) => {
  187. if (checked) {
  188. try {
  189. const allEquipmentDetails = await fetchAllMatchingEquipmentDetails();
  190. const allIds = allEquipmentDetails.map(equipmentDetail => equipmentDetail.id);
  191. setCheckboxIds(allIds);
  192. setSelectedEquipmentDetailsMap(prev => {
  193. const newMap = new Map(prev);
  194. allEquipmentDetails.forEach(equipmentDetail => {
  195. newMap.set(equipmentDetail.id, equipmentDetail);
  196. });
  197. return newMap;
  198. });
  199. setSelectAll(true);
  200. } catch (error) {
  201. console.error("Error selecting all equipment:", error);
  202. }
  203. } else {
  204. setCheckboxIds([]);
  205. setSelectedEquipmentDetailsMap(new Map());
  206. setSelectAll(false);
  207. }
  208. }, [fetchAllMatchingEquipmentDetails]);
  209. const showPdfPreview = useCallback(async (equipmentDetailIds: (string | number)[]) => {
  210. if (equipmentDetailIds.length === 0) {
  211. return;
  212. }
  213. try {
  214. setIsUploading(true);
  215. const numericIds = equipmentDetailIds.map(id => typeof id === 'string' ? parseInt(id) : id);
  216. const response = await exportEquipmentQrCode(numericIds);
  217. const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" });
  218. const url = URL.createObjectURL(blob);
  219. setPreviewUrl(`${url}#toolbar=0`);
  220. setPreviewOpen(true);
  221. } catch (error) {
  222. console.error("Error exporting QR code:", error);
  223. } finally {
  224. setIsUploading(false);
  225. }
  226. }, [setIsUploading]);
  227. const handleClosePreview = useCallback(() => {
  228. setPreviewOpen(false);
  229. if (previewUrl) {
  230. URL.revokeObjectURL(previewUrl);
  231. setPreviewUrl(null);
  232. }
  233. }, [previewUrl]);
  234. const handleDownloadQrCode = useCallback(async (equipmentDetailIds: (string | number)[]) => {
  235. if (equipmentDetailIds.length === 0) {
  236. return;
  237. }
  238. try {
  239. setIsUploading(true);
  240. const numericIds = equipmentDetailIds.map(id => typeof id === 'string' ? parseInt(id) : id);
  241. const response = await exportEquipmentQrCode(numericIds);
  242. downloadFile(response.blobValue, response.filename);
  243. setSelectedEquipmentDetailsModalOpen(false);
  244. successDialog("二維碼已下載", t);
  245. } catch (error) {
  246. console.error("Error exporting QR code:", error);
  247. } finally {
  248. setIsUploading(false);
  249. }
  250. }, [setIsUploading, t]);
  251. const handlePrint = useCallback(async () => {
  252. if (checkboxIds.length === 0 || !selectedPrinter) {
  253. return;
  254. }
  255. try {
  256. setIsUploading(true);
  257. const numericIds = checkboxIds.map(id => typeof id === 'string' ? parseInt(id) : id);
  258. await printEquipmentQrCode({
  259. equipmentDetailIds: numericIds,
  260. printerId: selectedPrinter.id,
  261. printQty,
  262. });
  263. setSelectedEquipmentDetailsModalOpen(false);
  264. successDialog("二維碼已列印", t);
  265. } catch (error) {
  266. console.error("Error printing QR code:", error);
  267. } finally {
  268. setIsUploading(false);
  269. }
  270. }, [checkboxIds, printQty, selectedPrinter, setIsUploading, t]);
  271. const handleViewSelectedQrCodes = useCallback(() => {
  272. if (checkboxIds.length === 0) {
  273. return;
  274. }
  275. setSelectedEquipmentDetailsModalOpen(true);
  276. }, [checkboxIds]);
  277. const selectedEquipmentDetails = useMemo(() => {
  278. return Array.from(selectedEquipmentDetailsMap.values());
  279. }, [selectedEquipmentDetailsMap]);
  280. const handleCloseSelectedEquipmentDetailsModal = useCallback(() => {
  281. setSelectedEquipmentDetailsModalOpen(false);
  282. }, []);
  283. const columns = useMemo<Column<EquipmentDetailResult>[]>(
  284. () => [
  285. {
  286. name: "id",
  287. label: "",
  288. sx: { width: "50px", minWidth: "50px" },
  289. renderCell: (params) => (
  290. <Checkbox
  291. checked={checkboxIds.includes(params.id)}
  292. onChange={(e) => handleSelectEquipmentDetail(params.id, e.target.checked)}
  293. onClick={(e) => e.stopPropagation()}
  294. />
  295. ),
  296. },
  297. {
  298. name: "code",
  299. label: "設備名稱",
  300. align: "left",
  301. headerAlign: "left",
  302. sx: { width: "150px", minWidth: "150px" },
  303. },
  304. {
  305. name: "description",
  306. label: "設備描述",
  307. align: "left",
  308. headerAlign: "left",
  309. sx: { width: "200px", minWidth: "200px" },
  310. },
  311. {
  312. name: "equipmentCode",
  313. label: "設備編號",
  314. align: "left",
  315. headerAlign: "left",
  316. sx: { width: "150px", minWidth: "150px" },
  317. },
  318. ],
  319. [t, checkboxIds, handleSelectEquipmentDetail],
  320. );
  321. const onReset = useCallback(() => {
  322. setFilterObj({});
  323. setPagingController({ pageNum: 1, pageSize: 10 });
  324. }, []);
  325. return (
  326. <>
  327. <SearchBox
  328. criteria={searchCriteria}
  329. onSearch={(query) => {
  330. setFilterObj({
  331. ...query,
  332. });
  333. setPagingController({ pageNum: 1, pageSize: 10 });
  334. }}
  335. onReset={onReset}
  336. />
  337. <SearchResults<EquipmentDetailResult>
  338. items={filteredEquipmentDetails}
  339. columns={columns}
  340. pagingController={pagingController}
  341. setPagingController={setPagingController}
  342. totalCount={totalCount}
  343. isAutoPaging={false}
  344. />
  345. <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
  346. <Button
  347. variant="outlined"
  348. onClick={() => handleSelectAll(!selectAll)}
  349. startIcon={<Checkbox checked={selectAll} />}
  350. disabled={isSearching || totalCount === 0}
  351. >
  352. 選擇全部設備 ({checkboxIds.length} / {totalCount})
  353. </Button>
  354. <Button
  355. variant="contained"
  356. onClick={handleViewSelectedQrCodes}
  357. disabled={checkboxIds.length === 0}
  358. color="primary"
  359. >
  360. 查看已選擇設備二維碼 ({checkboxIds.length})
  361. </Button>
  362. </Box>
  363. <Modal
  364. open={selectedEquipmentDetailsModalOpen}
  365. onClose={handleCloseSelectedEquipmentDetailsModal}
  366. sx={{
  367. display: 'flex',
  368. alignItems: 'center',
  369. justifyContent: 'center',
  370. }}
  371. >
  372. <Card
  373. sx={{
  374. position: 'relative',
  375. width: '90%',
  376. maxWidth: '800px',
  377. maxHeight: '90vh',
  378. display: 'flex',
  379. flexDirection: 'column',
  380. outline: 'none',
  381. }}
  382. >
  383. <Box
  384. sx={{
  385. display: 'flex',
  386. justifyContent: 'space-between',
  387. alignItems: 'center',
  388. p: 2,
  389. borderBottom: 1,
  390. borderColor: 'divider',
  391. }}
  392. >
  393. <Typography variant="h6" component="h2">
  394. 已選擇設備 ({selectedEquipmentDetails.length})
  395. </Typography>
  396. <IconButton onClick={handleCloseSelectedEquipmentDetailsModal}>
  397. <CloseIcon />
  398. </IconButton>
  399. </Box>
  400. <Box
  401. sx={{
  402. flex: 1,
  403. overflow: 'auto',
  404. p: 2,
  405. }}
  406. >
  407. <TableContainer component={Paper} variant="outlined">
  408. <Table>
  409. <TableHead>
  410. <TableRow>
  411. <TableCell>
  412. <strong>設備名稱</strong>
  413. </TableCell>
  414. <TableCell>
  415. <strong>設備描述</strong>
  416. </TableCell>
  417. <TableCell>
  418. <strong>設備編號</strong>
  419. </TableCell>
  420. </TableRow>
  421. </TableHead>
  422. <TableBody>
  423. {selectedEquipmentDetails.length === 0 ? (
  424. <TableRow>
  425. <TableCell colSpan={3} align="center">
  426. 沒有選擇的設備
  427. </TableCell>
  428. </TableRow>
  429. ) : (
  430. selectedEquipmentDetails.map((equipmentDetail) => (
  431. <TableRow key={equipmentDetail.id}>
  432. <TableCell>{equipmentDetail.code || '-'}</TableCell>
  433. <TableCell>{equipmentDetail.description || '-'}</TableCell>
  434. <TableCell>{equipmentDetail.equipmentCode || '-'}</TableCell>
  435. </TableRow>
  436. ))
  437. )}
  438. </TableBody>
  439. </Table>
  440. </TableContainer>
  441. </Box>
  442. <Box
  443. sx={{
  444. p: 2,
  445. borderTop: 1,
  446. borderColor: 'divider',
  447. bgcolor: 'background.paper',
  448. }}
  449. >
  450. <Stack direction="row" justifyContent="flex-end" alignItems="center" gap={2}>
  451. <Autocomplete<PrinterCombo>
  452. options={filteredPrinters}
  453. value={selectedPrinter ?? null}
  454. onChange={(event, value) => {
  455. setSelectedPrinter(value ?? undefined);
  456. }}
  457. getOptionLabel={(option) => option.name || option.label || option.code || String(option.id)}
  458. renderInput={(params) => (
  459. <TextField
  460. {...params}
  461. variant="outlined"
  462. label="列印機"
  463. sx={{ width: 300 }}
  464. />
  465. )}
  466. />
  467. <TextField
  468. variant="outlined"
  469. label="列印數量"
  470. type="number"
  471. value={printQty}
  472. onChange={(e) => {
  473. const value = parseInt(e.target.value) || 1;
  474. setPrintQty(Math.max(1, value));
  475. }}
  476. inputProps={{ min: 1 }}
  477. sx={{ width: 120 }}
  478. />
  479. <Button
  480. variant="contained"
  481. startIcon={<PrintIcon />}
  482. onClick={handlePrint}
  483. disabled={checkboxIds.length === 0 || filteredPrinters.length === 0 || !selectedPrinter}
  484. color="primary"
  485. >
  486. 列印
  487. </Button>
  488. <Button
  489. variant="contained"
  490. startIcon={<DownloadIcon />}
  491. onClick={() => handleDownloadQrCode(checkboxIds)}
  492. disabled={checkboxIds.length === 0}
  493. color="primary"
  494. >
  495. 下載二維碼
  496. </Button>
  497. </Stack>
  498. </Box>
  499. </Card>
  500. </Modal>
  501. <Modal
  502. open={previewOpen}
  503. onClose={handleClosePreview}
  504. sx={{
  505. display: 'flex',
  506. alignItems: 'center',
  507. justifyContent: 'center',
  508. }}
  509. >
  510. <Card
  511. sx={{
  512. position: 'relative',
  513. width: '90%',
  514. maxWidth: '900px',
  515. maxHeight: '90vh',
  516. display: 'flex',
  517. flexDirection: 'column',
  518. outline: 'none',
  519. }}
  520. >
  521. <Box
  522. sx={{
  523. display: 'flex',
  524. justifyContent: 'flex-end',
  525. alignItems: 'center',
  526. p: 2,
  527. borderBottom: 1,
  528. borderColor: 'divider',
  529. }}
  530. >
  531. <IconButton
  532. onClick={handleClosePreview}
  533. >
  534. <CloseIcon />
  535. </IconButton>
  536. </Box>
  537. <Box
  538. sx={{
  539. flex: 1,
  540. overflow: 'auto',
  541. p: 2,
  542. }}
  543. >
  544. {previewUrl && (
  545. <iframe
  546. src={previewUrl}
  547. width="100%"
  548. height="600px"
  549. style={{
  550. border: 'none',
  551. }}
  552. title="PDF Preview"
  553. />
  554. )}
  555. </Box>
  556. </Card>
  557. </Modal>
  558. </>
  559. );
  560. };
  561. export default QrCodeHandleEquipmentSearch;