| @@ -45,3 +45,15 @@ const qcItemAll: React.FC = async () => { | |||
| export default qcItemAll; | |||
| @@ -190,3 +190,21 @@ export const testing = cache(async (queryParams?: Record<string, any>) => { | |||
| ); | |||
| } | |||
| }); | |||
| export interface GoodsReceiptStatusRow { | |||
| supplierId: number | null; | |||
| supplierName: string; | |||
| expectedNoOfDelivery: number; | |||
| noOfOrdersReceivedAtDock: number; | |||
| noOfItemsInspected: number; | |||
| noOfItemsWithIqcIssue: number; | |||
| noOfItemsCompletedPutAwayAtStore: number; | |||
| } | |||
| export const fetchGoodsReceiptStatus = cache(async (date?: string) => { | |||
| const url = date | |||
| ? `${BASE_API_URL}/dashboard/goods-receipt-status?date=${date}` | |||
| : `${BASE_API_URL}/dashboard/goods-receipt-status`; | |||
| return await serverFetchJson<GoodsReceiptStatusRow[]>(url, { method: "GET" }); | |||
| }); | |||
| @@ -0,0 +1,17 @@ | |||
| "use client"; | |||
| import { | |||
| fetchGoodsReceiptStatus, | |||
| type GoodsReceiptStatusRow, | |||
| } from "./actions"; | |||
| export const fetchGoodsReceiptStatusClient = async ( | |||
| date?: string, | |||
| ): Promise<GoodsReceiptStatusRow[]> => { | |||
| return await fetchGoodsReceiptStatus(date); | |||
| }; | |||
| export type { GoodsReceiptStatusRow }; | |||
| export default fetchGoodsReceiptStatusClient; | |||
| @@ -207,6 +207,7 @@ export interface PickExecutionIssueData { | |||
| actualPickQty: number; | |||
| missQty: number; | |||
| badItemQty: number; | |||
| badPackageQty?: number; | |||
| issueRemark: string; | |||
| pickerName: string; | |||
| handledBy?: number; | |||
| @@ -996,6 +997,7 @@ export interface LotSubstitutionConfirmRequest { | |||
| stockOutLineId: number; | |||
| originalSuggestedPickLotId: number; | |||
| newInventoryLotNo: string; | |||
| newStockInLineId: number; | |||
| } | |||
| export const confirmLotSubstitution = async (data: LotSubstitutionConfirmRequest) => { | |||
| const response = await serverFetchJson<PostPickOrderResponse>( | |||
| @@ -2,23 +2,43 @@ | |||
| import { useTranslation } from "react-i18next"; | |||
| import { ThemeProvider } from "@mui/material/styles"; | |||
| import theme from "../../theme"; | |||
| import { TabsProps } from "@mui/material/Tabs"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import React, { useEffect, useState, ReactNode } from "react"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { Card, CardContent, CardHeader, Grid } from "@mui/material"; | |||
| import { Card, CardContent, CardHeader, Grid, Tabs, Tab, Box, FormControlLabel, Checkbox } from "@mui/material"; | |||
| import DashboardProgressChart from "./chart/DashboardProgressChart"; | |||
| import DashboardLineChart from "./chart/DashboardLineChart"; | |||
| import PendingInspectionChart from "./chart/PendingInspectionChart"; | |||
| import PendingStorageChart from "./chart/PendingStorageChart"; | |||
| import ApplicationCompletionChart from "./chart/ApplicationCompletionChart"; | |||
| import OrderCompletionChart from "./chart/OrderCompletionChart"; | |||
| import DashboardBox from "./Dashboardbox"; | |||
| 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"; | |||
| import { GoodsReceiptStatus } from "./goodsReceiptStatus"; | |||
| import { CardFilterContext } from "../CollapsibleCard/CollapsibleCard"; | |||
| interface TabPanelProps { | |||
| children?: ReactNode; | |||
| index: number; | |||
| value: number; | |||
| } | |||
| function TabPanel(props: TabPanelProps) { | |||
| const { children, value, index, ...other } = props; | |||
| return ( | |||
| <div | |||
| role="tabpanel" | |||
| hidden={value !== index} | |||
| id={`dashboard-tabpanel-${index}`} | |||
| aria-labelledby={`dashboard-tab-${index}`} | |||
| {...other} | |||
| > | |||
| {value === index && <Box sx={{ py: 2 }}>{children}</Box>} | |||
| </div> | |||
| ); | |||
| } | |||
| type Props = { | |||
| // iqc: IQCItems[] | undefined | |||
| escalationLogs: EscalationResult[] | |||
| @@ -32,6 +52,8 @@ const DashboardPage: React.FC<Props> = ({ | |||
| const router = useRouter(); | |||
| const [escLog, setEscLog] = useState<EscalationResult[]>([]); | |||
| const [currentTab, setCurrentTab] = useState(0); | |||
| const [showCompletedLogs, setShowCompletedLogs] = useState(false); | |||
| const getPendingLog = () => { | |||
| return escLog.filter(esc => esc.status == "pending"); | |||
| @@ -41,35 +63,66 @@ const DashboardPage: React.FC<Props> = ({ | |||
| setEscLog(escalationLogs); | |||
| }, [escalationLogs]) | |||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | |||
| setCurrentTab(newValue); | |||
| }; | |||
| const handleFilterChange = (checked: boolean) => { | |||
| setShowCompletedLogs(checked); | |||
| }; | |||
| return ( | |||
| <ThemeProvider theme={theme}> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12}> | |||
| <CollapsibleCard title={t("Truck Schedule Dashboard")} defaultOpen={true}> | |||
| <Card> | |||
| <CardHeader /> | |||
| <Box sx={{ borderBottom: 1, borderColor: 'divider' }}> | |||
| <Tabs | |||
| value={currentTab} | |||
| onChange={handleTabChange} | |||
| aria-label="dashboard tabs" | |||
| > | |||
| <Tab label={t("Truck Schedule Dashboard")} id="dashboard-tab-0" aria-controls="dashboard-tabpanel-0" /> | |||
| <Tab label={t("Goods Receipt Status")} id="dashboard-tab-1" aria-controls="dashboard-tabpanel-1" /> | |||
| <Tab | |||
| label={`${t("Responsible Escalation List")} (${t("pending")} : ${ | |||
| getPendingLog().length > 0 ? getPendingLog().length : t("No")})`} | |||
| id="dashboard-tab-2" | |||
| aria-controls="dashboard-tabpanel-2" | |||
| /> | |||
| </Tabs> | |||
| </Box> | |||
| <CardContent> | |||
| <TruckScheduleDashboard /> | |||
| <TabPanel value={currentTab} index={0}> | |||
| <TruckScheduleDashboard /> | |||
| </TabPanel> | |||
| <TabPanel value={currentTab} index={1}> | |||
| <GoodsReceiptStatus /> | |||
| </TabPanel> | |||
| <TabPanel value={currentTab} index={2}> | |||
| <CardFilterContext.Provider value={{ | |||
| filter: showCompletedLogs, | |||
| onFilterChange: handleFilterChange, | |||
| filterText: t("show completed logs"), | |||
| setOnFilterChange: () => {} | |||
| }}> | |||
| <Box sx={{ mb: 2 }}> | |||
| <FormControlLabel | |||
| control={ | |||
| <Checkbox | |||
| checked={showCompletedLogs} | |||
| onChange={(e) => handleFilterChange(e.target.checked)} | |||
| /> | |||
| } | |||
| label={t("show completed logs")} | |||
| /> | |||
| </Box> | |||
| <EscalationLogTable items={escLog}/> | |||
| </CardFilterContext.Provider> | |||
| </TabPanel> | |||
| </CardContent> | |||
| </CollapsibleCard> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <CollapsibleCard title={t("Goods Receipt Status")} defaultOpen={true}> | |||
| <CardContent> | |||
| <GoodsReceiptStatus /> | |||
| </CardContent> | |||
| </CollapsibleCard> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <CollapsibleCard | |||
| title={`${t("Responsible Escalation List")} (${t("pending")} : ${ | |||
| getPendingLog().length > 0 ? getPendingLog().length : t("No")})`} | |||
| showFilter={true} | |||
| filterText={t("show completed logs")} | |||
| > | |||
| <CardContent> | |||
| <EscalationLogTable items={escLog}/> | |||
| </CardContent> | |||
| </CollapsibleCard> | |||
| </Card> | |||
| </Grid> | |||
| {/* Hidden: Progress chart - not in use currently */} | |||
| {/* <Grid item xs={12}> | |||
| @@ -1,13 +1,9 @@ | |||
| "use client"; | |||
| import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; | |||
| import React, { useState, useEffect, useCallback, useMemo } from 'react'; | |||
| import { | |||
| Box, | |||
| Typography, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| Card, | |||
| CardContent, | |||
| Stack, | |||
| @@ -19,88 +15,112 @@ import { | |||
| TableRow, | |||
| Paper, | |||
| CircularProgress, | |||
| Chip | |||
| Button | |||
| } from '@mui/material'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import dayjs from 'dayjs'; | |||
| import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; | |||
| import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; | |||
| import { DatePicker } from '@mui/x-date-pickers/DatePicker'; | |||
| import { fetchGoodsReceiptStatusClient, type GoodsReceiptStatusRow } from '@/app/api/dashboard/client'; | |||
| interface GoodsReceiptStatusItem { | |||
| id: string; | |||
| } | |||
| const REFRESH_MS = 15 * 60 * 1000; | |||
| const GoodsReceiptStatus: React.FC = () => { | |||
| const { t } = useTranslation("dashboard"); | |||
| const [selectedFilter, setSelectedFilter] = useState<string>(""); | |||
| const [data, setData] = useState<GoodsReceiptStatusItem[]>([]); | |||
| const [loading, setLoading] = useState<boolean>(false); | |||
| const [currentTime, setCurrentTime] = useState<dayjs.Dayjs | null>(null); | |||
| const [isClient, setIsClient] = useState<boolean>(false); | |||
| useEffect(() => { | |||
| setIsClient(true); | |||
| setCurrentTime(dayjs()); | |||
| }, []); | |||
| const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs>(dayjs()); | |||
| const [data, setData] = useState<GoodsReceiptStatusRow[]>([]); | |||
| const [loading, setLoading] = useState<boolean>(true); | |||
| const [lastUpdated, setLastUpdated] = useState<dayjs.Dayjs | null>(null); | |||
| const [screenCleared, setScreenCleared] = useState<boolean>(false); | |||
| const loadData = useCallback(async () => { | |||
| if (screenCleared) return; | |||
| try { | |||
| setData([]); | |||
| setLoading(true); | |||
| const dateParam = selectedDate.format('YYYY-MM-DD'); | |||
| const result = await fetchGoodsReceiptStatusClient(dateParam); | |||
| setData(result ?? []); | |||
| setLastUpdated(dayjs()); | |||
| } catch (error) { | |||
| console.error('Error fetching goods receipt status:', error); | |||
| setData([]); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, []); | |||
| }, [selectedDate, screenCleared]); | |||
| useEffect(() => { | |||
| if (screenCleared) return; | |||
| loadData(); | |||
| const refreshInterval = setInterval(() => { | |||
| loadData(); | |||
| }, 5 * 60 * 1000); | |||
| }, REFRESH_MS); | |||
| return () => clearInterval(refreshInterval); | |||
| }, [loadData]); | |||
| }, [loadData, screenCleared]); | |||
| useEffect(() => { | |||
| if (!isClient) return; | |||
| const timeInterval = setInterval(() => { | |||
| setCurrentTime(dayjs()); | |||
| }, 60 * 1000); | |||
| return () => clearInterval(timeInterval); | |||
| }, [isClient]); | |||
| const filteredData = useMemo(() => { | |||
| if (!selectedFilter) return data; | |||
| return data.filter(item => true); | |||
| }, [data, selectedFilter]); | |||
| const selectedDateLabel = useMemo(() => { | |||
| return selectedDate.format('YYYY-MM-DD'); | |||
| }, [selectedDate]); | |||
| if (screenCleared) { | |||
| return ( | |||
| <Card sx={{ mb: 2 }}> | |||
| <CardContent> | |||
| <Stack direction="row" spacing={2} justifyContent="space-between" alignItems="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Screen cleared")} | |||
| </Typography> | |||
| <Button variant="contained" onClick={() => setScreenCleared(false)}> | |||
| {t("Restore Screen")} | |||
| </Button> | |||
| </Stack> | |||
| </CardContent> | |||
| </Card> | |||
| ); | |||
| } | |||
| return ( | |||
| <Card sx={{ mb: 2 }}> | |||
| <CardContent> | |||
| {/* Filter */} | |||
| <Stack direction="row" spacing={2} sx={{ mb: 3 }}> | |||
| <FormControl sx={{ minWidth: 150 }} size="small"> | |||
| <InputLabel id="filter-select-label" shrink={true}> | |||
| {t("Filter")} | |||
| </InputLabel> | |||
| <Select | |||
| labelId="filter-select-label" | |||
| id="filter-select" | |||
| value={selectedFilter} | |||
| label={t("Filter")} | |||
| onChange={(e) => setSelectedFilter(e.target.value)} | |||
| displayEmpty | |||
| > | |||
| <MenuItem value="">{t("All")}</MenuItem> | |||
| {/* TODO: Add filter options when implementing */} | |||
| </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') : '--:--:--'} | |||
| {/* Header */} | |||
| <Stack direction="row" spacing={2} sx={{ mb: 2 }} alignItems="center" flexWrap="wrap"> | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| <Typography variant="body2" sx={{ fontWeight: 600 }}> | |||
| {t("Date")}: | |||
| </Typography> | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| <DatePicker | |||
| value={selectedDate} | |||
| onChange={(value) => { | |||
| if (!value) return; | |||
| setSelectedDate(value); | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| size: "small", | |||
| sx: { minWidth: 160 } | |||
| } | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Allow to select Date to view history.")} | |||
| </Typography> | |||
| </Stack> | |||
| <Box sx={{ flexGrow: 1 }} /> | |||
| <Typography variant="body2" sx={{ color: 'text.secondary' }}> | |||
| {t("Auto-refresh every 15 minutes")} | {t("Last updated")}: {lastUpdated ? lastUpdated.format('HH:mm:ss') : '--:--:--'} | |||
| </Typography> | |||
| <Button variant="outlined" color="inherit" onClick={() => setScreenCleared(true)}> | |||
| {t("Exit Screen")} | |||
| </Button> | |||
| </Stack> | |||
| {/* Table */} | |||
| @@ -114,38 +134,80 @@ const GoodsReceiptStatus: React.FC = () => { | |||
| <Table size="small" sx={{ minWidth: 1200 }}> | |||
| <TableHead> | |||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("Column 1")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("Column 2")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("Column 3")}</TableCell> | |||
| {/* TODO: Add table columns when implementing */} | |||
| <TableCell sx={{ fontWeight: 600 }}>{t("Supplier")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("Expected No. of Delivery")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("No. of Orders Received at Dock")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("No. of Items Inspected")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("No. of Items with IQC Issue")}</TableCell> | |||
| <TableCell sx={{ fontWeight: 600 }} align="center">{t("No. of Items Completed Put Away at Store")}</TableCell> | |||
| </TableRow> | |||
| <TableRow sx={{ backgroundColor: 'grey.50' }}> | |||
| <TableCell> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Show Supplier Name")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Based on Expected Delivery Date")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Upon entry of DN and Lot No. for all items of the order")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Upon any IQC decision received")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Count any item with IQC defect in any IQC criteria")} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <Typography variant="caption" color="text.secondary"> | |||
| {t("Upon completion of put away for an material in order. Count no. of items being put away")} | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {filteredData.length === 0 ? ( | |||
| {data.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={3} align="center"> | |||
| <TableCell colSpan={6} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No data available")} | |||
| {t("No data available")} ({selectedDateLabel}) | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| ) : ( | |||
| filteredData.map((row, index) => ( | |||
| data.map((row, index) => ( | |||
| <TableRow | |||
| key={row.id || index} | |||
| key={`${row.supplierId ?? 'na'}-${index}`} | |||
| sx={{ | |||
| '&:hover': { backgroundColor: 'grey.50' } | |||
| }} | |||
| > | |||
| <TableCell> | |||
| {/* TODO: Add table cell content when implementing */} | |||
| - | |||
| {row.supplierName || '-'} | |||
| </TableCell> | |||
| <TableCell> | |||
| - | |||
| <TableCell align="center"> | |||
| {row.expectedNoOfDelivery ?? 0} | |||
| </TableCell> | |||
| <TableCell> | |||
| - | |||
| <TableCell align="center"> | |||
| {row.noOfOrdersReceivedAtDock ?? 0} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| {row.noOfItemsInspected ?? 0} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| {row.noOfItemsWithIqcIssue ?? 0} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| {row.noOfItemsCompletedPutAwayAtStore ?? 0} | |||
| </TableCell> | |||
| </TableRow> | |||
| )) | |||
| @@ -57,72 +57,119 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| loadSummaries(); | |||
| }, [loadSummaries]); | |||
| const handleAssignByLane = useCallback(async ( | |||
| storeId: string, | |||
| truckDepartureTime: string, | |||
| truckLanceCode: string, | |||
| requiredDate: string | |||
| const handleAssignByLane = useCallback(async ( | |||
| storeId: string, | |||
| truckDepartureTime: string, | |||
| truckLanceCode: string, | |||
| requiredDate: string | |||
| ) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| return; | |||
| } | |||
| let dateParam: string | undefined; | |||
| if (requiredDate === "today") { | |||
| dateParam = dayjs().format('YYYY-MM-DD'); | |||
| } else if (requiredDate === "tomorrow") { | |||
| dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||
| } else if (requiredDate === "dayAfterTomorrow") { | |||
| dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD'); | |||
| } | |||
| setIsAssigning(true); | |||
| try { | |||
| const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, dateParam); | |||
| if (res.code === "SUCCESS") { | |||
| console.log(" Successfully assigned pick order from lane", truckLanceCode); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| loadSummaries(); // 刷新按钮状态 | |||
| onPickOrderAssigned?.(); | |||
| onSwitchToDetailTab?.(); | |||
| } else if (res.code === "USER_BUSY") { | |||
| Swal.fire({ | |||
| icon: "warning", | |||
| title: t("Warning"), | |||
| text: t("You already have a pick order in progess. Please complete it first before taking next pick order."), | |||
| confirmButtonText: t("Confirm"), | |||
| confirmButtonColor: "#8dba00" | |||
| }); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| } else if (res.code === "NO_ORDERS") { | |||
| Swal.fire({ | |||
| icon: "info", | |||
| title: t("Info"), | |||
| text: t("No available pick order(s) for this lane."), | |||
| confirmButtonText: t("Confirm"), | |||
| confirmButtonColor: "#8dba00" | |||
| }); | |||
| } else { | |||
| console.log("ℹ️ Assignment result:", res.message); | |||
| } | |||
| } catch (error) { | |||
| console.error("❌ Error assigning by lane:", error); | |||
| ) => { | |||
| if (!currentUserId) { | |||
| console.error("Missing user id in session"); | |||
| return; | |||
| } | |||
| let dateParam: string | undefined; | |||
| if (requiredDate === "today") { | |||
| dateParam = dayjs().format('YYYY-MM-DD'); | |||
| } else if (requiredDate === "tomorrow") { | |||
| dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||
| } else if (requiredDate === "dayAfterTomorrow") { | |||
| dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD'); | |||
| } | |||
| setIsAssigning(true); | |||
| try { | |||
| const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, dateParam); | |||
| if (res.code === "SUCCESS") { | |||
| console.log(" Successfully assigned pick order from lane", truckLanceCode); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| loadSummaries(); // 刷新按钮状态 | |||
| onPickOrderAssigned?.(); | |||
| onSwitchToDetailTab?.(); | |||
| } else if (res.code === "USER_BUSY") { | |||
| Swal.fire({ | |||
| icon: "error", | |||
| title: t("Error"), | |||
| text: t("Error occurred during assignment."), | |||
| icon: "warning", | |||
| title: t("Warning"), | |||
| text: t("You already have a pick order in progess. Please complete it first before taking next pick order."), | |||
| confirmButtonText: t("Confirm"), | |||
| confirmButtonColor: "#8dba00" | |||
| }); | |||
| } finally { | |||
| setIsAssigning(false); | |||
| window.dispatchEvent(new CustomEvent('pickOrderAssigned')); | |||
| } else if (res.code === "NO_ORDERS") { | |||
| Swal.fire({ | |||
| icon: "info", | |||
| title: t("Info"), | |||
| text: t("No available pick order(s) for this lane."), | |||
| confirmButtonText: t("Confirm"), | |||
| confirmButtonColor: "#8dba00" | |||
| }); | |||
| } else { | |||
| console.log("ℹ️ Assignment result:", res.message); | |||
| } | |||
| }, [currentUserId, t, selectedDate, onPickOrderAssigned, onSwitchToDetailTab, loadSummaries]); | |||
| } catch (error) { | |||
| console.error("❌ Error assigning by lane:", error); | |||
| Swal.fire({ | |||
| icon: "error", | |||
| title: t("Error"), | |||
| text: t("Error occurred during assignment."), | |||
| confirmButtonText: t("Confirm"), | |||
| confirmButtonColor: "#8dba00" | |||
| }); | |||
| } finally { | |||
| setIsAssigning(false); | |||
| } | |||
| }, [currentUserId, t, selectedDate, onPickOrderAssigned, onSwitchToDetailTab, loadSummaries]); | |||
| const getDateLabel = (offset: number) => { | |||
| return dayjs().add(offset, 'day').format('YYYY-MM-DD'); | |||
| }; | |||
| const handleLaneButtonClick = useCallback(async ( | |||
| storeId: string, | |||
| truckDepartureTime: string, | |||
| truckLanceCode: string, | |||
| requiredDate: string, | |||
| unassigned: number, | |||
| total: number | |||
| ) => { | |||
| // Format the date for display | |||
| let dateDisplay: string; | |||
| if (requiredDate === "today") { | |||
| dateDisplay = dayjs().format('YYYY-MM-DD'); | |||
| } else if (requiredDate === "tomorrow") { | |||
| dateDisplay = dayjs().add(1, 'day').format('YYYY-MM-DD'); | |||
| } else if (requiredDate === "dayAfterTomorrow") { | |||
| dateDisplay = dayjs().add(2, 'day').format('YYYY-MM-DD'); | |||
| } else { | |||
| dateDisplay = requiredDate; | |||
| } | |||
| // Show confirmation dialog | |||
| const result = await Swal.fire({ | |||
| title: t("Confirm Assignment"), | |||
| html: ` | |||
| <div style="text-align: left; padding: 10px 0;"> | |||
| <p><strong>${t("Store")}:</strong> ${storeId}</p> | |||
| <p><strong>${t("Lane Code")}:</strong> ${truckLanceCode}</p> | |||
| <p><strong>${t("Departure Time")}:</strong> ${truckDepartureTime}</p> | |||
| <p><strong>${t("Required Date")}:</strong> ${dateDisplay}</p> | |||
| <p><strong>${t("Available Orders")}:</strong> ${unassigned}/${total}</p> | |||
| </div> | |||
| `, | |||
| icon: "question", | |||
| showCancelButton: true, | |||
| confirmButtonText: t("Confirm"), | |||
| cancelButtonText: t("Cancel"), | |||
| confirmButtonColor: "#8dba00", | |||
| cancelButtonColor: "#F04438", | |||
| reverseButtons: true | |||
| }); | |||
| // Only proceed if user confirmed | |||
| if (result.isConfirmed) { | |||
| await handleAssignByLane(storeId, truckDepartureTime, truckLanceCode, requiredDate); | |||
| } | |||
| }, [handleAssignByLane, t]); | |||
| const getDateLabel = (offset: number) => { | |||
| return dayjs().add(offset, 'day').format('YYYY-MM-DD'); | |||
| }; | |||
| // Flatten rows to create one box per lane | |||
| const flattenRows = (rows: any[]) => { | |||
| @@ -296,7 +343,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| onClick={() => handleLaneButtonClick("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate, item.lane.unassigned, item.lane.total)} | |||
| sx={{ | |||
| flex: 1, | |||
| fontSize: '1.1rem', | |||
| @@ -396,7 +443,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||
| variant="outlined" | |||
| size="medium" | |||
| disabled={item.lane.unassigned === 0 || isAssigning} | |||
| onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)} | |||
| onClick={() => handleLaneButtonClick("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate, item.lane.unassigned, item.lane.total)} | |||
| sx={{ | |||
| flex: 1, | |||
| fontSize: '1.1rem', | |||
| @@ -655,7 +655,7 @@ const handleAssignByLane = useCallback(async ( | |||
| > | |||
| {t("Print All Draft")} ({releasedOrderCount}) | |||
| </Button> | |||
| {/* | |||
| <Button | |||
| variant="contained" | |||
| sx={{ | |||
| @@ -676,6 +676,7 @@ const handleAssignByLane = useCallback(async ( | |||
| > | |||
| {t("Print Draft")} | |||
| </Button> | |||
| */} | |||
| </Stack> | |||
| </Box> | |||
| @@ -1,4 +1,3 @@ | |||
| // FPSMS-frontend/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx | |||
| "use client"; | |||
| import { | |||
| @@ -16,16 +15,18 @@ import { | |||
| TextField, | |||
| Typography, | |||
| } from "@mui/material"; | |||
| import { useCallback, useEffect, useState } from "react"; | |||
| import { useCallback, useEffect, useState, useRef } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions"; | |||
| import { | |||
| GetPickOrderLineInfo, | |||
| PickExecutionIssueData, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { fetchEscalationCombo } from "@/app/api/user/actions"; | |||
| import { useRef } from "react"; | |||
| import dayjs from 'dayjs'; | |||
| import dayjs from "dayjs"; | |||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| interface LotPickData { | |||
| id: number; | |||
| id: number; | |||
| lotId: number; | |||
| lotNo: string; | |||
| expiryDate: string; | |||
| @@ -39,7 +40,12 @@ interface LotPickData { | |||
| requiredQty: number; | |||
| actualPickQty: number; | |||
| lotStatus: string; | |||
| lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; | |||
| lotAvailability: | |||
| | "available" | |||
| | "insufficient_stock" | |||
| | "expired" | |||
| | "status_unavailable" | |||
| | "rejected"; | |||
| stockOutLineId?: number; | |||
| stockOutLineStatus?: string; | |||
| stockOutLineQty?: number; | |||
| @@ -77,12 +83,14 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({}); | |||
| const [errors, setErrors] = useState<FormErrors>({}); | |||
| const [loading, setLoading] = useState(false); | |||
| const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]); | |||
| const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>( | |||
| [] | |||
| ); | |||
| const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | |||
| return lot.availableQty || 0; | |||
| }, []); | |||
| const calculateRequiredQty = useCallback((lot: LotPickData) => { | |||
| return lot.requiredQty || 0; | |||
| }, []); | |||
| @@ -96,7 +104,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| console.error("Error fetching handlers:", error); | |||
| } | |||
| }; | |||
| fetchHandlers(); | |||
| }, []); | |||
| @@ -136,92 +144,119 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| requiredQty: selectedLot.requiredQty, | |||
| actualPickQty: selectedLot.actualPickQty || 0, | |||
| missQty: 0, | |||
| badItemQty: 0, | |||
| issueRemark: '', | |||
| pickerName: '', | |||
| badItemQty: 0, // Bad Item Qty | |||
| badPackageQty: 0, // Bad Package Qty (frontend only) | |||
| issueRemark: "", | |||
| pickerName: "", | |||
| handledBy: undefined, | |||
| reason: '', | |||
| badReason: '', | |||
| reason: "", | |||
| badReason: "", | |||
| }); | |||
| initKeyRef.current = key; | |||
| }, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]); | |||
| }, [ | |||
| open, | |||
| selectedPickOrderLine?.id, | |||
| selectedLot?.lotId, | |||
| pickOrderId, | |||
| pickOrderCreateDate, | |||
| ]); | |||
| const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { | |||
| setFormData(prev => ({ ...prev, [field]: value })); | |||
| if (errors[field as keyof FormErrors]) { | |||
| setErrors(prev => ({ ...prev, [field]: undefined })); | |||
| } | |||
| }, [errors]); | |||
| const handleInputChange = useCallback( | |||
| (field: keyof PickExecutionIssueData, value: any) => { | |||
| setFormData((prev) => ({ ...prev, [field]: value })); | |||
| if (errors[field as keyof FormErrors]) { | |||
| setErrors((prev) => ({ ...prev, [field]: undefined })); | |||
| } | |||
| }, | |||
| [errors] | |||
| ); | |||
| // Updated validation logic | |||
| const validateForm = (): boolean => { | |||
| const newErrors: FormErrors = {}; | |||
| const req = selectedLot?.requiredQty || 0; | |||
| const ap = Number(formData.actualPickQty) || 0; | |||
| const miss = Number(formData.missQty) || 0; | |||
| const bad = Number(formData.badItemQty) || 0; | |||
| const total = ap + miss + bad; | |||
| const badItem = Number(formData.badItemQty) || 0; | |||
| const badPackage = Number((formData as any).badPackageQty) || 0; | |||
| const totalBad = badItem + badPackage; | |||
| const total = ap + miss + totalBad; | |||
| const availableQty = selectedLot?.availableQty || 0; | |||
| // 1. Check actualPickQty cannot be negative | |||
| if (ap < 0) { | |||
| newErrors.actualPickQty = t('Qty cannot be negative'); | |||
| newErrors.actualPickQty = t("Qty cannot be negative"); | |||
| } | |||
| // 2. Check actualPickQty cannot exceed available quantity | |||
| if (ap > availableQty) { | |||
| newErrors.actualPickQty = t('Actual pick qty cannot exceed available qty'); | |||
| newErrors.actualPickQty = t("Actual pick qty cannot exceed available qty"); | |||
| } | |||
| // 3. Check missQty and badItemQty cannot be negative | |||
| // 3. Check missQty and both bad qtys cannot be negative | |||
| if (miss < 0) { | |||
| newErrors.missQty = t('Invalid qty'); | |||
| newErrors.missQty = t("Invalid qty"); | |||
| } | |||
| if (bad < 0) { | |||
| newErrors.badItemQty = t('Invalid qty'); | |||
| if (badItem < 0 || badPackage < 0) { | |||
| newErrors.badItemQty = t("Invalid qty"); | |||
| } | |||
| // 4. NEW: Total (actualPickQty + missQty + badItemQty) cannot exceed lot available qty | |||
| // 4. Total (actualPickQty + missQty + badItemQty + badPackageQty) cannot exceed lot available qty | |||
| if (total > availableQty) { | |||
| const errorMsg = t('Total qty (actual pick + miss + bad) cannot exceed available qty: {available}', { available: availableQty }); | |||
| const errorMsg = t( | |||
| "Total qty (actual pick + miss + bad) cannot exceed available qty: {available}", | |||
| { available: availableQty } | |||
| ); | |||
| newErrors.actualPickQty = errorMsg; | |||
| newErrors.missQty = errorMsg; | |||
| newErrors.badItemQty = errorMsg; | |||
| } | |||
| // 5. If badItemQty > 0, badReason is required | |||
| if (bad > 0 && !formData.badReason) { | |||
| newErrors.badReason = t('Bad reason is required when bad item qty > 0'); | |||
| newErrors.badItemQty = t('Bad reason is required'); | |||
| } | |||
| // 6. At least one field must have a value | |||
| if (ap === 0 && miss === 0 && bad === 0) { | |||
| newErrors.actualPickQty = t('Enter pick qty or issue qty'); | |||
| // 5. At least one field must have a value | |||
| if (ap === 0 && miss === 0 && totalBad === 0) { | |||
| newErrors.actualPickQty = t("Enter pick qty or issue qty"); | |||
| } | |||
| setErrors(newErrors); | |||
| return Object.keys(newErrors).length === 0; | |||
| }; | |||
| const handleSubmit = async () => { | |||
| if (!validateForm()) { | |||
| console.error('Form validation failed:', errors); | |||
| console.error("Form validation failed:", errors); | |||
| return; | |||
| } | |||
| if (!formData.pickOrderId) { | |||
| console.error('Missing pickOrderId'); | |||
| console.error("Missing pickOrderId"); | |||
| return; | |||
| } | |||
| const badItem = Number(formData.badItemQty) || 0; | |||
| const badPackage = Number((formData as any).badPackageQty) || 0; | |||
| const totalBadQty = badItem + badPackage; | |||
| let badReason: string | undefined; | |||
| if (totalBadQty > 0) { | |||
| // assumption: only one of them is > 0 | |||
| badReason = badPackage > 0 ? "package_problem" : "quantity_problem"; | |||
| } | |||
| const submitData: PickExecutionIssueData = { | |||
| ...(formData as PickExecutionIssueData), | |||
| badItemQty: totalBadQty, | |||
| badReason, | |||
| }; | |||
| setLoading(true); | |||
| try { | |||
| await onSubmit(formData as PickExecutionIssueData); | |||
| await onSubmit(submitData); | |||
| } catch (error: any) { | |||
| console.error('Error submitting pick execution issue:', error); | |||
| alert(t('Failed to submit issue. Please try again.') + (error.message ? `: ${error.message}` : '')); | |||
| console.error("Error submitting pick execution issue:", error); | |||
| alert( | |||
| t("Failed to submit issue. Please try again.") + | |||
| (error.message ? `: ${error.message}` : "") | |||
| ); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| @@ -239,11 +274,15 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); | |||
| const requiredQty = calculateRequiredQty(selectedLot); | |||
| return ( | |||
| <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | |||
| <DialogTitle> | |||
| {t('Pick Execution Issue Form')} | |||
| {t("Pick Execution Issue Form") } | |||
| <br /> | |||
| {selectedPickOrderLine.itemCode+ " "+ selectedPickOrderLine.itemName} | |||
| <br /> | |||
| {selectedLot.lotNo} | |||
| </DialogTitle> | |||
| <DialogContent> | |||
| <Box sx={{ mt: 2 }}> | |||
| @@ -251,17 +290,17 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('Required Qty')} | |||
| value={selectedLot?.requiredQty || 0} | |||
| label={t("Required Qty")} | |||
| value={requiredQty} | |||
| disabled | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('Remaining Available Qty')} | |||
| label={t("Remaining Available Qty")} | |||
| value={remainingAvailableQty} | |||
| disabled | |||
| variant="outlined" | |||
| @@ -269,43 +308,53 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('Actual Pick Qty')} | |||
| type="number" | |||
| inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }} | |||
| value={formData.actualPickQty ?? ''} | |||
| onChange={(e) => handleInputChange('actualPickQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} | |||
| error={!!errors.actualPickQty} | |||
| helperText={errors.actualPickQty || `${t('Max')}: ${remainingAvailableQty}`} | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t('Reason')}</InputLabel> | |||
| <Select | |||
| value={formData.reason || ''} | |||
| onChange={(e) => handleInputChange('reason', e.target.value)} | |||
| label={t('Reason')} | |||
| > | |||
| <MenuItem value="">{t('Select Reason')}</MenuItem> | |||
| <MenuItem value="miss">{t('Edit')}</MenuItem> | |||
| <MenuItem value="bad">{t('Just Complete')}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| fullWidth | |||
| label={t("Actual Pick Qty")} | |||
| type="number" | |||
| inputProps={{ | |||
| inputMode: "numeric", | |||
| pattern: "[0-9]*", | |||
| min: 0, | |||
| }} | |||
| value={formData.actualPickQty ?? ""} | |||
| onChange={(e) => | |||
| handleInputChange( | |||
| "actualPickQty", | |||
| e.target.value === "" | |||
| ? undefined | |||
| : Math.max(0, Number(e.target.value) || 0) | |||
| ) | |||
| } | |||
| error={!!errors.actualPickQty} | |||
| helperText={ | |||
| errors.actualPickQty || `${t("Max")}: ${remainingAvailableQty}` | |||
| } | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('Missing item Qty')} | |||
| label={t("Missing item Qty")} | |||
| type="number" | |||
| inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }} | |||
| inputProps={{ | |||
| inputMode: "numeric", | |||
| pattern: "[0-9]*", | |||
| min: 0, | |||
| }} | |||
| value={formData.missQty || 0} | |||
| onChange={(e) => handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} | |||
| onChange={(e) => | |||
| handleInputChange( | |||
| "missQty", | |||
| e.target.value === "" | |||
| ? undefined | |||
| : Math.max(0, Number(e.target.value) || 0) | |||
| ) | |||
| } | |||
| error={!!errors.missQty} | |||
| variant="outlined" | |||
| /> | |||
| @@ -314,53 +363,76 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| label={t('Bad Item Qty')} | |||
| label={t("Bad Item Qty")} | |||
| type="number" | |||
| inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }} | |||
| inputProps={{ | |||
| inputMode: "numeric", | |||
| pattern: "[0-9]*", | |||
| min: 0, | |||
| }} | |||
| value={formData.badItemQty || 0} | |||
| onChange={(e) => handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} | |||
| onChange={(e) => | |||
| handleInputChange( | |||
| "badItemQty", | |||
| e.target.value === "" | |||
| ? undefined | |||
| : Math.max(0, Number(e.target.value) || 0) | |||
| ) | |||
| } | |||
| error={!!errors.badItemQty} | |||
| //helperText={t("Quantity Problem")} | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| {/* Show bad reason dropdown when badItemQty > 0 */} | |||
| {(formData.badItemQty && formData.badItemQty > 0) ? ( | |||
| <Grid item xs={12}> | |||
| <FormControl fullWidth error={!!errors.badReason}> | |||
| <InputLabel>{t('Bad Reason')}</InputLabel> | |||
| <Select | |||
| value={formData.badReason || ''} | |||
| onChange={(e) => handleInputChange('badReason', e.target.value)} | |||
| label={t('Bad Reason')} | |||
| > | |||
| <MenuItem value="">{t('Select Bad Reason')}</MenuItem> | |||
| <MenuItem value="quantity_problem">{t('Quantity Problem')}</MenuItem> | |||
| <MenuItem value="package_problem">{t('Package Problem')}</MenuItem> | |||
| </Select> | |||
| {errors.badReason && ( | |||
| <Typography variant="caption" color="error" sx={{ mt: 0.5, ml: 1.75 }}> | |||
| {errors.badReason} | |||
| </Typography> | |||
| )} | |||
| </FormControl> | |||
| </Grid> | |||
| ) : null} | |||
| <Grid item xs={12}> | |||
| <TextField | |||
| fullWidth | |||
| label={t("Bad Package Qty")} | |||
| type="number" | |||
| inputProps={{ | |||
| inputMode: "numeric", | |||
| pattern: "[0-9]*", | |||
| min: 0, | |||
| }} | |||
| value={(formData as any).badPackageQty || 0} | |||
| onChange={(e) => | |||
| handleInputChange( | |||
| "badPackageQty", | |||
| e.target.value === "" | |||
| ? undefined | |||
| : Math.max(0, Number(e.target.value) || 0) | |||
| ) | |||
| } | |||
| error={!!errors.badItemQty} | |||
| //helperText={t("Package Problem")} | |||
| variant="outlined" | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <FormControl fullWidth> | |||
| <InputLabel>{t("Remark")}</InputLabel> | |||
| <Select | |||
| value={formData.reason || ""} | |||
| onChange={(e) => handleInputChange("reason", e.target.value)} | |||
| label={t("Remark")} | |||
| > | |||
| <MenuItem value="">{t("Select Remark")}</MenuItem> | |||
| <MenuItem value="miss">{t("Edit")}</MenuItem> | |||
| <MenuItem value="bad">{t("Just Complete")}</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| </Grid> | |||
| </Box> | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={handleClose} disabled={loading}> | |||
| {t('Cancel')} | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| onClick={handleSubmit} | |||
| variant="contained" | |||
| disabled={loading} | |||
| > | |||
| {loading ? t('submitting') : t('submit')} | |||
| <Button onClick={handleSubmit} variant="contained" disabled={loading}> | |||
| {loading ? t("submitting") : t("submit")} | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| @@ -19,6 +19,7 @@ import { | |||
| TablePagination, | |||
| Modal, | |||
| Chip, | |||
| LinearProgress, | |||
| } from "@mui/material"; | |||
| import dayjs from 'dayjs'; | |||
| import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; | |||
| @@ -78,7 +79,33 @@ interface Props { | |||
| onSwitchToRecordTab?: () => void; | |||
| onRefreshReleasedOrderCount?: () => void; | |||
| } | |||
| const LinearProgressWithLabel: React.FC<{ completed: number; total: number }> = ({ completed, total }) => { | |||
| const { t } = useTranslation(["pickOrder", "do"]); | |||
| const progress = total > 0 ? (completed / total) * 100 : 0; | |||
| return ( | |||
| <Box sx={{ width: '100%', mb: 2 }}> | |||
| <Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}> | |||
| <Box sx={{ width: '100%', mr: 1 }}> | |||
| <LinearProgress | |||
| variant="determinate" | |||
| value={progress} | |||
| sx={{ | |||
| height: 30, // ✅ Increase height from default (4px) to 10px | |||
| borderRadius: 5, // ✅ Add rounded corners | |||
| }} | |||
| /> | |||
| </Box> | |||
| <Box sx={{ minWidth: 80 }}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| <strong>{t("Progress")}: {completed}/{total}</strong> | |||
| </Typography> | |||
| </Box> | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| }; | |||
| // QR Code Modal Component (from LotTable) | |||
| const QrCodeModal: React.FC<{ | |||
| open: boolean; | |||
| @@ -86,7 +113,8 @@ const QrCodeModal: React.FC<{ | |||
| lot: any | null; | |||
| onQrCodeSubmit: (lotNo: string) => void; | |||
| combinedLotData: any[]; // Add this prop | |||
| }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { | |||
| lotConfirmationOpen: boolean; | |||
| }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData,lotConfirmationOpen = false }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | |||
| const [manualInput, setManualInput] = useState<string>(''); | |||
| @@ -100,8 +128,20 @@ const QrCodeModal: React.FC<{ | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [scannedQrResult, setScannedQrResult] = useState<string>(''); | |||
| const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null); | |||
| const fetchingRef = useRef<Set<number>>(new Set()); | |||
| // Process scanned QR codes | |||
| useEffect(() => { | |||
| // ✅ Don't process if modal is not open | |||
| if (!open) { | |||
| return; | |||
| } | |||
| // ✅ Don't process if lot confirmation modal is open | |||
| if (lotConfirmationOpen) { | |||
| console.log("Lot confirmation modal is open, skipping QrCodeModal processing..."); | |||
| return; | |||
| } | |||
| if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { | |||
| const latestQr = qrValues[qrValues.length - 1]; | |||
| @@ -110,17 +150,39 @@ const QrCodeModal: React.FC<{ | |||
| return; | |||
| } | |||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||
| try { | |||
| const qrData = JSON.parse(latestQr); | |||
| if (qrData.stockInLineId && qrData.itemId) { | |||
| // ✅ Check if we're already fetching this stockInLineId | |||
| if (fetchingRef.current.has(qrData.stockInLineId)) { | |||
| console.log(`⏱️ [QR MODAL] Already fetching stockInLineId: ${qrData.stockInLineId}, skipping duplicate call`); | |||
| return; | |||
| } | |||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||
| setIsProcessingQr(true); | |||
| setQrScanFailed(false); | |||
| // ✅ Mark as fetching | |||
| fetchingRef.current.add(qrData.stockInLineId); | |||
| const fetchStartTime = performance.now(); | |||
| console.log(`⏱️ [QR MODAL] Starting fetchStockInLineInfo for stockInLineId: ${qrData.stockInLineId}`); | |||
| fetchStockInLineInfo(qrData.stockInLineId) | |||
| .then((stockInLineInfo) => { | |||
| // ✅ Remove from fetching set | |||
| fetchingRef.current.delete(qrData.stockInLineId); | |||
| // ✅ Check again if modal is still open and lot confirmation is not open | |||
| if (!open || lotConfirmationOpen) { | |||
| console.log("Modal state changed, skipping result processing"); | |||
| return; | |||
| } | |||
| const fetchTime = performance.now() - fetchStartTime; | |||
| console.log(`⏱️ [QR MODAL] fetchStockInLineInfo time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`); | |||
| console.log("Stock in line info:", stockInLineInfo); | |||
| setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); | |||
| @@ -138,7 +200,17 @@ const QrCodeModal: React.FC<{ | |||
| } | |||
| }) | |||
| .catch((error) => { | |||
| console.error("Error fetching stock in line info:", error); | |||
| // ✅ Remove from fetching set | |||
| fetchingRef.current.delete(qrData.stockInLineId); | |||
| // ✅ Check again if modal is still open | |||
| if (!open || lotConfirmationOpen) { | |||
| console.log("Modal state changed, skipping error handling"); | |||
| return; | |||
| } | |||
| const fetchTime = performance.now() - fetchStartTime; | |||
| console.error(`❌ [QR MODAL] fetchStockInLineInfo failed after ${fetchTime.toFixed(2)}ms:`, error); | |||
| setScannedQrResult('Error fetching data'); | |||
| setQrScanFailed(true); | |||
| setManualInputError(true); | |||
| @@ -179,7 +251,7 @@ const QrCodeModal: React.FC<{ | |||
| } | |||
| } | |||
| } | |||
| }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]); | |||
| }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, lotConfirmationOpen, open]); | |||
| // Clear states when modal opens | |||
| useEffect(() => { | |||
| @@ -477,7 +549,7 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||
| const [paginationController, setPaginationController] = useState({ | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| pageSize: -1, | |||
| }); | |||
| const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||
| @@ -515,12 +587,79 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); | |||
| // TODO: Implement QR code functionality | |||
| }; | |||
| const progress = useMemo(() => { | |||
| if (combinedLotData.length === 0) { | |||
| return { completed: 0, total: 0 }; | |||
| } | |||
| const nonPendingCount = combinedLotData.filter(lot => { | |||
| const status = lot.stockOutLineStatus?.toLowerCase(); | |||
| return status !== 'pending'; | |||
| }).length; | |||
| return { | |||
| completed: nonPendingCount, | |||
| total: combinedLotData.length | |||
| }; | |||
| }, [combinedLotData]); | |||
| const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { | |||
| const mismatchStartTime = performance.now(); | |||
| console.log(`⏱️ [HANDLE LOT MISMATCH START]`); | |||
| console.log("Lot mismatch detected:", { expectedLot, scannedLot }); | |||
| setExpectedLotData(expectedLot); | |||
| setScannedLotData(scannedLot); | |||
| setLotConfirmationOpen(true); | |||
| // Check if we need to fetch scanned lot info | |||
| const needsFetch = !scannedLot.lotNo && scannedLot.stockInLineId; | |||
| if (needsFetch) { | |||
| console.log(`⏱️ [HANDLE LOT MISMATCH] Need to fetch lot info for stockInLineId: ${scannedLot.stockInLineId}`); | |||
| const fetchStartTime = performance.now(); | |||
| fetchStockInLineInfo(scannedLot.stockInLineId) | |||
| .then((stockInLineInfo) => { | |||
| const fetchTime = performance.now() - fetchStartTime; | |||
| console.log(`⏱️ [HANDLE LOT MISMATCH] fetchStockInLineInfo time: ${fetchTime.toFixed(2)}ms (${(fetchTime / 1000).toFixed(3)}s)`); | |||
| console.log("Stock in line info:", stockInLineInfo); | |||
| const updateStartTime = performance.now(); | |||
| setExpectedLotData(expectedLot); | |||
| setScannedLotData({ | |||
| ...scannedLot, | |||
| lotNo: stockInLineInfo.lotNo || null, | |||
| }); | |||
| setLotConfirmationOpen(true); | |||
| const updateTime = performance.now() - updateStartTime; | |||
| console.log(`⏱️ [HANDLE LOT MISMATCH] State update time: ${updateTime.toFixed(2)}ms`); | |||
| const totalTime = performance.now() - mismatchStartTime; | |||
| console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); | |||
| console.log(`📊 Breakdown: fetch=${fetchTime.toFixed(2)}ms, update=${updateTime.toFixed(2)}ms`); | |||
| }) | |||
| .catch((error) => { | |||
| const fetchTime = performance.now() - fetchStartTime; | |||
| console.error(`❌ [HANDLE LOT MISMATCH] fetchStockInLineInfo failed after ${fetchTime.toFixed(2)}ms:`, error); | |||
| // Still open modal with partial data | |||
| setExpectedLotData(expectedLot); | |||
| setScannedLotData(scannedLot); | |||
| setLotConfirmationOpen(true); | |||
| const totalTime = performance.now() - mismatchStartTime; | |||
| console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time (with error): ${totalTime.toFixed(2)}ms`); | |||
| }); | |||
| } else { | |||
| // No fetch needed, open modal immediately | |||
| const updateStartTime = performance.now(); | |||
| setExpectedLotData(expectedLot); | |||
| setScannedLotData(scannedLot); | |||
| setLotConfirmationOpen(true); | |||
| const updateTime = performance.now() - updateStartTime; | |||
| console.log(`⏱️ [HANDLE LOT MISMATCH] State update time (no fetch): ${updateTime.toFixed(2)}ms`); | |||
| const totalTime = performance.now() - mismatchStartTime; | |||
| console.log(`⏱️ [HANDLE LOT MISMATCH END] Total time: ${totalTime.toFixed(2)}ms`); | |||
| } | |||
| }, []); | |||
| const checkAllLotsCompleted = useCallback((lotData: any[]) => { | |||
| if (lotData.length === 0) { | |||
| @@ -937,18 +1076,15 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| setIsConfirmingLot(true); | |||
| try { | |||
| const newLotNo = scannedLotData?.lotNo; | |||
| if (!newLotNo) { | |||
| console.error("No lot number for scanned lot"); | |||
| alert(t("Cannot find lot number for scanned lot. Please verify the lot number is correct.")); | |||
| setIsConfirmingLot(false); | |||
| return; | |||
| } | |||
| const newStockInLineId = scannedLotData?.stockInLineId; | |||
| await confirmLotSubstitution({ | |||
| pickOrderLineId: selectedLotForQr.pickOrderLineId, | |||
| stockOutLineId: selectedLotForQr.stockOutLineId, | |||
| originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, | |||
| newInventoryLotNo: newLotNo | |||
| newInventoryLotNo: "", | |||
| newStockInLineId: newStockInLineId | |||
| }); | |||
| setQrScanError(false); | |||
| @@ -1261,12 +1397,19 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId }; // ✅ 添加 byStockInLineId | |||
| }, [combinedLotData]); | |||
| const processOutsideQrCode = useCallback(async (latestQr: string) => { | |||
| const totalStartTime = performance.now(); | |||
| console.log(`⏱️ [PROCESS OUTSIDE QR START] QR: ${latestQr.substring(0, 50)}...`); | |||
| console.log(`⏰ Start time: ${new Date().toISOString()}`); | |||
| // 1) Parse JSON safely | |||
| const parseStartTime = performance.now(); | |||
| let qrData: any = null; | |||
| let parseTime = 0; // ✅ Declare parseTime in outer scope | |||
| try { | |||
| qrData = JSON.parse(latestQr); | |||
| parseTime = performance.now() - parseStartTime; // ✅ Assign value | |||
| console.log(`⏱️ JSON parse time: ${parseTime.toFixed(2)}ms`); | |||
| } catch { | |||
| console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches."); | |||
| setQrScanError(true); | |||
| @@ -1287,12 +1430,15 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| const scannedStockInLineId = qrData.stockInLineId; | |||
| // ✅ OPTIMIZATION: 使用索引快速查找相同 item 的 lots | |||
| const lookupStartTime = performance.now(); | |||
| const sameItemLots: any[] = []; | |||
| // 使用索引快速查找 | |||
| if (lotDataIndexes.byItemId.has(scannedItemId)) { | |||
| sameItemLots.push(...lotDataIndexes.byItemId.get(scannedItemId)!); | |||
| } | |||
| const lookupTime = performance.now() - lookupStartTime; | |||
| console.log(`⏱️ Index lookup time: ${lookupTime.toFixed(2)}ms, found ${sameItemLots.length} lots`); | |||
| if (sameItemLots.length === 0) { | |||
| console.error("No item match in expected lots for scanned code"); | |||
| @@ -1302,12 +1448,15 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| // ✅ OPTIMIZATION: 过滤出活跃的 lots(非 rejected) | |||
| const filterStartTime = performance.now(); | |||
| const rejectedStatuses = new Set(['rejected']); | |||
| const activeSuggestedLots = sameItemLots.filter(lot => | |||
| !rejectedStatuses.has(lot.lotAvailability) && | |||
| !rejectedStatuses.has(lot.stockOutLineStatus) && | |||
| !rejectedStatuses.has(lot.processingStatus) | |||
| ); | |||
| const filterTime = performance.now() - filterStartTime; | |||
| console.log(`⏱️ Filter active lots time: ${filterTime.toFixed(2)}ms, active: ${activeSuggestedLots.length}`); | |||
| if (activeSuggestedLots.length === 0) { | |||
| console.error("No active suggested lots found for this item"); | |||
| @@ -1317,10 +1466,13 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| // ✅ OPTIMIZATION: 按优先级查找匹配的 lot | |||
| const matchStartTime = performance.now(); | |||
| // 1. 首先查找 stockInLineId 完全匹配的(正确的 lot) | |||
| let exactMatch = activeSuggestedLots.find(lot => | |||
| lot.stockInLineId === scannedStockInLineId | |||
| ); | |||
| const matchTime = performance.now() - matchStartTime; | |||
| console.log(`⏱️ Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); | |||
| if (exactMatch) { | |||
| // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认 | |||
| @@ -1334,6 +1486,8 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| try { | |||
| const apiStartTime = performance.now(); | |||
| console.log(`⏱️ [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`); | |||
| const res = await updateStockOutLineStatusByQRCodeAndLotNo({ | |||
| pickOrderLineId: exactMatch.pickOrderLineId, | |||
| inventoryLotNo: exactMatch.lotNo, | |||
| @@ -1341,8 +1495,11 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| itemId: exactMatch.itemId, | |||
| status: "checked", | |||
| }); | |||
| const apiTime = performance.now() - apiStartTime; | |||
| console.log(`⏱️ [API CALL END] Total API time: ${apiTime.toFixed(2)}ms (${(apiTime / 1000).toFixed(3)}s)`); | |||
| if (res.code === "checked" || res.code === "SUCCESS") { | |||
| const stateUpdateStartTime = performance.now(); | |||
| setQrScanError(false); | |||
| setQrScanSuccess(true); | |||
| @@ -1371,7 +1528,13 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| return lot; | |||
| })); | |||
| const stateUpdateTime = performance.now() - stateUpdateStartTime; | |||
| console.log(`⏱️ State update time: ${stateUpdateTime.toFixed(2)}ms`); | |||
| const totalTime = performance.now() - totalStartTime; | |||
| console.log(`✅ [PROCESS OUTSIDE QR END] Total time: ${totalTime.toFixed(2)}ms (${(totalTime / 1000).toFixed(3)}s)`); | |||
| console.log(`⏰ End time: ${new Date().toISOString()}`); | |||
| console.log(`📊 Breakdown: parse=${parseTime.toFixed(2)}ms, lookup=${lookupTime.toFixed(2)}ms, filter=${filterTime.toFixed(2)}ms, match=${matchTime.toFixed(2)}ms, api=${apiTime.toFixed(2)}ms, state=${stateUpdateTime.toFixed(2)}ms`); | |||
| console.log("✅ Status updated locally, no full data refresh needed"); | |||
| } else { | |||
| console.warn("Unexpected response code from backend:", res.code); | |||
| @@ -1379,6 +1542,8 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| setQrScanSuccess(false); | |||
| } | |||
| } catch (e) { | |||
| const totalTime = performance.now() - totalStartTime; | |||
| console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`); | |||
| console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| @@ -1417,6 +1582,8 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||
| } | |||
| ); | |||
| } catch (error) { | |||
| const totalTime = performance.now() - totalStartTime; | |||
| console.error(`❌ [PROCESS OUTSIDE QR ERROR] Total time: ${totalTime.toFixed(2)}ms`); | |||
| console.error("Error during QR code processing:", error); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| @@ -1811,13 +1978,16 @@ useEffect(() => { | |||
| const newPageSize = parseInt(event.target.value, 10); | |||
| setPaginationController({ | |||
| pageNum: 0, | |||
| pageSize: newPageSize, | |||
| pageSize: newPageSize === -1 ? -1 : newPageSize, | |||
| }); | |||
| }, []); | |||
| // Pagination data with sorting by routerIndex | |||
| // Remove the sorting logic and just do pagination | |||
| const paginatedData = useMemo(() => { | |||
| if (paginationController.pageSize === -1) { | |||
| return combinedLotData; // Show all items | |||
| } | |||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||
| const endIndex = startIndex + paginationController.pageSize; | |||
| return combinedLotData.slice(startIndex, endIndex); // No sorting needed | |||
| @@ -2340,6 +2510,24 @@ const handleSubmitAllScanned = useCallback(async () => { | |||
| > | |||
| <FormProvider {...formProps}> | |||
| <Stack spacing={2}> | |||
| <Box | |||
| sx={{ | |||
| position: 'fixed', | |||
| top: 0, | |||
| left: 0, | |||
| right: 0, | |||
| zIndex: 1100, // Higher than other elements | |||
| backgroundColor: 'background.paper', | |||
| pt: 2, | |||
| pb: 1, | |||
| px: 2, | |||
| borderBottom: '1px solid', | |||
| borderColor: 'divider', | |||
| boxShadow: '0 2px 4px rgba(0,0,0,0.1)', | |||
| }} | |||
| > | |||
| <LinearProgressWithLabel completed={progress.completed} total={progress.total} /> | |||
| </Box> | |||
| {/* DO Header */} | |||
| @@ -2543,7 +2731,7 @@ paginatedData.map((lot, index) => { | |||
| }} | |||
| > | |||
| {lot.lotNo || | |||
| t('⚠️ No Stock Available')} | |||
| t('No Stock Available')} | |||
| </Typography> | |||
| </Box> | |||
| </TableCell> | |||
| @@ -2698,7 +2886,7 @@ paginatedData.map((lot, index) => { | |||
| }} | |||
| title="Report missing or bad items" | |||
| > | |||
| {t("Issue")} | |||
| {t("Edit")} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| @@ -2707,7 +2895,7 @@ paginatedData.map((lot, index) => { | |||
| disabled={lot.stockOutLineStatus === 'completed'} | |||
| sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} | |||
| > | |||
| {t("Skip")} | |||
| {t("Just Completed")} | |||
| </Button> | |||
| </Stack> | |||
| ); | |||
| @@ -2729,7 +2917,7 @@ paginatedData.map((lot, index) => { | |||
| rowsPerPage={paginationController.pageSize} | |||
| onPageChange={handlePageChange} | |||
| onRowsPerPageChange={handlePageSizeChange} | |||
| rowsPerPageOptions={[10, 25, 50]} | |||
| rowsPerPageOptions={[10, 25, 50,-1]} | |||
| labelRowsPerPage={t("Rows per page")} | |||
| labelDisplayedRows={({ from, to, count }) => | |||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||
| @@ -2750,6 +2938,7 @@ paginatedData.map((lot, index) => { | |||
| lot={selectedLotForQr} | |||
| combinedLotData={combinedLotData} | |||
| onQrCodeSubmit={handleQrCodeSubmitFromModal} | |||
| lotConfirmationOpen={lotConfirmationOpen} // ✅ Add this prop | |||
| /> | |||
| <ManualLotConfirmationModal | |||
| open={manualLotConfirmationOpen} | |||
| @@ -640,7 +640,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||
| variant="contained" | |||
| color="primary" | |||
| onClick={() => handleRelease(jobOrderId)} | |||
| disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"} | |||
| //disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"} | |||
| > | |||
| {t("Release")} | |||
| </Button> | |||
| @@ -79,6 +79,25 @@ | |||
| "Tomorrow": "Tomorrow", | |||
| "Day After Tomorrow": "Day After Tomorrow", | |||
| "Goods Receipt Status": "Goods Receipt Status", | |||
| "Date": "Date", | |||
| "Time": "Time", | |||
| "Allow to select Date to view history.": "Allow to select Date to view history.", | |||
| "Auto-refresh every 15 minutes": "Auto-refresh every 15 minutes", | |||
| "Exit Screen": "Exit Screen", | |||
| "Restore Screen": "Restore Screen", | |||
| "Screen cleared": "Screen cleared", | |||
| "Supplier": "Supplier", | |||
| "Expected No. of Delivery": "Expected No. of Delivery", | |||
| "No. of Orders Received at Dock": "No. of Orders Received at Dock", | |||
| "No. of Items Inspected": "No. of Items Inspected", | |||
| "No. of Items with IQC Issue": "No. of Items with IQC Issue", | |||
| "No. of Items Completed Put Away at Store": "No. of Items Completed Put Away at Store", | |||
| "Show Supplier Name": "Show Supplier Name", | |||
| "Based on Expected Delivery Date": "Based on Expected Delivery Date", | |||
| "Upon entry of DN and Lot No. for all items of the order": "Upon entry of DN and Lot No. for all items of the order", | |||
| "Upon any IQC decision received": "Upon any IQC decision received", | |||
| "Count any item with IQC defect in any IQC criteria": "Count any item with IQC defect in any IQC criteria", | |||
| "Upon completion of put away for an material in order. Count no. of items being put away": "Upon completion of put away for an material in order. Count no. of items being put away", | |||
| "Filter": "Filter", | |||
| "All": "All", | |||
| "Column 1": "Column 1", | |||
| @@ -79,6 +79,25 @@ | |||
| "Tomorrow": "翌日", | |||
| "Day After Tomorrow": "後日", | |||
| "Goods Receipt Status": "貨物接收狀態", | |||
| "Date": "日期", | |||
| "Time": "時間", | |||
| "Allow to select Date to view history.": "可選擇日期查看歷史記錄。", | |||
| "Auto-refresh every 15 minutes": "每15分鐘自動刷新", | |||
| "Exit Screen": "退出畫面", | |||
| "Restore Screen": "恢復畫面", | |||
| "Screen cleared": "畫面已清除", | |||
| "Supplier": "供應商", | |||
| "Expected No. of Delivery": "預計送貨單數", | |||
| "No. of Orders Received at Dock": "已收訂單數", | |||
| "No. of Items Inspected": "已檢驗貨品數", | |||
| "No. of Items with IQC Issue": "IQC異常貨品數", | |||
| "No. of Items Completed Put Away at Store": "已完成上架貨品數", | |||
| "Show Supplier Name": "顯示供應商名稱", | |||
| "Based on Expected Delivery Date": "按預計送貨日期統計", | |||
| "Upon entry of DN and Lot No. for all items of the order": "當訂單所有貨品已輸入DN及批號時", | |||
| "Upon any IQC decision received": "當收到任何IQC判定", | |||
| "Count any item with IQC defect in any IQC criteria": "統計任何IQC準則不合格的貨品", | |||
| "Upon completion of put away for an material in order. Count no. of items being put away": "當訂單物料完成上架。統計正在上架的貨品數", | |||
| "Filter": "篩選", | |||
| "All": "全部", | |||
| "Column 1": "欄位1", | |||
| @@ -11,14 +11,24 @@ | |||
| "Status": "來貨狀態", | |||
| "Order Date From": "訂單日期", | |||
| "Delivery Order Code": "送貨訂單編號", | |||
| "Select Remark": "選擇備註", | |||
| "Confirm Assignment": "確認分配", | |||
| "Required Date": "所需日期", | |||
| "Store": "位置", | |||
| "Lane Code": "車線號碼", | |||
| "Available Orders": "可用訂單", | |||
| "Just Complete": "已完成", | |||
| "Order Date To": "訂單日期至", | |||
| "Warning: Some delivery orders do not have matching trucks for the target date.": "警告:部分送貨訂單於目標日期沒有可匹配的車輛。", | |||
| "Truck Availability Warning": "車輛可用性警告", | |||
| "Problem DO(s): ": "問題送貨訂單", | |||
| "Fetching all matching records...": "正在獲取所有匹配的記錄...", | |||
| "Progress": "進度", | |||
| "Loading...": "正在加載...", | |||
| "Available Trucks": "可用車輛", | |||
| "No trucks available": "沒有車輛可用", | |||
| "Remark": "備註", | |||
| "Just Completed": "已完成", | |||
| "Code": "門店訂單編號", | |||
| "code": "門店訂單編號", | |||
| "Create": "新增", | |||
| @@ -9,12 +9,22 @@ | |||
| "Status": "來貨狀態", | |||
| "N/A": "不適用", | |||
| "Release Pick Orders": "放單", | |||
| "Remark": "備註", | |||
| "Escalated": "上報狀態", | |||
| "NotEscalated": "無上報", | |||
| "Assigned To": "已分配", | |||
| "Progress": "進度", | |||
| "Select Remark": "選擇備註", | |||
| "Just Complete": "已完成", | |||
| "Skip": "跳過", | |||
| "Confirm Assignment": "確認分配", | |||
| "Required Date": "所需日期", | |||
| "Store": "位置", | |||
| "Available Orders": "可用訂單", | |||
| "Lane Code": "車線號碼", | |||
| "Fetching all matching records...": "正在獲取所有匹配的記錄...", | |||
| "Edit": "改數", | |||
| "Just Completed": "已完成", | |||
| "Do you want to start?": "確定開始嗎?", | |||
| "Start": "開始", | |||
| "Pick Order Code(s)": "提料單編號", | |||
| @@ -257,9 +267,11 @@ | |||
| "Pick Execution Issue Form":"提料問題表單", | |||
| "This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。", | |||
| "Bad item Qty":"不良貨品數量", | |||
| "Missing item Qty":"缺少貨品數量", | |||
| "Missing item Qty":"貨品遺失數量", | |||
| "Missing Item Qty":"貨品遺失數量", | |||
| "Bad Item Qty":"不良貨品數量", | |||
| "Missing Item Qty":"缺少貨品數量", | |||
| "Bad Package Qty":"不良包裝數量", | |||
| "Actual Pick Qty":"實際提料數量", | |||
| "Required Qty":"所需數量", | |||
| "Issue Remark":"問題描述", | |||