| @@ -4,6 +4,7 @@ import Typography from "@mui/material/Typography"; | |||||
| import { getServerI18n } from "@/i18n"; | import { getServerI18n } from "@/i18n"; | ||||
| import QrCodeHandleSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleSearchWrapper"; | import QrCodeHandleSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleSearchWrapper"; | ||||
| import QrCodeHandleEquipmentSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper"; | import QrCodeHandleEquipmentSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleEquipmentSearchWrapper"; | ||||
| import QrCodeHandleWarehouseSearchWrapper from "@/components/qrCodeHandles/qrCodeHandleWarehouseSearchWrapper"; | |||||
| import QrCodeHandleTabs from "@/components/qrCodeHandles/qrCodeHandleTabs"; | import QrCodeHandleTabs from "@/components/qrCodeHandles/qrCodeHandleTabs"; | ||||
| import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
| import Box from "@mui/material/Box"; | import Box from "@mui/material/Box"; | ||||
| @@ -19,7 +20,7 @@ const QrCodeHandlePage: React.FC = async () => { | |||||
| {t("QR Code Handle")} | {t("QR Code Handle")} | ||||
| </Typography> | </Typography> | ||||
| <I18nProvider namespaces={["common", "user"]}> | |||||
| <I18nProvider namespaces={["common", "user", "warehouse"]}> | |||||
| <QrCodeHandleTabs | <QrCodeHandleTabs | ||||
| userTabContent={ | userTabContent={ | ||||
| <Suspense fallback={<QrCodeHandleSearchWrapper.Loading />}> | <Suspense fallback={<QrCodeHandleSearchWrapper.Loading />}> | ||||
| @@ -35,6 +36,13 @@ const QrCodeHandlePage: React.FC = async () => { | |||||
| </I18nProvider> | </I18nProvider> | ||||
| </Suspense> | </Suspense> | ||||
| } | } | ||||
| warehouseTabContent={ | |||||
| <Suspense fallback={<QrCodeHandleWarehouseSearchWrapper.Loading />}> | |||||
| <I18nProvider namespaces={["warehouse", "common", "dashboard"]}> | |||||
| <QrCodeHandleWarehouseSearchWrapper /> | |||||
| </I18nProvider> | |||||
| </Suspense> | |||||
| } | |||||
| /> | /> | ||||
| </I18nProvider> | </I18nProvider> | ||||
| </Box> | </Box> | ||||
| @@ -131,6 +131,21 @@ export interface getTicketReleaseTable { | |||||
| handlerName: string | null; | handlerName: string | null; | ||||
| numberOfFGItems: number; | 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 { | export interface SearchDeliveryOrderInfoRequest { | ||||
| code: string; | code: string; | ||||
| shopName: string; | shopName: string; | ||||
| @@ -181,6 +196,15 @@ export const fetchTicketReleaseTable = cache(async (startDate: string, endDate: | |||||
| } | } | ||||
| ); | ); | ||||
| }); | }); | ||||
| export const fetchTruckScheduleDashboard = cache(async () => { | |||||
| return await serverFetchJson<TruckScheduleDashboardItem[]>( | |||||
| `${BASE_API_URL}/doPickOrder/truck-schedule-dashboard`, | |||||
| { | |||||
| method: "GET", | |||||
| } | |||||
| ); | |||||
| }); | |||||
| export const startBatchReleaseAsyncSingle = cache(async (data: { doId: number; userId: number }) => { | export const startBatchReleaseAsyncSingle = cache(async (data: { doId: number; userId: number }) => { | ||||
| const { doId, userId } = data; | const { doId, userId } = data; | ||||
| return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( | return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( | ||||
| @@ -0,0 +1,16 @@ | |||||
| "use client"; | |||||
| import { | |||||
| fetchTruckScheduleDashboard, | |||||
| type TruckScheduleDashboardItem | |||||
| } from "./actions"; | |||||
| export const fetchTruckScheduleDashboardClient = async (): Promise<TruckScheduleDashboardItem[]> => { | |||||
| return await fetchTruckScheduleDashboard(); | |||||
| }; | |||||
| export type { TruckScheduleDashboardItem }; | |||||
| export default fetchTruckScheduleDashboardClient; | |||||
| @@ -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 }; | |||||
| }; | |||||
| @@ -159,9 +159,8 @@ const CreateItem: React.FC<Props> = ({ | |||||
| console.log(qcCheck); | console.log(qcCheck); | ||||
| // return | // return | ||||
| // do api | // do api | ||||
| console.log("asdad"); | |||||
| const responseI = await saveItem(data); | const responseI = await saveItem(data); | ||||
| console.log("asdad"); | |||||
| const responseQ = await saveItemQcChecks(qcCheck); | const responseQ = await saveItemQcChecks(qcCheck); | ||||
| if (responseI && responseQ) { | if (responseI && responseQ) { | ||||
| if (!Boolean(responseI.id)) { | if (!Boolean(responseI.id)) { | ||||
| @@ -17,6 +17,7 @@ import CollapsibleCard from "../CollapsibleCard"; | |||||
| // import SupervisorQcApproval, { IQCItems } from "./QC/SupervisorQcApproval"; | // import SupervisorQcApproval, { IQCItems } from "./QC/SupervisorQcApproval"; | ||||
| import { EscalationResult } from "@/app/api/escalation"; | import { EscalationResult } from "@/app/api/escalation"; | ||||
| import EscalationLogTable from "./escalation/EscalationLogTable"; | import EscalationLogTable from "./escalation/EscalationLogTable"; | ||||
| import { TruckScheduleDashboard } from "./truckSchedule"; | |||||
| type Props = { | type Props = { | ||||
| // iqc: IQCItems[] | undefined | // iqc: IQCItems[] | undefined | ||||
| escalationLogs: EscalationResult[] | escalationLogs: EscalationResult[] | ||||
| @@ -42,6 +43,13 @@ const DashboardPage: React.FC<Props> = ({ | |||||
| return ( | return ( | ||||
| <ThemeProvider theme={theme}> | <ThemeProvider theme={theme}> | ||||
| <Grid container spacing={2}> | <Grid container spacing={2}> | ||||
| <Grid item xs={12}> | |||||
| <CollapsibleCard title={t("Truck Schedule Dashboard")} defaultOpen={true}> | |||||
| <CardContent> | |||||
| <TruckScheduleDashboard /> | |||||
| </CardContent> | |||||
| </CollapsibleCard> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <CollapsibleCard | <CollapsibleCard | ||||
| title={`${t("Responsible Escalation List")} (${t("pending")} : ${ | title={`${t("Responsible Escalation List")} (${t("pending")} : ${ | ||||
| @@ -0,0 +1,397 @@ | |||||
| "use client"; | |||||
| import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; | |||||
| import { | |||||
| Box, | |||||
| Typography, | |||||
| FormControl, | |||||
| InputLabel, | |||||
| Select, | |||||
| MenuItem, | |||||
| Card, | |||||
| CardContent, | |||||
| Stack, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| CircularProgress, | |||||
| Chip | |||||
| } from '@mui/material'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import dayjs from 'dayjs'; | |||||
| import { fetchTruckScheduleDashboardClient, type TruckScheduleDashboardItem } from '@/app/api/do/client'; | |||||
| import { formatDepartureTime, arrayToDayjs } from '@/app/utils/formatUtil'; | |||||
| // Track completed items for hiding after 2 refresh cycles | |||||
| interface CompletedTracker { | |||||
| key: string; | |||||
| refreshCount: number; | |||||
| } | |||||
| const TruckScheduleDashboard: React.FC = () => { | |||||
| const { t } = useTranslation("dashboard"); | |||||
| const [selectedStore, setSelectedStore] = useState<string>(""); | |||||
| const [data, setData] = useState<TruckScheduleDashboardItem[]>([]); | |||||
| const [loading, setLoading] = useState<boolean>(true); | |||||
| // Initialize as null to avoid SSR/client hydration mismatch | |||||
| const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null); | |||||
| const [isClient, setIsClient] = useState<boolean>(false); | |||||
| const completedTrackerRef = useRef<Map<string, CompletedTracker>>(new Map()); | |||||
| const refreshCountRef = useRef<number>(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 ( | |||||
| <Card sx={{ mb: 2 }}> | |||||
| <CardContent> | |||||
| {/* Title */} | |||||
| <Typography variant="h5" sx={{ mb: 3, fontWeight: 600 }}> | |||||
| {t("Truck Schedule Dashboard")} | |||||
| </Typography> | |||||
| {/* Filter */} | |||||
| <Stack direction="row" spacing={2} sx={{ mb: 3 }}> | |||||
| <FormControl sx={{ minWidth: 150 }} size="small"> | |||||
| <InputLabel id="store-select-label" shrink={true}> | |||||
| {t("Store ID")} | |||||
| </InputLabel> | |||||
| <Select | |||||
| labelId="store-select-label" | |||||
| id="store-select" | |||||
| value={selectedStore} | |||||
| label={t("Store ID")} | |||||
| onChange={(e) => setSelectedStore(e.target.value)} | |||||
| displayEmpty | |||||
| > | |||||
| <MenuItem value="">{t("All Stores")}</MenuItem> | |||||
| <MenuItem value="2/F">2/F</MenuItem> | |||||
| <MenuItem value="4/F">4/F</MenuItem> | |||||
| </Select> | |||||
| </FormControl> | |||||
| <Typography variant="body2" sx={{ alignSelf: 'center', color: 'text.secondary' }}> | |||||
| {t("Auto-refresh every 5 minutes")} | {t("Last updated")}: {isClient && currentTime ? currentTime.format('HH:mm:ss') : '--:--:--'} | |||||
| </Typography> | |||||
| </Stack> | |||||
| {/* Table */} | |||||
| <Box sx={{ mt: 2 }}> | |||||
| {loading ? ( | |||||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ) : ( | |||||
| <TableContainer component={Paper}> | |||||
| <Table size="small" sx={{ minWidth: 1200 }}> | |||||
| <TableHead> | |||||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | |||||
| <TableCell sx={{ fontWeight: 600 }}>{t("Store ID")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }}>{t("Truck Schedule")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }}>{t("Time Remaining")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("No. of Shops")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Total Items")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Tickets Released")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }}>{t("First Ticket Start")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Tickets Completed")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }}>{t("Last Ticket End")}</TableCell> | |||||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Pick Time (min)")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {filteredData.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={10} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No truck schedules available for today")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| filteredData.map((row, index) => { | |||||
| const timeRemaining = calculateTimeRemaining(row.truckDepartureTime); | |||||
| const chipColor = getTimeChipColor(row.truckDepartureTime); | |||||
| return ( | |||||
| <TableRow | |||||
| key={`${row.storeId}-${row.truckLanceCode}-${index}`} | |||||
| sx={{ | |||||
| '&:hover': { backgroundColor: 'grey.50' }, | |||||
| backgroundColor: row.numberOfPickTickets > 0 && row.numberOfTicketsCompleted >= row.numberOfPickTickets | |||||
| ? 'success.light' | |||||
| : 'inherit' | |||||
| }} | |||||
| > | |||||
| <TableCell> | |||||
| <Chip | |||||
| label={row.storeId || '-'} | |||||
| size="small" | |||||
| color={row.storeId === '2/F' ? 'primary' : 'secondary'} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}> | |||||
| <Typography variant="body2" sx={{ fontWeight: 500 }}> | |||||
| {row.truckLanceCode || '-'} | |||||
| </Typography> | |||||
| <Typography variant="caption" color="text.secondary"> | |||||
| ETD: {formatTime(row.truckDepartureTime)} | |||||
| </Typography> | |||||
| </Box> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Chip | |||||
| label={timeRemaining} | |||||
| size="small" | |||||
| color={chipColor} | |||||
| sx={{ fontWeight: 600 }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| <Typography variant="body2"> | |||||
| {row.numberOfShopsToServe} [{row.numberOfPickTickets}] | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| <Typography variant="body2" sx={{ fontWeight: 500 }}> | |||||
| {row.totalItemsToPick} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| <Chip | |||||
| label={row.numberOfTicketsReleased} | |||||
| size="small" | |||||
| color={row.numberOfTicketsReleased > 0 ? 'info' : 'default'} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {formatDateTime(row.firstTicketStartTime)} | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| <Chip | |||||
| label={row.numberOfTicketsCompleted} | |||||
| size="small" | |||||
| color={row.numberOfTicketsCompleted > 0 ? 'success' : 'default'} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {formatDateTime(row.lastTicketEndTime)} | |||||
| </TableCell> | |||||
| <TableCell align="center"> | |||||
| <Typography | |||||
| variant="body2" | |||||
| sx={{ | |||||
| fontWeight: 500, | |||||
| color: row.pickTimeTakenMinutes !== null ? 'text.primary' : 'text.secondary' | |||||
| }} | |||||
| > | |||||
| {row.pickTimeTakenMinutes !== null ? row.pickTimeTakenMinutes : '-'} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ); | |||||
| }) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| )} | |||||
| </Box> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default TruckScheduleDashboard; | |||||
| @@ -0,0 +1,3 @@ | |||||
| export { default as TruckScheduleDashboard } from './TruckScheduleDashboard'; | |||||
| @@ -69,6 +69,7 @@ const TruckLaneDetail: React.FC = () => { | |||||
| const [uniqueRemarks, setUniqueRemarks] = useState<string[]>([]); | const [uniqueRemarks, setUniqueRemarks] = useState<string[]>([]); | ||||
| const [uniqueShopCodes, setUniqueShopCodes] = useState<string[]>([]); | const [uniqueShopCodes, setUniqueShopCodes] = useState<string[]>([]); | ||||
| const [uniqueShopNames, setUniqueShopNames] = useState<string[]>([]); | const [uniqueShopNames, setUniqueShopNames] = useState<string[]>([]); | ||||
| const [shopNameByCodeMap, setShopNameByCodeMap] = useState<Map<string, string>>(new Map()); | |||||
| const [addShopDialogOpen, setAddShopDialogOpen] = useState<boolean>(false); | const [addShopDialogOpen, setAddShopDialogOpen] = useState<boolean>(false); | ||||
| const [newShop, setNewShop] = useState({ | const [newShop, setNewShop] = useState({ | ||||
| shopName: "", | shopName: "", | ||||
| @@ -86,11 +87,12 @@ const TruckLaneDetail: React.FC = () => { | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const fetchAutocompleteData = async () => { | const fetchAutocompleteData = async () => { | ||||
| try { | try { | ||||
| const [shopData, remarks, codes, names] = await Promise.all([ | |||||
| const [shopData, remarks, codes, names, allShopsFromShopTable] = await Promise.all([ | |||||
| findAllUniqueShopNamesAndCodesFromTrucksClient() as Promise<Array<{ name: string; code: string }>>, | findAllUniqueShopNamesAndCodesFromTrucksClient() as Promise<Array<{ name: string; code: string }>>, | ||||
| findAllUniqueRemarksFromTrucksClient() as Promise<string[]>, | findAllUniqueRemarksFromTrucksClient() as Promise<string[]>, | ||||
| findAllUniqueShopCodesFromTrucksClient() as Promise<string[]>, | findAllUniqueShopCodesFromTrucksClient() as Promise<string[]>, | ||||
| findAllUniqueShopNamesFromTrucksClient() as Promise<string[]>, | findAllUniqueShopNamesFromTrucksClient() as Promise<string[]>, | ||||
| fetchAllShopsClient() as Promise<ShopAndTruck[]>, | |||||
| ]); | ]); | ||||
| // Convert to Shop format (id will be 0 since we don't have shop IDs from truck table) | // 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 || []); | setUniqueRemarks(remarks || []); | ||||
| setUniqueShopCodes(codes || []); | setUniqueShopCodes(codes || []); | ||||
| setUniqueShopNames(names || []); | setUniqueShopNames(names || []); | ||||
| // Create lookup map: shopCode -> shopName from shop table | |||||
| const shopNameMap = new Map<string, string>(); | |||||
| (allShopsFromShopTable || []).forEach((shop) => { | |||||
| if (shop.code) { | |||||
| shopNameMap.set(String(shop.code).trim().toLowerCase(), String(shop.name || "").trim()); | |||||
| } | |||||
| }); | |||||
| setShopNameByCodeMap(shopNameMap); | |||||
| } catch (err) { | } catch (err) { | ||||
| console.error("Failed to load autocomplete data:", err); | console.error("Failed to load autocomplete data:", err); | ||||
| } | } | ||||
| @@ -700,6 +711,7 @@ const TruckLaneDetail: React.FC = () => { | |||||
| <TableHead> | <TableHead> | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Shop Name")}</TableCell> | <TableCell>{t("Shop Name")}</TableCell> | ||||
| <TableCell>{t("Shop Branch")}</TableCell> | |||||
| <TableCell>{t("Shop Code")}</TableCell> | <TableCell>{t("Shop Code")}</TableCell> | ||||
| <TableCell>{t("Remark")}</TableCell> | <TableCell>{t("Remark")}</TableCell> | ||||
| <TableCell>{t("Loading Sequence")}</TableCell> | <TableCell>{t("Loading Sequence")}</TableCell> | ||||
| @@ -709,7 +721,7 @@ const TruckLaneDetail: React.FC = () => { | |||||
| <TableBody> | <TableBody> | ||||
| {shopsData.length === 0 ? ( | {shopsData.length === 0 ? ( | ||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={5} align="center"> | |||||
| <TableCell colSpan={6} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("No shops found using this truck lane")} | {t("No shops found using this truck lane")} | ||||
| </Typography> | </Typography> | ||||
| @@ -719,6 +731,14 @@ const TruckLaneDetail: React.FC = () => { | |||||
| shopsData.map((shop, index) => ( | shopsData.map((shop, index) => ( | ||||
| <TableRow key={shop.id ?? `shop-${index}`}> | <TableRow key={shop.id ?? `shop-${index}`}> | ||||
| <TableCell> | <TableCell> | ||||
| {/* Shop Name from shop table (read-only, looked up by shop code) */} | |||||
| {(() => { | |||||
| const shopCode = String(shop.code || "").trim().toLowerCase(); | |||||
| return shopNameByCodeMap.get(shopCode) || "-"; | |||||
| })()} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {/* Shop Branch from truck table (editable) */} | |||||
| {editingRowIndex === index ? ( | {editingRowIndex === index ? ( | ||||
| <Autocomplete | <Autocomplete | ||||
| freeSolo | freeSolo | ||||
| @@ -53,8 +53,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| successDialog(t("Delete Success"), t); | successDialog(t("Delete Success"), t); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Failed to delete warehouse:", 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); | ||||
| }, [t, router]); | }, [t, router]); | ||||
| @@ -76,18 +74,14 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| try { | try { | ||||
| let results: WarehouseResult[] = warehouses; | 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 storeId = searchInputs.store_id?.trim() || ""; | ||||
| const warehouse = searchInputs.warehouse?.trim() || ""; | const warehouse = searchInputs.warehouse?.trim() || ""; | ||||
| const area = searchInputs.area?.trim() || ""; | const area = searchInputs.area?.trim() || ""; | ||||
| const slot = searchInputs.slot?.trim() || ""; | const slot = searchInputs.slot?.trim() || ""; | ||||
| const stockTakeSection = searchInputs.stockTakeSection?.trim() || ""; | const stockTakeSection = searchInputs.stockTakeSection?.trim() || ""; | ||||
| // If any field has a value, filter by code pattern and stockTakeSection | |||||
| if (storeId || warehouse || area || slot || stockTakeSection) { | if (storeId || warehouse || area || slot || stockTakeSection) { | ||||
| results = warehouses.filter((warehouseItem) => { | results = warehouses.filter((warehouseItem) => { | ||||
| // Filter by stockTakeSection if provided | |||||
| if (stockTakeSection) { | if (stockTakeSection) { | ||||
| const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | ||||
| if (!itemStockTakeSection.includes(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 (storeId || warehouse || area || slot) { | ||||
| if (!warehouseItem.code) { | if (!warehouseItem.code) { | ||||
| return false; | return false; | ||||
| @@ -103,8 +96,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| const codeValue = String(warehouseItem.code).toLowerCase(); | 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("-"); | const codeParts = codeValue.split("-"); | ||||
| if (codeParts.length >= 4) { | if (codeParts.length >= 4) { | ||||
| @@ -121,7 +112,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | 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 storeIdMatch = !storeId || codeValue.includes(storeId.toLowerCase()); | ||||
| const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase()); | const warehouseMatch = !warehouse || codeValue.includes(warehouse.toLowerCase()); | ||||
| const areaMatch = !area || codeValue.includes(area.toLowerCase()); | const areaMatch = !area || codeValue.includes(area.toLowerCase()); | ||||
| @@ -130,11 +120,9 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | return storeIdMatch && warehouseMatch && areaMatch && slotMatch; | ||||
| } | } | ||||
| // If only stockTakeSection is provided, return true (already filtered above) | |||||
| return true; | return true; | ||||
| }); | }); | ||||
| } else { | } else { | ||||
| // If no search terms, show all warehouses | |||||
| results = warehouses; | results = warehouses; | ||||
| } | } | ||||
| @@ -142,7 +130,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | setPagingController({ pageNum: 1, pageSize: pagingController.pageSize }); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error searching warehouses:", error); | console.error("Error searching warehouses:", error); | ||||
| // Fallback: filter by code pattern and stockTakeSection | |||||
| const storeId = searchInputs.store_id?.trim().toLowerCase() || ""; | const storeId = searchInputs.store_id?.trim().toLowerCase() || ""; | ||||
| const warehouse = searchInputs.warehouse?.trim().toLowerCase() || ""; | const warehouse = searchInputs.warehouse?.trim().toLowerCase() || ""; | ||||
| const area = searchInputs.area?.trim().toLowerCase() || ""; | const area = searchInputs.area?.trim().toLowerCase() || ""; | ||||
| @@ -151,7 +138,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| setFilteredWarehouse( | setFilteredWarehouse( | ||||
| warehouses.filter((warehouseItem) => { | warehouses.filter((warehouseItem) => { | ||||
| // Filter by stockTakeSection if provided | |||||
| if (stockTakeSection) { | if (stockTakeSection) { | ||||
| const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | const itemStockTakeSection = String(warehouseItem.stockTakeSection || "").toLowerCase(); | ||||
| if (!itemStockTakeSection.includes(stockTakeSection)) { | 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 (storeId || warehouse || area || slot) { | ||||
| if (!warehouseItem.code) { | if (!warehouseItem.code) { | ||||
| return false; | return false; | ||||
| @@ -267,7 +252,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| justifyContent: "flex-start", | justifyContent: "flex-start", | ||||
| }} | }} | ||||
| > | > | ||||
| {/* 樓層 field with F inside on the right */} | |||||
| <TextField | <TextField | ||||
| label={t("store_id")} | label={t("store_id")} | ||||
| value={searchInputs.store_id} | value={searchInputs.store_id} | ||||
| @@ -285,7 +269,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| <Typography variant="body1" sx={{ mx: 0.5 }}> | <Typography variant="body1" sx={{ mx: 0.5 }}> | ||||
| - | - | ||||
| </Typography> | </Typography> | ||||
| {/* 倉庫 field */} | |||||
| <TextField | <TextField | ||||
| label={t("warehouse")} | label={t("warehouse")} | ||||
| value={searchInputs.warehouse} | value={searchInputs.warehouse} | ||||
| @@ -298,7 +281,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| <Typography variant="body1" sx={{ mx: 0.5 }}> | <Typography variant="body1" sx={{ mx: 0.5 }}> | ||||
| - | - | ||||
| </Typography> | </Typography> | ||||
| {/* 區域 field */} | |||||
| <TextField | <TextField | ||||
| label={t("area")} | label={t("area")} | ||||
| value={searchInputs.area} | value={searchInputs.area} | ||||
| @@ -311,7 +293,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| <Typography variant="body1" sx={{ mx: 0.5 }}> | <Typography variant="body1" sx={{ mx: 0.5 }}> | ||||
| - | - | ||||
| </Typography> | </Typography> | ||||
| {/* 儲位 field */} | |||||
| <TextField | <TextField | ||||
| label={t("slot")} | label={t("slot")} | ||||
| value={searchInputs.slot} | value={searchInputs.slot} | ||||
| @@ -321,7 +302,6 @@ const WarehouseHandle: React.FC<Props> = ({ warehouses }) => { | |||||
| size="small" | size="small" | ||||
| sx={{ width: "150px", minWidth: "120px" }} | sx={{ width: "150px", minWidth: "120px" }} | ||||
| /> | /> | ||||
| {/* 盤點區域 field */} | |||||
| <Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}> | <Box sx={{ flex: 1, minWidth: "150px", ml: 2 }}> | ||||
| <TextField | <TextField | ||||
| label={t("stockTakeSection")} | label={t("stockTakeSection")} | ||||
| @@ -30,20 +30,24 @@ function TabPanel(props: TabPanelProps) { | |||||
| interface QrCodeHandleTabsProps { | interface QrCodeHandleTabsProps { | ||||
| userTabContent: ReactNode; | userTabContent: ReactNode; | ||||
| equipmentTabContent: ReactNode; | equipmentTabContent: ReactNode; | ||||
| warehouseTabContent: ReactNode; | |||||
| } | } | ||||
| const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({ | const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({ | ||||
| userTabContent, | userTabContent, | ||||
| equipmentTabContent, | equipmentTabContent, | ||||
| warehouseTabContent, | |||||
| }) => { | }) => { | ||||
| const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
| const { t: tUser } = useTranslation("user"); | const { t: tUser } = useTranslation("user"); | ||||
| const { t: tWarehouse } = useTranslation("warehouse"); | |||||
| const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const getInitialTab = () => { | const getInitialTab = () => { | ||||
| const tab = searchParams.get("tab"); | const tab = searchParams.get("tab"); | ||||
| if (tab === "equipment") return 1; | if (tab === "equipment") return 1; | ||||
| if (tab === "warehouse") return 2; | |||||
| if (tab === "user") return 0; | if (tab === "user") return 0; | ||||
| return 0; | return 0; | ||||
| }; | }; | ||||
| @@ -54,6 +58,8 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({ | |||||
| const tab = searchParams.get("tab"); | const tab = searchParams.get("tab"); | ||||
| if (tab === "equipment") { | if (tab === "equipment") { | ||||
| setCurrentTab(1); | setCurrentTab(1); | ||||
| } else if (tab === "warehouse") { | |||||
| setCurrentTab(2); | |||||
| } else if (tab === "user") { | } else if (tab === "user") { | ||||
| setCurrentTab(0); | setCurrentTab(0); | ||||
| } | } | ||||
| @@ -61,7 +67,9 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({ | |||||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | ||||
| setCurrentTab(newValue); | 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()); | const params = new URLSearchParams(searchParams.toString()); | ||||
| params.set("tab", tabName); | params.set("tab", tabName); | ||||
| router.push(`?${params.toString()}`, { scroll: false }); | router.push(`?${params.toString()}`, { scroll: false }); | ||||
| @@ -73,6 +81,7 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({ | |||||
| <Tabs value={currentTab} onChange={handleTabChange}> | <Tabs value={currentTab} onChange={handleTabChange}> | ||||
| <Tab label={tUser("User")} /> | <Tab label={tUser("User")} /> | ||||
| <Tab label={t("Equipment")} /> | <Tab label={t("Equipment")} /> | ||||
| <Tab label={tWarehouse("Warehouse")} /> | |||||
| </Tabs> | </Tabs> | ||||
| </Box> | </Box> | ||||
| @@ -83,6 +92,10 @@ const QrCodeHandleTabs: React.FC<QrCodeHandleTabsProps> = ({ | |||||
| <TabPanel value={currentTab} index={1}> | <TabPanel value={currentTab} index={1}> | ||||
| {equipmentTabContent} | {equipmentTabContent} | ||||
| </TabPanel> | </TabPanel> | ||||
| <TabPanel value={currentTab} index={2}> | |||||
| {warehouseTabContent} | |||||
| </TabPanel> | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -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; | |||||
| @@ -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; | |||||
| @@ -26,6 +26,9 @@ | |||||
| "Shop added to truck lane successfully": "Shop added to truck lane successfully", | "Shop added to truck lane successfully": "Shop added to truck lane successfully", | ||||
| "Failed to create shop in truck lane": "Failed to create shop in truck lane", | "Failed to create shop in truck lane": "Failed to create shop in truck lane", | ||||
| "Add Shop": "Add Shop", | "Add Shop": "Add Shop", | ||||
| "Shop Name": "Shop Name", | |||||
| "Shop Branch": "Shop Branch", | |||||
| "Shop Code": "Shop Code", | |||||
| "Search or select shop name": "Search or select shop name", | "Search or select shop name": "Search or select shop name", | ||||
| "Search or select shop code": "Search or select shop code", | "Search or select shop code": "Search or select shop code", | ||||
| "Search or select remark": "Search or select remark", | "Search or select remark": "Search or select remark", | ||||
| @@ -1 +1,76 @@ | |||||
| {} | |||||
| { | |||||
| "Dashboard": "Dashboard", | |||||
| "Order status": "Order status", | |||||
| "pending": "pending", | |||||
| "receiving": "receiving", | |||||
| "total": "total", | |||||
| "Warehouse temperature record": "Warehouse temperature record", | |||||
| "Warehouse type": "Warehouse type", | |||||
| "Last 6 hours": "Last 6 hours", | |||||
| "Add some entries!": "Add some entries!", | |||||
| "Last 24 hours": "Last 24 hours", | |||||
| "Cold storage": "Cold storage", | |||||
| "Normal temperature storage": "Normal temperature storage", | |||||
| "Temperature status": "Temperature status", | |||||
| "Humidity status": "Humidity status", | |||||
| "Warehouse status": "Warehouse status", | |||||
| "Progress chart": "Progress chart", | |||||
| "Purchase Order Code": "Purchase Order Code", | |||||
| "Item Name": "Item Name", | |||||
| "Escalation Level": "Escalation Level", | |||||
| "Reason": "Reason", | |||||
| "escalated date": "escalated date", | |||||
| "Order completion": "Order completion", | |||||
| "Store Management": "Store Management", | |||||
| "Consumable": "Consumable", | |||||
| "Shipment": "Shipment", | |||||
| "Extracted order": "Extracted order", | |||||
| "Pending order": "Pending order", | |||||
| "Temperature": "Temperature", | |||||
| "Humidity": "Humidity", | |||||
| "Pending storage": "Pending storage", | |||||
| "Total storage": "Total storage", | |||||
| "Application completion": "Application completion", | |||||
| "Processed application": "Processed application", | |||||
| "Pending application": "Pending application", | |||||
| "pending inspection material": "pending inspection material", | |||||
| "rejected": "rejected", | |||||
| "accepted": "accepted", | |||||
| "escalated": "escalated", | |||||
| "inspected material": "inspected material", | |||||
| "total material": "total material", | |||||
| "stock in escalation list": "stock in escalation list", | |||||
| "Responsible for handling colleagues": "Responsible for handling colleagues", | |||||
| "Completed QC Total": "Completed QC Total", | |||||
| "QC Fail Count": "QC Fail Count", | |||||
| "DN Date": "DN Date", | |||||
| "Received Qty": "Received Qty", | |||||
| "Po Code": "Po Code", | |||||
| "My Escalation List": "My Escalation List", | |||||
| "Escalation List": "Escalation List", | |||||
| "Purchase UoM": "Purchase UoM", | |||||
| "QC Completed Count": "QC Completed Count", | |||||
| "QC Fail-Total Count": "QC Fail-Total Count", | |||||
| "escalationStatus": "escalationStatus", | |||||
| "escalated datetime": "escalated datetime", | |||||
| "escalateFrom": "escalateFrom", | |||||
| "No": "No", | |||||
| "Responsible Escalation List": "Responsible Escalation List", | |||||
| "show completed logs": "show completed logs", | |||||
| "Rows per page": "Rows per page", | |||||
| "Truck Schedule Dashboard": "Truck Schedule Dashboard", | |||||
| "Store ID": "Store ID", | |||||
| "All Stores": "All Stores", | |||||
| "Auto-refresh every 5 minutes": "Auto-refresh every 5 minutes", | |||||
| "Last updated": "Last updated", | |||||
| "Truck Schedule": "Truck Schedule", | |||||
| "Time Remaining": "Time Remaining", | |||||
| "No. of Shops": "No. of Shops", | |||||
| "Total Items": "Total Items", | |||||
| "Tickets Released": "Tickets Released", | |||||
| "First Ticket Start": "First Ticket Start", | |||||
| "Tickets Completed": "Tickets Completed", | |||||
| "Last Ticket End": "Last Ticket End", | |||||
| "Pick Time (min)": "Pick Time (min)", | |||||
| "No truck schedules available for today": "No truck schedules available for today" | |||||
| } | |||||
| @@ -322,6 +322,7 @@ | |||||
| "ShopAndTruck": "店鋪路線管理", | "ShopAndTruck": "店鋪路線管理", | ||||
| "Shop Information": "店鋪資訊", | "Shop Information": "店鋪資訊", | ||||
| "Shop Name": "店鋪名稱", | "Shop Name": "店鋪名稱", | ||||
| "Shop Branch": "店鋪分店", | |||||
| "Shop Code": "店鋪編號", | "Shop Code": "店鋪編號", | ||||
| "Truck Lane": "卡車路線", | "Truck Lane": "卡車路線", | ||||
| "Truck Lane Detail": "卡車路線詳情", | "Truck Lane Detail": "卡車路線詳情", | ||||
| @@ -57,5 +57,20 @@ | |||||
| "No": "無", | "No": "無", | ||||
| "Responsible Escalation List": "負責的上報列表", | "Responsible Escalation List": "負責的上報列表", | ||||
| "show completed logs": "顯示已完成上報", | "show completed logs": "顯示已完成上報", | ||||
| "Rows per page": "每頁行數" | |||||
| "Rows per page": "每頁行數", | |||||
| "Truck Schedule Dashboard": "車輛調度儀表板", | |||||
| "Store ID": "樓層", | |||||
| "All Stores": "所有樓層", | |||||
| "Auto-refresh every 5 minutes": "每5分鐘自動刷新", | |||||
| "Last updated": "最後更新", | |||||
| "Truck Schedule": "車輛班次", | |||||
| "Time Remaining": "剩餘時間", | |||||
| "No. of Shops": "門店數量", | |||||
| "Total Items": "總貨品數", | |||||
| "Tickets Released": "已發放成品出倉單", | |||||
| "First Ticket Start": "首單開始時間", | |||||
| "Tickets Completed": "已完成成品出倉單", | |||||
| "Last Ticket End": "末單結束時間", | |||||
| "Pick Time (min)": "揀貨時間(分鐘)", | |||||
| "No truck schedules available for today": "今日無車輛調度計劃" | |||||
| } | } | ||||