|
- "use client";
-
- import SearchBox, { Criterion } from "../SearchBox";
- import { useCallback, useMemo, useState, useEffect } from "react";
- import { useTranslation } from "react-i18next";
- import SearchResults, { Column } from "../SearchResults";
- import { useRouter } from "next/navigation";
- import { successDialog } from "../Swal/CustomAlerts";
- import useUploadContext from "../UploadProvider/useUploadContext";
- import { downloadFile } from "@/app/utils/commonUtil";
- import { EquipmentDetailResult } from "@/app/api/settings/equipmentDetail";
- import { exportEquipmentQrCode, printEquipmentQrCode } from "@/app/api/settings/equipmentDetail/client";
- import {
- Checkbox,
- Box,
- Button,
- TextField,
- Stack,
- Autocomplete,
- Modal,
- Card,
- IconButton,
- Table,
- TableBody,
- TableCell,
- TableContainer,
- TableHead,
- TableRow,
- Paper,
- Typography
- } from "@mui/material";
- import DownloadIcon from "@mui/icons-material/Download";
- import PrintIcon from "@mui/icons-material/Print";
- import CloseIcon from "@mui/icons-material/Close";
- import { PrinterCombo } from "@/app/api/settings/printer";
- import axiosInstance from "@/app/(main)/axios/axiosInstance";
- import { NEXT_PUBLIC_API_URL } from "@/config/api";
-
- interface Props {
- equipmentDetails: EquipmentDetailResult[];
- printerCombo: PrinterCombo[];
- }
-
- type SearchQuery = Partial<Omit<EquipmentDetailResult, "id">>;
- type SearchParamNames = keyof SearchQuery;
-
- const QrCodeHandleEquipmentSearch: React.FC<Props> = ({ equipmentDetails, printerCombo }) => {
- const { t } = useTranslation("common");
- const [filteredEquipmentDetails, setFilteredEquipmentDetails] = useState<EquipmentDetailResult[]>([]);
- const router = useRouter();
- const { setIsUploading } = useUploadContext();
- const [pagingController, setPagingController] = useState({
- pageNum: 1,
- pageSize: 10,
- });
- const [filterObj, setFilterObj] = useState({});
- const [totalCount, setTotalCount] = useState(0);
-
- const [checkboxIds, setCheckboxIds] = useState<(string | number)[]>([]);
- const [selectedEquipmentDetailsMap, setSelectedEquipmentDetailsMap] = useState<Map<string | number, EquipmentDetailResult>>(new Map());
- const [selectAll, setSelectAll] = useState(false);
- const [printQty, setPrintQty] = useState(1);
- const [isSearching, setIsSearching] = useState(false);
-
- const [previewOpen, setPreviewOpen] = useState(false);
- const [previewUrl, setPreviewUrl] = useState<string | null>(null);
-
- const [selectedEquipmentDetailsModalOpen, setSelectedEquipmentDetailsModalOpen] = useState(false);
-
- const filteredPrinters = useMemo(() => {
- return printerCombo.filter((printer) => {
- return printer.type === "A4";
- });
- }, [printerCombo]);
-
- const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | undefined>(
- filteredPrinters.length > 0 ? filteredPrinters[0] : undefined
- );
-
- useEffect(() => {
- if (!selectedPrinter || !filteredPrinters.find(p => p.id === selectedPrinter.id)) {
- setSelectedPrinter(filteredPrinters.length > 0 ? filteredPrinters[0] : undefined);
- }
- }, [filteredPrinters, selectedPrinter]);
-
- const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
- () => [
- {
- label: "設備名稱",
- paramName: "code",
- type: "text",
- },
- {
- label: "設備編號",
- paramName: "equipmentCode",
- type: "text",
- },
- ],
- [],
- );
-
- interface ApiResponse<T> {
- records: T[];
- total: number;
- }
-
- const refetchData = useCallback(
- async (filterObj: SearchQuery, pageNum: number, pageSize: number) => {
- const authHeader = axiosInstance.defaults.headers["Authorization"];
- if (!authHeader) {
- setTimeout(() => {
- refetchData(filterObj, pageNum, pageSize);
- }, 10);
- return;
- }
- const params = {
- pageNum: pageNum,
- pageSize: pageSize,
- ...filterObj,
- };
- try {
- const response = await axiosInstance.get<ApiResponse<EquipmentDetailResult>>(
- `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage`,
- { params },
- );
- if (response.status == 200) {
- setFilteredEquipmentDetails(response.data.records);
- setTotalCount(response.data.total);
- return response;
- } else {
- throw "400";
- }
- } catch (error) {
- console.error("Error fetching equipment details:", error);
- throw error;
- }
- },
- [],
- );
-
- useEffect(() => {
- refetchData(filterObj, pagingController.pageNum, pagingController.pageSize);
- }, [filterObj, pagingController.pageNum, pagingController.pageSize]);
-
- useEffect(() => {
- if (filteredEquipmentDetails.length > 0) {
- const allCurrentPageSelected = filteredEquipmentDetails.every(ed => checkboxIds.includes(ed.id));
- setSelectAll(allCurrentPageSelected);
- } else {
- setSelectAll(false);
- }
- }, [filteredEquipmentDetails, checkboxIds]);
-
- const handleSelectEquipmentDetail = useCallback((equipmentDetailId: string | number, checked: boolean) => {
- if (checked) {
- const equipmentDetail = filteredEquipmentDetails.find(ed => ed.id === equipmentDetailId);
- if (equipmentDetail) {
- setCheckboxIds(prev => [...prev, equipmentDetailId]);
- setSelectedEquipmentDetailsMap(prev => {
- const newMap = new Map(prev);
- newMap.set(equipmentDetailId, equipmentDetail);
- return newMap;
- });
- }
- } else {
- setCheckboxIds(prev => prev.filter(id => id !== equipmentDetailId));
- setSelectedEquipmentDetailsMap(prev => {
- const newMap = new Map(prev);
- newMap.delete(equipmentDetailId);
- return newMap;
- });
- setSelectAll(false);
- }
- }, [filteredEquipmentDetails]);
-
- const fetchAllMatchingEquipmentDetails = useCallback(async (): Promise<EquipmentDetailResult[]> => {
- const authHeader = axiosInstance.defaults.headers["Authorization"];
- if (!authHeader) {
- return [];
- }
-
- if (totalCount === 0) {
- return [];
- }
-
- const params = {
- pageNum: 1,
- pageSize: totalCount > 0 ? totalCount : 10000,
- ...filterObj,
- };
- try {
- const response = await axiosInstance.get<ApiResponse<EquipmentDetailResult>>(
- `${NEXT_PUBLIC_API_URL}/EquipmentDetail/getRecordByPage`,
- { params },
- );
- if (response.status == 200) {
- return response.data.records;
- }
- return [];
- } catch (error) {
- console.error("Error fetching all equipment details:", error);
- return [];
- }
- }, [filterObj, totalCount]);
-
- const handleSelectAll = useCallback(async (checked: boolean) => {
- if (checked) {
- try {
- const allEquipmentDetails = await fetchAllMatchingEquipmentDetails();
- const allIds = allEquipmentDetails.map(equipmentDetail => equipmentDetail.id);
-
- setCheckboxIds(allIds);
- setSelectedEquipmentDetailsMap(prev => {
- const newMap = new Map(prev);
- allEquipmentDetails.forEach(equipmentDetail => {
- newMap.set(equipmentDetail.id, equipmentDetail);
- });
- return newMap;
- });
- setSelectAll(true);
- } catch (error) {
- console.error("Error selecting all equipment:", error);
- }
- } else {
- setCheckboxIds([]);
- setSelectedEquipmentDetailsMap(new Map());
- setSelectAll(false);
- }
- }, [fetchAllMatchingEquipmentDetails]);
-
- const showPdfPreview = useCallback(async (equipmentDetailIds: (string | number)[]) => {
- if (equipmentDetailIds.length === 0) {
- return;
- }
- try {
- setIsUploading(true);
- const numericIds = equipmentDetailIds.map(id => typeof id === 'string' ? parseInt(id) : id);
- const response = await exportEquipmentQrCode(numericIds);
-
- const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" });
- const url = URL.createObjectURL(blob);
-
- setPreviewUrl(`${url}#toolbar=0`);
- setPreviewOpen(true);
- } catch (error) {
- console.error("Error exporting QR code:", error);
- } finally {
- setIsUploading(false);
- }
- }, [setIsUploading]);
-
- const handleClosePreview = useCallback(() => {
- setPreviewOpen(false);
- if (previewUrl) {
- URL.revokeObjectURL(previewUrl);
- setPreviewUrl(null);
- }
- }, [previewUrl]);
-
- const handleDownloadQrCode = useCallback(async (equipmentDetailIds: (string | number)[]) => {
- if (equipmentDetailIds.length === 0) {
- return;
- }
- try {
- setIsUploading(true);
- const numericIds = equipmentDetailIds.map(id => typeof id === 'string' ? parseInt(id) : id);
- const response = await exportEquipmentQrCode(numericIds);
- downloadFile(response.blobValue, response.filename);
- setSelectedEquipmentDetailsModalOpen(false);
- successDialog("二維碼已下載", t);
- } catch (error) {
- console.error("Error exporting QR code:", error);
- } finally {
- setIsUploading(false);
- }
- }, [setIsUploading, t]);
-
- const handlePrint = useCallback(async () => {
- if (checkboxIds.length === 0 || !selectedPrinter) {
- return;
- }
- try {
- setIsUploading(true);
- const numericIds = checkboxIds.map(id => typeof id === 'string' ? parseInt(id) : id);
- await printEquipmentQrCode({
- equipmentDetailIds: numericIds,
- printerId: selectedPrinter.id,
- printQty,
- });
- setSelectedEquipmentDetailsModalOpen(false);
- successDialog("二維碼已列印", t);
- } catch (error) {
- console.error("Error printing QR code:", error);
- } finally {
- setIsUploading(false);
- }
- }, [checkboxIds, printQty, selectedPrinter, setIsUploading, t]);
-
- const handleViewSelectedQrCodes = useCallback(() => {
- if (checkboxIds.length === 0) {
- return;
- }
- setSelectedEquipmentDetailsModalOpen(true);
- }, [checkboxIds]);
-
- const selectedEquipmentDetails = useMemo(() => {
- return Array.from(selectedEquipmentDetailsMap.values());
- }, [selectedEquipmentDetailsMap]);
-
- const handleCloseSelectedEquipmentDetailsModal = useCallback(() => {
- setSelectedEquipmentDetailsModalOpen(false);
- }, []);
-
- const columns = useMemo<Column<EquipmentDetailResult>[]>(
- () => [
- {
- name: "id",
- label: "",
- sx: { width: "50px", minWidth: "50px" },
- renderCell: (params) => (
- <Checkbox
- checked={checkboxIds.includes(params.id)}
- onChange={(e) => handleSelectEquipmentDetail(params.id, e.target.checked)}
- onClick={(e) => e.stopPropagation()}
- />
- ),
- },
- {
- name: "code",
- label: "設備名稱",
- align: "left",
- headerAlign: "left",
- sx: { width: "150px", minWidth: "150px" },
- },
- {
- name: "description",
- label: "設備描述",
- align: "left",
- headerAlign: "left",
- sx: { width: "200px", minWidth: "200px" },
- },
- {
- name: "equipmentCode",
- label: "設備編號",
- align: "left",
- headerAlign: "left",
- sx: { width: "150px", minWidth: "150px" },
- },
- ],
- [t, checkboxIds, handleSelectEquipmentDetail],
- );
-
- const onReset = useCallback(() => {
- setFilterObj({});
- setPagingController({ pageNum: 1, pageSize: 10 });
- }, []);
-
- return (
- <>
- <SearchBox
- criteria={searchCriteria}
- onSearch={(query) => {
- setFilterObj({
- ...query,
- });
- setPagingController({ pageNum: 1, pageSize: 10 });
- }}
- onReset={onReset}
- />
- <SearchResults<EquipmentDetailResult>
- items={filteredEquipmentDetails}
- columns={columns}
- pagingController={pagingController}
- setPagingController={setPagingController}
- totalCount={totalCount}
- isAutoPaging={false}
- />
- <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
- <Button
- variant="outlined"
- onClick={() => handleSelectAll(!selectAll)}
- startIcon={<Checkbox checked={selectAll} />}
- disabled={isSearching || totalCount === 0}
- >
- 選擇全部設備 ({checkboxIds.length} / {totalCount})
- </Button>
- <Button
- variant="contained"
- onClick={handleViewSelectedQrCodes}
- disabled={checkboxIds.length === 0}
- color="primary"
- >
- 查看已選擇設備二維碼 ({checkboxIds.length})
- </Button>
- </Box>
-
- <Modal
- open={selectedEquipmentDetailsModalOpen}
- onClose={handleCloseSelectedEquipmentDetailsModal}
- sx={{
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- }}
- >
- <Card
- sx={{
- position: 'relative',
- width: '90%',
- maxWidth: '800px',
- maxHeight: '90vh',
- display: 'flex',
- flexDirection: 'column',
- outline: 'none',
- }}
- >
- <Box
- sx={{
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- p: 2,
- borderBottom: 1,
- borderColor: 'divider',
- }}
- >
- <Typography variant="h6" component="h2">
- 已選擇設備 ({selectedEquipmentDetails.length})
- </Typography>
- <IconButton onClick={handleCloseSelectedEquipmentDetailsModal}>
- <CloseIcon />
- </IconButton>
- </Box>
-
- <Box
- sx={{
- flex: 1,
- overflow: 'auto',
- p: 2,
- }}
- >
- <TableContainer component={Paper} variant="outlined">
- <Table>
- <TableHead>
- <TableRow>
- <TableCell>
- <strong>設備名稱</strong>
- </TableCell>
- <TableCell>
- <strong>設備描述</strong>
- </TableCell>
- <TableCell>
- <strong>設備編號</strong>
- </TableCell>
- </TableRow>
- </TableHead>
- <TableBody>
- {selectedEquipmentDetails.length === 0 ? (
- <TableRow>
- <TableCell colSpan={3} align="center">
- 沒有選擇的設備
- </TableCell>
- </TableRow>
- ) : (
- selectedEquipmentDetails.map((equipmentDetail) => (
- <TableRow key={equipmentDetail.id}>
- <TableCell>{equipmentDetail.code || '-'}</TableCell>
- <TableCell>{equipmentDetail.description || '-'}</TableCell>
- <TableCell>{equipmentDetail.equipmentCode || '-'}</TableCell>
- </TableRow>
- ))
- )}
- </TableBody>
- </Table>
- </TableContainer>
- </Box>
-
- <Box
- sx={{
- p: 2,
- borderTop: 1,
- borderColor: 'divider',
- bgcolor: 'background.paper',
- }}
- >
- <Stack direction="row" justifyContent="flex-end" alignItems="center" gap={2}>
- <Autocomplete<PrinterCombo>
- options={filteredPrinters}
- value={selectedPrinter ?? null}
- onChange={(event, value) => {
- setSelectedPrinter(value ?? undefined);
- }}
- getOptionLabel={(option) => option.name || option.label || option.code || String(option.id)}
- renderInput={(params) => (
- <TextField
- {...params}
- variant="outlined"
- label="列印機"
- sx={{ width: 300 }}
- />
- )}
- />
- <TextField
- variant="outlined"
- label="列印數量"
- type="number"
- value={printQty}
- onChange={(e) => {
- const value = parseInt(e.target.value) || 1;
- setPrintQty(Math.max(1, value));
- }}
- inputProps={{ min: 1 }}
- sx={{ width: 120 }}
- />
- <Button
- variant="contained"
- startIcon={<PrintIcon />}
- onClick={handlePrint}
- disabled={checkboxIds.length === 0 || filteredPrinters.length === 0 || !selectedPrinter}
- color="primary"
- >
- 列印
- </Button>
- <Button
- variant="contained"
- startIcon={<DownloadIcon />}
- onClick={() => handleDownloadQrCode(checkboxIds)}
- disabled={checkboxIds.length === 0}
- color="primary"
- >
- 下載二維碼
- </Button>
- </Stack>
- </Box>
- </Card>
- </Modal>
-
- <Modal
- open={previewOpen}
- onClose={handleClosePreview}
- sx={{
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- }}
- >
- <Card
- sx={{
- position: 'relative',
- width: '90%',
- maxWidth: '900px',
- maxHeight: '90vh',
- display: 'flex',
- flexDirection: 'column',
- outline: 'none',
- }}
- >
- <Box
- sx={{
- display: 'flex',
- justifyContent: 'flex-end',
- alignItems: 'center',
- p: 2,
- borderBottom: 1,
- borderColor: 'divider',
- }}
- >
- <IconButton
- onClick={handleClosePreview}
- >
- <CloseIcon />
- </IconButton>
- </Box>
-
- <Box
- sx={{
- flex: 1,
- overflow: 'auto',
- p: 2,
- }}
- >
- {previewUrl && (
- <iframe
- src={previewUrl}
- width="100%"
- height="600px"
- style={{
- border: 'none',
- }}
- title="PDF Preview"
- />
- )}
- </Box>
- </Card>
- </Modal>
- </>
- );
- };
-
- export default QrCodeHandleEquipmentSearch;
|