diff --git a/src/app/(main)/settings/qrCodeHandle/page.tsx b/src/app/(main)/settings/qrCodeHandle/page.tsx index e0a84c7..d363561 100644 --- a/src/app/(main)/settings/qrCodeHandle/page.tsx +++ b/src/app/(main)/settings/qrCodeHandle/page.tsx @@ -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")} - + }> @@ -35,6 +36,13 @@ const QrCodeHandlePage: React.FC = async () => { } + warehouseTabContent={ + }> + + + + + } /> diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index 5007a80..ff20f0a 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -131,6 +131,21 @@ export interface getTicketReleaseTable { handlerName: string | null; numberOfFGItems: number; } + +export interface TruckScheduleDashboardItem { + storeId: string | null; + truckId: number | null; + truckLanceCode: string | null; + truckDepartureTime: string | number[] | null; + numberOfShopsToServe: number; + numberOfPickTickets: number; + totalItemsToPick: number; + numberOfTicketsReleased: number; + firstTicketStartTime: string | number[] | null; + numberOfTicketsCompleted: number; + lastTicketEndTime: string | number[] | null; + pickTimeTakenMinutes: number | null; +} export interface SearchDeliveryOrderInfoRequest { code: string; shopName: string; @@ -181,6 +196,15 @@ export const fetchTicketReleaseTable = cache(async (startDate: string, endDate: } ); }); + +export const fetchTruckScheduleDashboard = cache(async () => { + return await serverFetchJson( + `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`, + { + method: "GET", + } + ); +}); export const startBatchReleaseAsyncSingle = cache(async (data: { doId: number; userId: number }) => { const { doId, userId } = data; return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( diff --git a/src/app/api/do/client.ts b/src/app/api/do/client.ts new file mode 100644 index 0000000..8adddde --- /dev/null +++ b/src/app/api/do/client.ts @@ -0,0 +1,16 @@ +"use client"; + +import { + fetchTruckScheduleDashboard, + type TruckScheduleDashboardItem +} from "./actions"; + +export const fetchTruckScheduleDashboardClient = async (): Promise => { + return await fetchTruckScheduleDashboard(); +}; + +export type { TruckScheduleDashboardItem }; + +export default fetchTruckScheduleDashboardClient; + + diff --git a/src/app/api/warehouse/client.ts b/src/app/api/warehouse/client.ts new file mode 100644 index 0000000..454d48a --- /dev/null +++ b/src/app/api/warehouse/client.ts @@ -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 }; +}; diff --git a/src/components/CreateItem/CreateItem.tsx b/src/components/CreateItem/CreateItem.tsx index 69b8e9e..f4fd8e2 100644 --- a/src/components/CreateItem/CreateItem.tsx +++ b/src/components/CreateItem/CreateItem.tsx @@ -159,9 +159,8 @@ const CreateItem: React.FC = ({ console.log(qcCheck); // return // do api - console.log("asdad"); const responseI = await saveItem(data); - console.log("asdad"); + const responseQ = await saveItemQcChecks(qcCheck); if (responseI && responseQ) { if (!Boolean(responseI.id)) { diff --git a/src/components/DashboardPage/DashboardPage.tsx b/src/components/DashboardPage/DashboardPage.tsx index f2d0dad..d417a8a 100644 --- a/src/components/DashboardPage/DashboardPage.tsx +++ b/src/components/DashboardPage/DashboardPage.tsx @@ -17,6 +17,7 @@ import CollapsibleCard from "../CollapsibleCard"; // import SupervisorQcApproval, { IQCItems } from "./QC/SupervisorQcApproval"; import { EscalationResult } from "@/app/api/escalation"; import EscalationLogTable from "./escalation/EscalationLogTable"; +import { TruckScheduleDashboard } from "./truckSchedule"; type Props = { // iqc: IQCItems[] | undefined escalationLogs: EscalationResult[] @@ -42,6 +43,13 @@ const DashboardPage: React.FC = ({ return ( + + + + + + + { + const { t } = useTranslation("dashboard"); + const [selectedStore, setSelectedStore] = useState(""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + // Initialize as null to avoid SSR/client hydration mismatch + const [currentTime, setCurrentTime] = useState(null); + const [isClient, setIsClient] = useState(false); + const completedTrackerRef = useRef>(new Map()); + const refreshCountRef = useRef(0); + + // Set client flag and time on mount + useEffect(() => { + setIsClient(true); + setCurrentTime(dayjs()); + }, []); + + // Format time from array or string to HH:mm + const formatTime = (timeData: string | number[] | null): string => { + if (!timeData) return '-'; + + if (Array.isArray(timeData)) { + if (timeData.length >= 2) { + const hour = timeData[0] || 0; + const minute = timeData[1] || 0; + return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + } + return '-'; + } + + if (typeof timeData === 'string') { + const parts = timeData.split(':'); + if (parts.length >= 2) { + const hour = parseInt(parts[0], 10); + const minute = parseInt(parts[1], 10); + return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`; + } + } + + return '-'; + }; + + // Format datetime from array or string + const formatDateTime = (dateTimeData: string | number[] | null): string => { + if (!dateTimeData) return '-'; + + if (Array.isArray(dateTimeData)) { + return arrayToDayjs(dateTimeData, true).format('HH:mm'); + } + + const parsed = dayjs(dateTimeData); + if (parsed.isValid()) { + return parsed.format('HH:mm'); + } + + return '-'; + }; + + // Calculate time remaining for truck departure + const calculateTimeRemaining = useCallback((departureTime: string | number[] | null): string => { + if (!departureTime || !currentTime) return '-'; + + const now = currentTime; + let departureHour: number; + let departureMinute: number; + + if (Array.isArray(departureTime)) { + if (departureTime.length < 2) return '-'; + departureHour = departureTime[0] || 0; + departureMinute = departureTime[1] || 0; + } else if (typeof departureTime === 'string') { + const parts = departureTime.split(':'); + if (parts.length < 2) return '-'; + departureHour = parseInt(parts[0], 10); + departureMinute = parseInt(parts[1], 10); + } else { + return '-'; + } + + // Create departure datetime for today + const departure = now.clone().hour(departureHour).minute(departureMinute).second(0); + const diffMinutes = departure.diff(now, 'minute'); + + if (diffMinutes < 0) { + // Past departure time + const absDiff = Math.abs(diffMinutes); + const hours = Math.floor(absDiff / 60); + const minutes = absDiff % 60; + return `-${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; + } else { + const hours = Math.floor(diffMinutes / 60); + const minutes = diffMinutes % 60; + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; + } + }, [currentTime]); + + // Generate unique key for tracking completed items + const getItemKey = (item: TruckScheduleDashboardItem): string => { + return `${item.storeId}-${item.truckLanceCode}-${item.truckDepartureTime}`; + }; + + // Load data from API + const loadData = useCallback(async () => { + try { + const result = await fetchTruckScheduleDashboardClient(); + + // Update completed tracker + refreshCountRef.current += 1; + const currentRefresh = refreshCountRef.current; + + result.forEach(item => { + const key = getItemKey(item); + // If all tickets are completed, track it + if (item.numberOfPickTickets > 0 && item.numberOfTicketsCompleted >= item.numberOfPickTickets) { + const existing = completedTrackerRef.current.get(key); + if (!existing) { + completedTrackerRef.current.set(key, { key, refreshCount: currentRefresh }); + } + } else { + // Remove from tracker if no longer completed + completedTrackerRef.current.delete(key); + } + }); + + // Filter out items that have been completed for 2+ refresh cycles + const filteredResult = result.filter(item => { + const key = getItemKey(item); + const tracker = completedTrackerRef.current.get(key); + if (tracker) { + // Hide if completed for 2 or more refresh cycles + if (currentRefresh - tracker.refreshCount >= 2) { + return false; + } + } + return true; + }); + + setData(filteredResult); + } catch (error) { + console.error('Error fetching truck schedule dashboard:', error); + } finally { + setLoading(false); + } + }, []); + + // Initial load and auto-refresh every 5 minutes + useEffect(() => { + loadData(); + + const refreshInterval = setInterval(() => { + loadData(); + }, 5 * 60 * 1000); // 5 minutes + + return () => clearInterval(refreshInterval); + }, [loadData]); + + // Update current time every 1 minute for time remaining calculation + useEffect(() => { + if (!isClient) return; + + const timeInterval = setInterval(() => { + setCurrentTime(dayjs()); + }, 60 * 1000); // 1 minute + + return () => clearInterval(timeInterval); + }, [isClient]); + + // Filter data by selected store + const filteredData = useMemo(() => { + if (!selectedStore) return data; + return data.filter(item => item.storeId === selectedStore); + }, [data, selectedStore]); + + // Get chip color based on time remaining + const getTimeChipColor = (departureTime: string | number[] | null): "success" | "warning" | "error" | "default" => { + if (!departureTime || !currentTime) return "default"; + + const now = currentTime; + let departureHour: number; + let departureMinute: number; + + if (Array.isArray(departureTime)) { + if (departureTime.length < 2) return "default"; + departureHour = departureTime[0] || 0; + departureMinute = departureTime[1] || 0; + } else if (typeof departureTime === 'string') { + const parts = departureTime.split(':'); + if (parts.length < 2) return "default"; + departureHour = parseInt(parts[0], 10); + departureMinute = parseInt(parts[1], 10); + } else { + return "default"; + } + + const departure = now.clone().hour(departureHour).minute(departureMinute).second(0); + const diffMinutes = departure.diff(now, 'minute'); + + if (diffMinutes < 0) return "error"; // Past due + if (diffMinutes <= 30) return "warning"; // Within 30 minutes + return "success"; // More than 30 minutes + }; + + return ( + + + {/* Title */} + + {t("Truck Schedule Dashboard")} + + + {/* Filter */} + + + + {t("Store ID")} + + + + + + {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} + + + + {/* Table */} + + {loading ? ( + + + + ) : ( + + + + + {t("Store ID")} + {t("Truck Schedule")} + {t("Time Remaining")} + {t("No. of Shops")} + {t("Total Items")} + {t("Tickets Released")} + {t("First Ticket Start")} + {t("Tickets Completed")} + {t("Last Ticket End")} + {t("Pick Time (min)")} + + + + {filteredData.length === 0 ? ( + + + + {t("No truck schedules available for today")} + + + + ) : ( + filteredData.map((row, index) => { + const timeRemaining = calculateTimeRemaining(row.truckDepartureTime); + const chipColor = getTimeChipColor(row.truckDepartureTime); + + return ( + 0 && row.numberOfTicketsCompleted >= row.numberOfPickTickets + ? 'success.light' + : 'inherit' + }} + > + + + + + + + {row.truckLanceCode || '-'} + + + ETD: {formatTime(row.truckDepartureTime)} + + + + + + + + + {row.numberOfShopsToServe} [{row.numberOfPickTickets}] + + + + + {row.totalItemsToPick} + + + + 0 ? 'info' : 'default'} + /> + + + {formatDateTime(row.firstTicketStartTime)} + + + 0 ? 'success' : 'default'} + /> + + + {formatDateTime(row.lastTicketEndTime)} + + + + {row.pickTimeTakenMinutes !== null ? row.pickTimeTakenMinutes : '-'} + + + + ); + }) + )} + +
+
+ )} +
+
+
+ ); +}; + +export default TruckScheduleDashboard; diff --git a/src/components/DashboardPage/truckSchedule/index.ts b/src/components/DashboardPage/truckSchedule/index.ts new file mode 100644 index 0000000..b3609a6 --- /dev/null +++ b/src/components/DashboardPage/truckSchedule/index.ts @@ -0,0 +1,3 @@ +export { default as TruckScheduleDashboard } from './TruckScheduleDashboard'; + + diff --git a/src/components/Shop/TruckLaneDetail.tsx b/src/components/Shop/TruckLaneDetail.tsx index 21b5536..e8eef0f 100644 --- a/src/components/Shop/TruckLaneDetail.tsx +++ b/src/components/Shop/TruckLaneDetail.tsx @@ -69,6 +69,7 @@ const TruckLaneDetail: React.FC = () => { const [uniqueRemarks, setUniqueRemarks] = useState([]); const [uniqueShopCodes, setUniqueShopCodes] = useState([]); const [uniqueShopNames, setUniqueShopNames] = useState([]); + const [shopNameByCodeMap, setShopNameByCodeMap] = useState>(new Map()); const [addShopDialogOpen, setAddShopDialogOpen] = useState(false); const [newShop, setNewShop] = useState({ shopName: "", @@ -86,11 +87,12 @@ const TruckLaneDetail: React.FC = () => { useEffect(() => { const fetchAutocompleteData = async () => { try { - const [shopData, remarks, codes, names] = await Promise.all([ + const [shopData, remarks, codes, names, allShopsFromShopTable] = await Promise.all([ findAllUniqueShopNamesAndCodesFromTrucksClient() as Promise>, findAllUniqueRemarksFromTrucksClient() as Promise, findAllUniqueShopCodesFromTrucksClient() as Promise, findAllUniqueShopNamesFromTrucksClient() as Promise, + fetchAllShopsClient() as Promise, ]); // Convert to Shop format (id will be 0 since we don't have shop IDs from truck table) @@ -105,6 +107,15 @@ const TruckLaneDetail: React.FC = () => { setUniqueRemarks(remarks || []); setUniqueShopCodes(codes || []); setUniqueShopNames(names || []); + + // Create lookup map: shopCode -> shopName from shop table + const shopNameMap = new Map(); + (allShopsFromShopTable || []).forEach((shop) => { + if (shop.code) { + shopNameMap.set(String(shop.code).trim().toLowerCase(), String(shop.name || "").trim()); + } + }); + setShopNameByCodeMap(shopNameMap); } catch (err) { console.error("Failed to load autocomplete data:", err); } @@ -700,6 +711,7 @@ const TruckLaneDetail: React.FC = () => { {t("Shop Name")} + {t("Shop Branch")} {t("Shop Code")} {t("Remark")} {t("Loading Sequence")} @@ -709,7 +721,7 @@ const TruckLaneDetail: React.FC = () => { {shopsData.length === 0 ? ( - + {t("No shops found using this truck lane")} @@ -719,6 +731,14 @@ const TruckLaneDetail: React.FC = () => { shopsData.map((shop, index) => ( + {/* Shop Name from shop table (read-only, looked up by shop code) */} + {(() => { + const shopCode = String(shop.code || "").trim().toLowerCase(); + return shopNameByCodeMap.get(shopCode) || "-"; + })()} + + + {/* Shop Branch from truck table (editable) */} {editingRowIndex === index ? ( = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 = ({ warehouses }) => { justifyContent: "flex-start", }} > - {/* 樓層 field with F inside on the right */} = ({ warehouses }) => { - - {/* 倉庫 field */} = ({ warehouses }) => { - - {/* 區域 field */} = ({ warehouses }) => { - - {/* 儲位 field */} = ({ warehouses }) => { size="small" sx={{ width: "150px", minWidth: "120px" }} /> - {/* 盤點區域 field */} = ({ 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 = ({ 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 = ({ 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 = ({ + @@ -83,6 +92,10 @@ const QrCodeHandleTabs: React.FC = ({ {equipmentTabContent} + + + {warehouseTabContent} + ); }; diff --git a/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx b/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx new file mode 100644 index 0000000..7bed7ec --- /dev/null +++ b/src/components/qrCodeHandles/qrCodeHandleWarehouseSearch.tsx @@ -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 = ({ 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([]); + const [selectAll, setSelectAll] = useState(false); + const [printQty, setPrintQty] = useState(1); + const [isSearching, setIsSearching] = useState(false); + + const [previewOpen, setPreviewOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(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( + 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[]>( + () => [ + { + name: "id", + label: "", + sx: { width: "50px", minWidth: "50px" }, + renderCell: (params) => ( + 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 ( + <> + + + {t("Search Criteria")} + + + setSearchInputs((prev) => ({ ...prev, store_id: e.target.value })) + } + size="small" + sx={{ width: "150px", minWidth: "120px" }} + InputProps={{ + endAdornment: ( + F + ), + }} + /> + + - + + + setSearchInputs((prev) => ({ ...prev, warehouse: e.target.value })) + } + size="small" + sx={{ width: "150px", minWidth: "120px" }} + /> + + - + + + setSearchInputs((prev) => ({ ...prev, area: e.target.value })) + } + size="small" + sx={{ width: "150px", minWidth: "120px" }} + /> + + - + + + setSearchInputs((prev) => ({ ...prev, slot: e.target.value })) + } + size="small" + sx={{ width: "150px", minWidth: "120px" }} + /> + + + + + + + + + items={filteredWarehouses} + columns={columns} + pagingController={pagingController} + setPagingController={setPagingController} + totalCount={filteredWarehouses.length} + isAutoPaging={true} + /> + + + + + + + + + + 已選擇倉庫 ({selectedWarehouses.length}) + + + + + + + + + + + + + {t("code")} + + + {t("store_id")} + + + {t("warehouse")} + + + {t("area")} + + + {t("slot")} + + + + + {selectedWarehouses.length === 0 ? ( + + + 沒有選擇的倉庫 + + + ) : ( + selectedWarehouses.map((warehouse) => ( + + {warehouse.code || '-'} + {warehouse.store_id || '-'} + {warehouse.warehouse || '-'} + {warehouse.area || '-'} + {warehouse.slot || '-'} + + )) + )} + +
+
+
+ + + + + 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) => ( + + )} + /> + { + const value = parseInt(e.target.value) || 1; + setPrintQty(Math.max(1, value)); + }} + inputProps={{ min: 1 }} + sx={{ width: 120 }} + /> + + + + +
+
+ + + + + + + + + + + {previewUrl && ( +