Bläddra i källkod

Supporting Function: Warehouse QR Code Printing

master
B.E.N.S.O.N 3 veckor sedan
förälder
incheckning
d0a0736f0d
6 ändrade filer med 752 tillägg och 22 borttagningar
  1. +9
    -1
      src/app/(main)/settings/qrCodeHandle/page.tsx
  2. +33
    -0
      src/app/api/warehouse/client.ts
  3. +0
    -20
      src/components/WarehouseHandle/WarehouseHandle.tsx
  4. +14
    -1
      src/components/qrCodeHandles/qrCodeHandleTabs.tsx
  5. +675
    -0
      src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx
  6. +21
    -0
      src/components/qrCodeHandles/qrCodeHandleWarehouseSearchWrapper.tsx

+ 9
- 1
src/app/(main)/settings/qrCodeHandle/page.tsx Visa fil

@@ -4,6 +4,7 @@ import Typography from "@mui/material/Typography";
import { getServerI18n } from "@/i18n";
import QrCodeHandleSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleSearchWrapper";
import QrCodeHandleEquipmentSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper";
import QrCodeHandleWarehouseSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleWarehouseSearchWrapper";
import QrCodeHandleTabs from "@/components/qrCodeHandles/qrCodeHandleTabs";
import { I18nProvider } from "@/i18n";
import Box from "@mui/material/Box";
@@ -19,7 +20,7 @@ const QrCodeHandlePage: React.FC = async () => {
{t("QR Code Handle")}
</Typography>
<I18nProvider namespaces={["common", "user"]}>
<I18nProvider namespaces={["common", "user", "warehouse"]}>
<QrCodeHandleTabs
userTabContent={
<Suspense fallback={<QrCodeHandleSearchWrapper.Loading />}>
@@ -35,6 +36,13 @@ const QrCodeHandlePage: React.FC = async () => {
</I18nProvider>
</Suspense>
}
warehouseTabContent={
<Suspense fallback={<QrCodeHandleWarehouseSearchWrapper.Loading />}>
<I18nProvider namespaces={["warehouse", "common", "dashboard"]}>
<QrCodeHandleWarehouseSearchWrapper />
</I18nProvider>
</Suspense>
}
/>
</I18nProvider>
</Box>


+ 33
- 0
src/app/api/warehouse/client.ts Visa fil

@@ -0,0 +1,33 @@
"use client";

import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { WarehouseResult } from "./index";

export const exportWarehouseQrCode = async (warehouseIds: number[]): Promise<{ blobValue: Uint8Array; filename: string }> => {

const token = localStorage.getItem("accessToken");
const response = await fetch(`${NEXT_PUBLIC_API_URL}/warehouse/export-qrcode`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token && { Authorization: `Bearer ${token}` }),
},
body: JSON.stringify({ warehouseIds }),
});

if (!response.ok) {
if (response.status === 401) {
throw new Error("Unauthorized: Please log in again");
}
throw new Error(`Failed to export QR code: ${response.status} ${response.statusText}`);
}

const filename = response.headers.get("Content-Disposition")?.split("filename=")[1]?.replace(/"/g, "") || "warehouse_qrcode.pdf";
const blob = await response.blob();
const arrayBuffer = await blob.arrayBuffer();
const blobValue = new Uint8Array(arrayBuffer);

return { blobValue, filename };
};

+ 0
- 20
src/components/WarehouseHandle/WarehouseHandle.tsx Visa fil

@@ -53,8 +53,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
successDialog(t("Delete Success"), t);
} catch (error) {
console.error("Failed to delete warehouse:", error);
// Don't redirect on error, just show error message
// The error will be logged but user stays on the page
}
}, t);
}, [t, router]);
@@ -76,18 +74,14 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
try {
let results: WarehouseResult[] = warehouses;

// Build search pattern from the four fields: store_idF-warehouse-area-slot
// Only search by code field - match the code that follows this pattern
const storeId = searchInputs.store_id?.trim() || "";
const warehouse = searchInputs.warehouse?.trim() || "";
const area = searchInputs.area?.trim() || "";
const slot = searchInputs.slot?.trim() || "";
const stockTakeSection = searchInputs.stockTakeSection?.trim() || "";

// If any field has a value, filter by code pattern and stockTakeSection
if (storeId || warehouse || area || slot || stockTakeSection) {
results = warehouses.filter((warehouseItem) => {
// Filter by stockTakeSection if provided
if (stockTakeSection) {
const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase();
if (!itemStockTakeSection.includes(stockTakeSection.toLowerCase())) {
@@ -95,7 +89,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
}
}
// Filter by code pattern if any code-related field is provided
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
@@ -103,8 +96,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
const codeValue = String(warehouseItem.code).toLowerCase();
// Check if code matches the pattern: store_id-warehouse-area-slot
// Match each part if provided
const codeParts = codeValue.split("-");
if (codeParts.length >= 4) {
@@ -121,7 +112,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
// Fallback: if code doesn't follow the pattern, check if it contains any of the search terms
const storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase());
const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase());
const areaMatch = !area || codeValue.includes(area.toLowerCase());
@@ -130,11 +120,9 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
// If only stockTakeSection is provided, return true (already filtered above)
return true;
});
} else {
// If no search terms, show all warehouses
results = warehouses;
}

@@ -142,7 +130,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
} catch (error) {
console.error("Error searching warehouses:", error);
// Fallback: filter by code pattern and stockTakeSection
const storeId = searchInputs.store_id?.trim().toLowerCase() || "";
const warehouse = searchInputs.warehouse?.trim().toLowerCase() || "";
const area = searchInputs.area?.trim().toLowerCase() || "";
@@ -151,7 +138,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
setFilteredWarehouse(
warehouses.filter((warehouseItem) => {
// Filter by stockTakeSection if provided
if (stockTakeSection) {
const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase();
if (!itemStockTakeSection.includes(stockTakeSection)) {
@@ -159,7 +145,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
}
}
// Filter by code if any code-related field is provided
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
@@ -267,7 +252,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
justifyContent: "flex-start",
}}
>
{/* 樓層 field with F inside on the right */}
<TextField
label={t("store_id")}
value={searchInputs.store_id}
@@ -285,7 +269,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
{/* 倉庫 field */}
<TextField
label={t("warehouse")}
value={searchInputs.warehouse}
@@ -298,7 +281,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
{/* 區域 field */}
<TextField
label={t("area")}
value={searchInputs.area}
@@ -311,7 +293,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
{/* 儲位 field */}
<TextField
label={t("slot")}
value={searchInputs.slot}
@@ -321,7 +302,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => {
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
{/* 盤點區域 field */}
<Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}>
<TextField
label={t("stockTakeSection")}


+ 14
- 1
src/components/qrCodeHandles/qrCodeHandleTabs.tsx Visa fil

@@ -30,20 +30,24 @@ function TabPanel(props: TabPanelProps) {
interface QrCodeHandleTabsProps {
userTabContent: ReactNode;
equipmentTabContent: ReactNode;
warehouseTabContent: ReactNode;
}

const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
userTabContent,
equipmentTabContent,
warehouseTabContent,
}) => {
const { t } = useTranslation("common");
const { t: tUser } = useTranslation("user");
const { t: tWarehouse } = useTranslation("warehouse");
const searchParams = useSearchParams();
const router = useRouter();
const getInitialTab = () => {
const tab = searchParams.get("tab");
if (tab === "equipment") return 1;
if (tab === "warehouse") return 2;
if (tab === "user") return 0;
return 0;
};
@@ -54,6 +58,8 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
const tab = searchParams.get("tab");
if (tab === "equipment") {
setCurrentTab(1);
} else if (tab === "warehouse") {
setCurrentTab(2);
} else if (tab === "user") {
setCurrentTab(0);
}
@@ -61,7 +67,9 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({

const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setCurrentTab(newValue);
const tabName = newValue === 1 ? "equipment" : "user";
let tabName = "user";
if (newValue === 1) tabName = "equipment";
else if (newValue === 2) tabName = "warehouse";
const params = new URLSearchParams(searchParams.toString());
params.set("tab", tabName);
router.push(`?${params.toString()}`, { scroll: false });
@@ -73,6 +81,7 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
<Tabs value={currentTab} onChange={handleTabChange}>
<Tab label={tUser("User")} />
<Tab label={t("Equipment")} />
<Tab label={tWarehouse("Warehouse")} />
</Tabs>
</Box>

@@ -83,6 +92,10 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({
<TabPanel value={currentTab} index={1}>
{equipmentTabContent}
</TabPanel>

<TabPanel value={currentTab} index={2}>
{warehouseTabContent}
</TabPanel>
</Box>
);
};


+ 675
- 0
src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx Visa fil

@@ -0,0 +1,675 @@
"use client";

import { useCallback, useMemo, useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import { successDialog } from "../Swal/CustomAlerts";
import useUploadContext from "../UploadProvider/useUploadContext";
import { downloadFile } from "@/app/utils/commonUtil";
import { WarehouseResult } from "@/app/api/warehouse";
import { exportWarehouseQrCode } from "@/app/api/warehouse/client";
import {
Checkbox,
Box,
Button,
TextField,
Stack,
Autocomplete,
Modal,
Card,
CardContent,
CardActions,
IconButton,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
Typography,
InputAdornment
} 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 RestartAlt from "@mui/icons-material/RestartAlt";
import Search from "@mui/icons-material/Search";
import { PrinterCombo } from "@/app/api/settings/printer";

interface Props {
warehouses: WarehouseResult[];
printerCombo: PrinterCombo[];
}

const QrCodeHandleWarehouseSearch: React.FC<Props> = ({ warehouses, printerCombo }) => {
const { t } = useTranslation(["warehouse", "common"]);
const [filteredWarehouses, setFilteredWarehouses] = useState(warehouses);
const { setIsUploading } = useUploadContext();
const [pagingController, setPagingController] = useState({
pageNum: 1,
pageSize: 10,
});

const [checkboxIds, setCheckboxIds] = useState<number[]>([]);
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 [selectedWarehousesModalOpen, setSelectedWarehousesModalOpen] = useState(false);

const [searchInputs, setSearchInputs] = useState({
store_id: "",
warehouse: "",
area: "",
slot: "",
});

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 handleReset = useCallback(() => {
setSearchInputs({
store_id: "",
warehouse: "",
area: "",
slot: "",
});
setFilteredWarehouses(warehouses);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
}, [warehouses, pagingController.pageSize]);

const handleSearch = useCallback(() => {
setIsSearching(true);
try {
let results: WarehouseResult[] = warehouses;

const storeId = searchInputs.store_id?.trim() || "";
const warehouse = searchInputs.warehouse?.trim() || "";
const area = searchInputs.area?.trim() || "";
const slot = searchInputs.slot?.trim() || "";

if (storeId || warehouse || area || slot) {
results = warehouses.filter((warehouseItem) => {
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
}
const codeValue = String(warehouseItem.code).toLowerCase();
const codeParts = codeValue.split("-");
if (codeParts.length >= 4) {
const codeStoreId = codeParts[0] || "";
const codeWarehouse = codeParts[1] || "";
const codeArea = codeParts[2] || "";
const codeSlot = codeParts[3] || "";
const storeIdMatch = !storeId || codeStoreId.includes(storeId.toLowerCase());
const warehouseMatch = !warehouse || codeWarehouse.includes(warehouse.toLowerCase());
const areaMatch = !area || codeArea.includes(area.toLowerCase());
const slotMatch = !slot || codeSlot.includes(slot.toLowerCase());
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
const storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase());
const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase());
const areaMatch = !area || codeValue.includes(area.toLowerCase());
const slotMatch = !slot || codeValue.includes(slot.toLowerCase());
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
return true;
});
} else {
results = warehouses;
}

setFilteredWarehouses(results);
setPagingController({ pageNum: 1, pageSize: pagingController.pageSize });
} catch (error) {
console.error("Error searching warehouses:", error);
const storeId = searchInputs.store_id?.trim().toLowerCase() || "";
const warehouse = searchInputs.warehouse?.trim().toLowerCase() || "";
const area = searchInputs.area?.trim().toLowerCase() || "";
const slot = searchInputs.slot?.trim().toLowerCase() || "";
setFilteredWarehouses(
warehouses.filter((warehouseItem) => {
if (storeId || warehouse || area || slot) {
if (!warehouseItem.code) {
return false;
}
const codeValue = String(warehouseItem.code).toLowerCase();
const codeParts = codeValue.split("-");
if (codeParts.length >= 4) {
const storeIdMatch = !storeId || codeParts[0].includes(storeId);
const warehouseMatch = !warehouse || codeParts[1].includes(warehouse);
const areaMatch = !area || codeParts[2].includes(area);
const slotMatch = !slot || codeParts[3].includes(slot);
return storeIdMatch && warehouseMatch && areaMatch && slotMatch;
}
return (!storeId || codeValue.includes(storeId)) &&
(!warehouse || codeValue.includes(warehouse)) &&
(!area || codeValue.includes(area)) &&
(!slot || codeValue.includes(slot));
}
return true;
})
);
} finally {
setIsSearching(false);
}
}, [searchInputs, warehouses, pagingController.pageSize]);

const handleSelectWarehouse = useCallback((warehouseId: number, checked: boolean) => {
if (checked) {
setCheckboxIds(prev => [...prev, warehouseId]);
} else {
setCheckboxIds(prev => prev.filter(id => id !== warehouseId));
setSelectAll(false);
}
}, []);

const handleSelectAll = useCallback((checked: boolean) => {
if (checked) {
setCheckboxIds(filteredWarehouses.map(warehouse => warehouse.id));
setSelectAll(true);
} else {
setCheckboxIds([]);
setSelectAll(false);
}
}, [filteredWarehouses]);

const showPdfPreview = useCallback(async (warehouseIds: number[]) => {
if (warehouseIds.length === 0) {
return;
}
try {
setIsUploading(true);
const response = await exportWarehouseQrCode(warehouseIds);
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 (warehouseIds: number[]) => {
if (warehouseIds.length === 0) {
return;
}
try {
setIsUploading(true);
const response = await exportWarehouseQrCode(warehouseIds);
downloadFile(response.blobValue, response.filename);
setSelectedWarehousesModalOpen(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) {
return;
}
try {
setIsUploading(true);
const response = await exportWarehouseQrCode(checkboxIds);
const blob = new Blob([new Uint8Array(response.blobValue)], { type: "application/pdf" });
const url = URL.createObjectURL(blob);
const printWindow = window.open(url, '_blank');
if (printWindow) {
printWindow.onload = () => {
for (let i = 0; i < printQty; i++) {
setTimeout(() => {
printWindow.print();
}, i * 500);
}
};
}
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
setSelectedWarehousesModalOpen(false);
successDialog("二維碼已列印", t);
} catch (error) {
console.error("Error printing QR code:", error);
} finally {
setIsUploading(false);
}
}, [checkboxIds, printQty, setIsUploading, t]);

const handleViewSelectedQrCodes = useCallback(() => {
if (checkboxIds.length === 0) {
return;
}
setSelectedWarehousesModalOpen(true);
}, [checkboxIds]);

const selectedWarehouses = useMemo(() => {
return warehouses.filter(warehouse => checkboxIds.includes(warehouse.id));
}, [warehouses, checkboxIds]);

const handleCloseSelectedWarehousesModal = useCallback(() => {
setSelectedWarehousesModalOpen(false);
}, []);

const columns = useMemo<Column<WarehouseResult>[]>(
() => [
{
name: "id",
label: "",
sx: { width: "50px", minWidth: "50px" },
renderCell: (params) => (
<Checkbox
checked={checkboxIds.includes(params.id)}
onChange={(e) => handleSelectWarehouse(params.id, e.target.checked)}
onClick={(e) => e.stopPropagation()}
/>
),
},
{
name: "code",
label: t("code"),
align: "left",
headerAlign: "left",
sx: { width: "200px", minWidth: "200px" },
},
{
name: "store_id",
label: t("store_id"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
},
{
name: "warehouse",
label: t("warehouse"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
},
{
name: "area",
label: t("area"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
},
{
name: "slot",
label: t("slot"),
align: "left",
headerAlign: "left",
sx: { width: "150px", minWidth: "150px" },
},
],
[t, checkboxIds, handleSelectWarehouse],
);

return (
<>
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
flexWrap: "nowrap",
justifyContent: "flex-start",
}}
>
<TextField
label={t("store_id")}
value={searchInputs.store_id}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, store_id: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
InputProps={{
endAdornment: (
<InputAdornment position="end">F</InputAdornment>
),
}}
/>
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
<TextField
label={t("warehouse")}
value={searchInputs.warehouse}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, warehouse: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
<TextField
label={t("area")}
value={searchInputs.area}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, area: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
<Typography variant="body1" sx={{ mx: 0.5 }}>
-
</Typography>
<TextField
label={t("slot")}
value={searchInputs.slot}
onChange={(e) =>
setSearchInputs((prev) => ({ ...prev, slot: e.target.value }))
}
size="small"
sx={{ width: "150px", minWidth: "120px" }}
/>
</Box>
<CardActions sx={{ justifyContent: "flex-start", px: 0, pt: 1 }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleSearch}
>
{t("Search")}
</Button>
</CardActions>
</CardContent>
</Card>
<SearchResults<WarehouseResult>
items={filteredWarehouses}
columns={columns}
pagingController={pagingController}
setPagingController={setPagingController}
totalCount={filteredWarehouses.length}
isAutoPaging={true}
/>
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
<Button
variant="outlined"
onClick={() => handleSelectAll(!selectAll)}
startIcon={<Checkbox checked={selectAll} />}
>
選擇全部倉庫 ({checkboxIds.length} / {filteredWarehouses.length})
</Button>
<Button
variant="contained"
onClick={handleViewSelectedQrCodes}
disabled={checkboxIds.length === 0}
color="primary"
>
查看已選擇倉庫二維碼 ({checkboxIds.length})
</Button>
</Box>

<Modal
open={selectedWarehousesModalOpen}
onClose={handleCloseSelectedWarehousesModal}
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">
已選擇倉庫 ({selectedWarehouses.length})
</Typography>
<IconButton onClick={handleCloseSelectedWarehousesModal}>
<CloseIcon />
</IconButton>
</Box>

<Box
sx={{
flex: 1,
overflow: 'auto',
p: 2,
}}
>
<TableContainer component={Paper} variant="outlined">
<Table>
<TableHead>
<TableRow>
<TableCell>
<strong>{t("code")}</strong>
</TableCell>
<TableCell>
<strong>{t("store_id")}</strong>
</TableCell>
<TableCell>
<strong>{t("warehouse")}</strong>
</TableCell>
<TableCell>
<strong>{t("area")}</strong>
</TableCell>
<TableCell>
<strong>{t("slot")}</strong>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{selectedWarehouses.length === 0 ? (
<TableRow>
<TableCell colSpan={5} align="center">
沒有選擇的倉庫
</TableCell>
</TableRow>
) : (
selectedWarehouses.map((warehouse) => (
<TableRow key={warehouse.id}>
<TableCell>{warehouse.code || '-'}</TableCell>
<TableCell>{warehouse.store_id || '-'}</TableCell>
<TableCell>{warehouse.warehouse || '-'}</TableCell>
<TableCell>{warehouse.area || '-'}</TableCell>
<TableCell>{warehouse.slot || '-'}</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)}
isOptionEqualToValue={(option, value) => option.id === value.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}
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 QrCodeHandleWarehouseSearch;

+ 21
- 0
src/components/qrCodeHandles/qrCodeHandleWarehouseSearchWrapper.tsx Visa fil

@@ -0,0 +1,21 @@
import React from "react";
import QrCodeHandleWarehouseSearch from "./qrCodeHandleWarehouseSearch";
import QrCodeHandleSearchLoading from "./qrCodeHandleSearchLoading";
import { fetchWarehouseList } from "@/app/api/warehouse";
import { fetchPrinterCombo } from "@/app/api/settings/printer";

interface SubComponents {
Loading: typeof QrCodeHandleSearchLoading;
}

const QrCodeHandleWarehouseSearchWrapper: React.FC & SubComponents = async () => {
const [warehouses, printerCombo] = await Promise.all([
fetchWarehouseList(),
fetchPrinterCombo(),
]);
return <QrCodeHandleWarehouseSearch warehouses={warehouses} printerCombo={printerCombo} />;
};

QrCodeHandleWarehouseSearchWrapper.Loading = QrCodeHandleSearchLoading;

export default QrCodeHandleWarehouseSearchWrapper;

Laddar…
Avbryt
Spara