| @@ -45,3 +45,15 @@ const qcItemAll: React.FC = async () => { | |||||
| export default qcItemAll; | 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; | actualPickQty: number; | ||||
| missQty: number; | missQty: number; | ||||
| badItemQty: number; | badItemQty: number; | ||||
| badPackageQty?: number; | |||||
| issueRemark: string; | issueRemark: string; | ||||
| pickerName: string; | pickerName: string; | ||||
| handledBy?: number; | handledBy?: number; | ||||
| @@ -996,6 +997,7 @@ export interface LotSubstitutionConfirmRequest { | |||||
| stockOutLineId: number; | stockOutLineId: number; | ||||
| originalSuggestedPickLotId: number; | originalSuggestedPickLotId: number; | ||||
| newInventoryLotNo: string; | newInventoryLotNo: string; | ||||
| newStockInLineId: number; | |||||
| } | } | ||||
| export const confirmLotSubstitution = async (data: LotSubstitutionConfirmRequest) => { | export const confirmLotSubstitution = async (data: LotSubstitutionConfirmRequest) => { | ||||
| const response = await serverFetchJson<PostPickOrderResponse>( | const response = await serverFetchJson<PostPickOrderResponse>( | ||||
| @@ -2,23 +2,43 @@ | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { ThemeProvider } from "@mui/material/styles"; | import { ThemeProvider } from "@mui/material/styles"; | ||||
| import theme from "../../theme"; | 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 { 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 DashboardProgressChart from "./chart/DashboardProgressChart"; | ||||
| import DashboardLineChart from "./chart/DashboardLineChart"; | import DashboardLineChart from "./chart/DashboardLineChart"; | ||||
| import PendingInspectionChart from "./chart/PendingInspectionChart"; | import PendingInspectionChart from "./chart/PendingInspectionChart"; | ||||
| import PendingStorageChart from "./chart/PendingStorageChart"; | import PendingStorageChart from "./chart/PendingStorageChart"; | ||||
| import ApplicationCompletionChart from "./chart/ApplicationCompletionChart"; | import ApplicationCompletionChart from "./chart/ApplicationCompletionChart"; | ||||
| import OrderCompletionChart from "./chart/OrderCompletionChart"; | import OrderCompletionChart from "./chart/OrderCompletionChart"; | ||||
| import DashboardBox from "./Dashboardbox"; | |||||
| 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"; | import { TruckScheduleDashboard } from "./truckSchedule"; | ||||
| import { GoodsReceiptStatus } from "./goodsReceiptStatus"; | 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 = { | type Props = { | ||||
| // iqc: IQCItems[] | undefined | // iqc: IQCItems[] | undefined | ||||
| escalationLogs: EscalationResult[] | escalationLogs: EscalationResult[] | ||||
| @@ -32,6 +52,8 @@ const DashboardPage: React.FC<Props> = ({ | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const [escLog, setEscLog] = useState<EscalationResult[]>([]); | const [escLog, setEscLog] = useState<EscalationResult[]>([]); | ||||
| const [currentTab, setCurrentTab] = useState(0); | |||||
| const [showCompletedLogs, setShowCompletedLogs] = useState(false); | |||||
| const getPendingLog = () => { | const getPendingLog = () => { | ||||
| return escLog.filter(esc => esc.status == "pending"); | return escLog.filter(esc => esc.status == "pending"); | ||||
| @@ -41,35 +63,66 @@ const DashboardPage: React.FC<Props> = ({ | |||||
| setEscLog(escalationLogs); | setEscLog(escalationLogs); | ||||
| }, [escalationLogs]) | }, [escalationLogs]) | ||||
| const handleTabChange = (event: React.SyntheticEvent, newValue: number) => { | |||||
| setCurrentTab(newValue); | |||||
| }; | |||||
| const handleFilterChange = (checked: boolean) => { | |||||
| setShowCompletedLogs(checked); | |||||
| }; | |||||
| return ( | return ( | ||||
| <ThemeProvider theme={theme}> | <ThemeProvider theme={theme}> | ||||
| <Grid container spacing={2}> | <Grid container spacing={2}> | ||||
| <Grid item xs={12}> | <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> | <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> | </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> | </Grid> | ||||
| {/* Hidden: Progress chart - not in use currently */} | {/* Hidden: Progress chart - not in use currently */} | ||||
| {/* <Grid item xs={12}> | {/* <Grid item xs={12}> | ||||
| @@ -1,13 +1,9 @@ | |||||
| "use client"; | "use client"; | ||||
| import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; | |||||
| import React, { useState, useEffect, useCallback, useMemo } from 'react'; | |||||
| import { | import { | ||||
| Box, | Box, | ||||
| Typography, | Typography, | ||||
| FormControl, | |||||
| InputLabel, | |||||
| Select, | |||||
| MenuItem, | |||||
| Card, | Card, | ||||
| CardContent, | CardContent, | ||||
| Stack, | Stack, | ||||
| @@ -19,88 +15,112 @@ import { | |||||
| TableRow, | TableRow, | ||||
| Paper, | Paper, | ||||
| CircularProgress, | CircularProgress, | ||||
| Chip | |||||
| Button | |||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import dayjs from 'dayjs'; | 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 GoodsReceiptStatus: React.FC = () => { | ||||
| const { t } = useTranslation("dashboard"); | 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 () => { | const loadData = useCallback(async () => { | ||||
| if (screenCleared) return; | |||||
| try { | try { | ||||
| setData([]); | |||||
| setLoading(true); | |||||
| const dateParam = selectedDate.format('YYYY-MM-DD'); | |||||
| const result = await fetchGoodsReceiptStatusClient(dateParam); | |||||
| setData(result ?? []); | |||||
| setLastUpdated(dayjs()); | |||||
| } catch (error) { | } catch (error) { | ||||
| console.error('Error fetching goods receipt status:', error); | console.error('Error fetching goods receipt status:', error); | ||||
| setData([]); | |||||
| } finally { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| }, []); | |||||
| }, [selectedDate, screenCleared]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if (screenCleared) return; | |||||
| loadData(); | loadData(); | ||||
| const refreshInterval = setInterval(() => { | const refreshInterval = setInterval(() => { | ||||
| loadData(); | loadData(); | ||||
| }, 5 * 60 * 1000); | |||||
| }, REFRESH_MS); | |||||
| return () => clearInterval(refreshInterval); | 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 ( | return ( | ||||
| <Card sx={{ mb: 2 }}> | <Card sx={{ mb: 2 }}> | ||||
| <CardContent> | <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> | </Typography> | ||||
| <Button variant="outlined" color="inherit" onClick={() => setScreenCleared(true)}> | |||||
| {t("Exit Screen")} | |||||
| </Button> | |||||
| </Stack> | </Stack> | ||||
| {/* Table */} | {/* Table */} | ||||
| @@ -114,38 +134,80 @@ const GoodsReceiptStatus: React.FC = () => { | |||||
| <Table size="small" sx={{ minWidth: 1200 }}> | <Table size="small" sx={{ minWidth: 1200 }}> | ||||
| <TableHead> | <TableHead> | ||||
| <TableRow sx={{ backgroundColor: 'grey.100' }}> | <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> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| {filteredData.length === 0 ? ( | |||||
| {data.length === 0 ? ( | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={3} align="center"> | |||||
| <TableCell colSpan={6} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("No data available")} | |||||
| {t("No data available")} ({selectedDateLabel}) | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| ) : ( | ) : ( | ||||
| filteredData.map((row, index) => ( | |||||
| data.map((row, index) => ( | |||||
| <TableRow | <TableRow | ||||
| key={row.id || index} | |||||
| key={`${row.supplierId ?? 'na'}-${index}`} | |||||
| sx={{ | sx={{ | ||||
| '&:hover': { backgroundColor: 'grey.50' } | '&:hover': { backgroundColor: 'grey.50' } | ||||
| }} | }} | ||||
| > | > | ||||
| <TableCell> | <TableCell> | ||||
| {/* TODO: Add table cell content when implementing */} | |||||
| - | |||||
| {row.supplierName || '-'} | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell> | |||||
| - | |||||
| <TableCell align="center"> | |||||
| {row.expectedNoOfDelivery ?? 0} | |||||
| </TableCell> | </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> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| )) | )) | ||||
| @@ -57,72 +57,119 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| loadSummaries(); | loadSummaries(); | ||||
| }, [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({ | 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"), | confirmButtonText: t("Confirm"), | ||||
| confirmButtonColor: "#8dba00" | 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 | // Flatten rows to create one box per lane | ||||
| const flattenRows = (rows: any[]) => { | const flattenRows = (rows: any[]) => { | ||||
| @@ -296,7 +343,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| variant="outlined" | variant="outlined" | ||||
| size="medium" | size="medium" | ||||
| disabled={item.lane.unassigned === 0 || isAssigning} | 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={{ | sx={{ | ||||
| flex: 1, | flex: 1, | ||||
| fontSize: '1.1rem', | fontSize: '1.1rem', | ||||
| @@ -396,7 +443,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw | |||||
| variant="outlined" | variant="outlined" | ||||
| size="medium" | size="medium" | ||||
| disabled={item.lane.unassigned === 0 || isAssigning} | 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={{ | sx={{ | ||||
| flex: 1, | flex: 1, | ||||
| fontSize: '1.1rem', | fontSize: '1.1rem', | ||||
| @@ -655,7 +655,7 @@ const handleAssignByLane = useCallback(async ( | |||||
| > | > | ||||
| {t("Print All Draft")} ({releasedOrderCount}) | {t("Print All Draft")} ({releasedOrderCount}) | ||||
| </Button> | </Button> | ||||
| {/* | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| sx={{ | sx={{ | ||||
| @@ -676,6 +676,7 @@ const handleAssignByLane = useCallback(async ( | |||||
| > | > | ||||
| {t("Print Draft")} | {t("Print Draft")} | ||||
| </Button> | </Button> | ||||
| */} | |||||
| </Stack> | </Stack> | ||||
| </Box> | </Box> | ||||
| @@ -1,4 +1,3 @@ | |||||
| // FPSMS-frontend/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx | |||||
| "use client"; | "use client"; | ||||
| import { | import { | ||||
| @@ -16,16 +15,18 @@ import { | |||||
| TextField, | TextField, | ||||
| Typography, | Typography, | ||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useCallback, useEffect, useState } from "react"; | |||||
| import { useCallback, useEffect, useState, useRef } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | 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 { 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"; | import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| interface LotPickData { | interface LotPickData { | ||||
| id: number; | |||||
| id: number; | |||||
| lotId: number; | lotId: number; | ||||
| lotNo: string; | lotNo: string; | ||||
| expiryDate: string; | expiryDate: string; | ||||
| @@ -39,7 +40,12 @@ interface LotPickData { | |||||
| requiredQty: number; | requiredQty: number; | ||||
| actualPickQty: number; | actualPickQty: number; | ||||
| lotStatus: string; | lotStatus: string; | ||||
| lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected'; | |||||
| lotAvailability: | |||||
| | "available" | |||||
| | "insufficient_stock" | |||||
| | "expired" | |||||
| | "status_unavailable" | |||||
| | "rejected"; | |||||
| stockOutLineId?: number; | stockOutLineId?: number; | ||||
| stockOutLineStatus?: string; | stockOutLineStatus?: string; | ||||
| stockOutLineQty?: number; | stockOutLineQty?: number; | ||||
| @@ -77,12 +83,14 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({}); | const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({}); | ||||
| const [errors, setErrors] = useState<FormErrors>({}); | const [errors, setErrors] = useState<FormErrors>({}); | ||||
| const [loading, setLoading] = useState(false); | 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) => { | const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { | ||||
| return lot.availableQty || 0; | return lot.availableQty || 0; | ||||
| }, []); | }, []); | ||||
| const calculateRequiredQty = useCallback((lot: LotPickData) => { | const calculateRequiredQty = useCallback((lot: LotPickData) => { | ||||
| return lot.requiredQty || 0; | return lot.requiredQty || 0; | ||||
| }, []); | }, []); | ||||
| @@ -96,7 +104,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| console.error("Error fetching handlers:", error); | console.error("Error fetching handlers:", error); | ||||
| } | } | ||||
| }; | }; | ||||
| fetchHandlers(); | fetchHandlers(); | ||||
| }, []); | }, []); | ||||
| @@ -136,92 +144,119 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| requiredQty: selectedLot.requiredQty, | requiredQty: selectedLot.requiredQty, | ||||
| actualPickQty: selectedLot.actualPickQty || 0, | actualPickQty: selectedLot.actualPickQty || 0, | ||||
| missQty: 0, | missQty: 0, | ||||
| badItemQty: 0, | |||||
| issueRemark: '', | |||||
| pickerName: '', | |||||
| badItemQty: 0, // Bad Item Qty | |||||
| badPackageQty: 0, // Bad Package Qty (frontend only) | |||||
| issueRemark: "", | |||||
| pickerName: "", | |||||
| handledBy: undefined, | handledBy: undefined, | ||||
| reason: '', | |||||
| badReason: '', | |||||
| reason: "", | |||||
| badReason: "", | |||||
| }); | }); | ||||
| initKeyRef.current = key; | 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 | // Updated validation logic | ||||
| const validateForm = (): boolean => { | const validateForm = (): boolean => { | ||||
| const newErrors: FormErrors = {}; | const newErrors: FormErrors = {}; | ||||
| const req = selectedLot?.requiredQty || 0; | |||||
| const ap = Number(formData.actualPickQty) || 0; | const ap = Number(formData.actualPickQty) || 0; | ||||
| const miss = Number(formData.missQty) || 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; | const availableQty = selectedLot?.availableQty || 0; | ||||
| // 1. Check actualPickQty cannot be negative | // 1. Check actualPickQty cannot be negative | ||||
| if (ap < 0) { | if (ap < 0) { | ||||
| newErrors.actualPickQty = t('Qty cannot be negative'); | |||||
| newErrors.actualPickQty = t("Qty cannot be negative"); | |||||
| } | } | ||||
| // 2. Check actualPickQty cannot exceed available quantity | // 2. Check actualPickQty cannot exceed available quantity | ||||
| if (ap > availableQty) { | 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) { | 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) { | 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.actualPickQty = errorMsg; | ||||
| newErrors.missQty = errorMsg; | newErrors.missQty = errorMsg; | ||||
| newErrors.badItemQty = 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); | setErrors(newErrors); | ||||
| return Object.keys(newErrors).length === 0; | return Object.keys(newErrors).length === 0; | ||||
| }; | }; | ||||
| const handleSubmit = async () => { | const handleSubmit = async () => { | ||||
| if (!validateForm()) { | if (!validateForm()) { | ||||
| console.error('Form validation failed:', errors); | |||||
| console.error("Form validation failed:", errors); | |||||
| return; | return; | ||||
| } | } | ||||
| if (!formData.pickOrderId) { | if (!formData.pickOrderId) { | ||||
| console.error('Missing pickOrderId'); | |||||
| console.error("Missing pickOrderId"); | |||||
| return; | 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); | setLoading(true); | ||||
| try { | try { | ||||
| await onSubmit(formData as PickExecutionIssueData); | |||||
| await onSubmit(submitData); | |||||
| } catch (error: any) { | } 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 { | } finally { | ||||
| setLoading(false); | setLoading(false); | ||||
| } | } | ||||
| @@ -239,11 +274,15 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); | const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); | ||||
| const requiredQty = calculateRequiredQty(selectedLot); | const requiredQty = calculateRequiredQty(selectedLot); | ||||
| return ( | return ( | ||||
| <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | <Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth> | ||||
| <DialogTitle> | <DialogTitle> | ||||
| {t('Pick Execution Issue Form')} | |||||
| {t("Pick Execution Issue Form") } | |||||
| <br /> | |||||
| {selectedPickOrderLine.itemCode+ " "+ selectedPickOrderLine.itemName} | |||||
| <br /> | |||||
| {selectedLot.lotNo} | |||||
| </DialogTitle> | </DialogTitle> | ||||
| <DialogContent> | <DialogContent> | ||||
| <Box sx={{ mt: 2 }}> | <Box sx={{ mt: 2 }}> | ||||
| @@ -251,17 +290,17 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label={t('Required Qty')} | |||||
| value={selectedLot?.requiredQty || 0} | |||||
| label={t("Required Qty")} | |||||
| value={requiredQty} | |||||
| disabled | disabled | ||||
| variant="outlined" | variant="outlined" | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label={t('Remaining Available Qty')} | |||||
| label={t("Remaining Available Qty")} | |||||
| value={remainingAvailableQty} | value={remainingAvailableQty} | ||||
| disabled | disabled | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -269,43 +308,53 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <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> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label={t('Missing item Qty')} | |||||
| label={t("Missing item Qty")} | |||||
| type="number" | type="number" | ||||
| inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }} | |||||
| inputProps={{ | |||||
| inputMode: "numeric", | |||||
| pattern: "[0-9]*", | |||||
| min: 0, | |||||
| }} | |||||
| value={formData.missQty || 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} | error={!!errors.missQty} | ||||
| variant="outlined" | variant="outlined" | ||||
| /> | /> | ||||
| @@ -314,53 +363,76 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <TextField | <TextField | ||||
| fullWidth | fullWidth | ||||
| label={t('Bad Item Qty')} | |||||
| label={t("Bad Item Qty")} | |||||
| type="number" | type="number" | ||||
| inputProps={{ inputMode: 'numeric', pattern: '[0-9]*', min: 0 }} | |||||
| inputProps={{ | |||||
| inputMode: "numeric", | |||||
| pattern: "[0-9]*", | |||||
| min: 0, | |||||
| }} | |||||
| value={formData.badItemQty || 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} | error={!!errors.badItemQty} | ||||
| //helperText={t("Quantity Problem")} | |||||
| variant="outlined" | variant="outlined" | ||||
| /> | /> | ||||
| </Grid> | </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> | ||||
| <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> | </Box> | ||||
| </DialogContent> | </DialogContent> | ||||
| <DialogActions> | <DialogActions> | ||||
| <Button onClick={handleClose} disabled={loading}> | <Button onClick={handleClose} disabled={loading}> | ||||
| {t('Cancel')} | |||||
| {t("Cancel")} | |||||
| </Button> | </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> | </Button> | ||||
| </DialogActions> | </DialogActions> | ||||
| </Dialog> | </Dialog> | ||||
| @@ -19,6 +19,7 @@ import { | |||||
| TablePagination, | TablePagination, | ||||
| Modal, | Modal, | ||||
| Chip, | Chip, | ||||
| LinearProgress, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; | import TestQrCodeProvider from '../QrCodeScannerProvider/TestQrCodeProvider'; | ||||
| @@ -78,7 +79,33 @@ interface Props { | |||||
| onSwitchToRecordTab?: () => void; | onSwitchToRecordTab?: () => void; | ||||
| onRefreshReleasedOrderCount?: () => 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) | // QR Code Modal Component (from LotTable) | ||||
| const QrCodeModal: React.FC<{ | const QrCodeModal: React.FC<{ | ||||
| open: boolean; | open: boolean; | ||||
| @@ -86,7 +113,8 @@ const QrCodeModal: React.FC<{ | |||||
| lot: any | null; | lot: any | null; | ||||
| onQrCodeSubmit: (lotNo: string) => void; | onQrCodeSubmit: (lotNo: string) => void; | ||||
| combinedLotData: any[]; // Add this prop | combinedLotData: any[]; // Add this prop | ||||
| }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { | |||||
| lotConfirmationOpen: boolean; | |||||
| }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData,lotConfirmationOpen = false }) => { | |||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); | ||||
| const [manualInput, setManualInput] = useState<string>(''); | const [manualInput, setManualInput] = useState<string>(''); | ||||
| @@ -100,8 +128,20 @@ const QrCodeModal: React.FC<{ | |||||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | ||||
| const [scannedQrResult, setScannedQrResult] = useState<string>(''); | const [scannedQrResult, setScannedQrResult] = useState<string>(''); | ||||
| const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null); | const [fgPickOrder, setFgPickOrder] = useState<FGPickOrderResponse | null>(null); | ||||
| const fetchingRef = useRef<Set<number>>(new Set()); | |||||
| // Process scanned QR codes | // Process scanned QR codes | ||||
| useEffect(() => { | 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) { | if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) { | ||||
| const latestQr = qrValues[qrValues.length - 1]; | const latestQr = qrValues[qrValues.length - 1]; | ||||
| @@ -110,17 +150,39 @@ const QrCodeModal: React.FC<{ | |||||
| return; | return; | ||||
| } | } | ||||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||||
| try { | try { | ||||
| const qrData = JSON.parse(latestQr); | const qrData = JSON.parse(latestQr); | ||||
| if (qrData.stockInLineId && qrData.itemId) { | 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); | setIsProcessingQr(true); | ||||
| setQrScanFailed(false); | 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) | fetchStockInLineInfo(qrData.stockInLineId) | ||||
| .then((stockInLineInfo) => { | .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); | console.log("Stock in line info:", stockInLineInfo); | ||||
| setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); | setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number'); | ||||
| @@ -138,7 +200,17 @@ const QrCodeModal: React.FC<{ | |||||
| } | } | ||||
| }) | }) | ||||
| .catch((error) => { | .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'); | setScannedQrResult('Error fetching data'); | ||||
| setQrScanFailed(true); | setQrScanFailed(true); | ||||
| setManualInputError(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 | // Clear states when modal opens | ||||
| useEffect(() => { | useEffect(() => { | ||||
| @@ -477,7 +549,7 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false); | |||||
| const [paginationController, setPaginationController] = useState({ | const [paginationController, setPaginationController] = useState({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| pageSize: 10, | |||||
| pageSize: -1, | |||||
| }); | }); | ||||
| const [usernameList, setUsernameList] = useState<NameList[]>([]); | const [usernameList, setUsernameList] = useState<NameList[]>([]); | ||||
| @@ -515,12 +587,79 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||||
| console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); | console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); | ||||
| // TODO: Implement QR code functionality | // 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 handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => { | ||||
| const mismatchStartTime = performance.now(); | |||||
| console.log(`⏱️ [HANDLE LOT MISMATCH START]`); | |||||
| console.log("Lot mismatch detected:", { expectedLot, scannedLot }); | 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[]) => { | const checkAllLotsCompleted = useCallback((lotData: any[]) => { | ||||
| if (lotData.length === 0) { | if (lotData.length === 0) { | ||||
| @@ -937,18 +1076,15 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| setIsConfirmingLot(true); | setIsConfirmingLot(true); | ||||
| try { | try { | ||||
| const newLotNo = scannedLotData?.lotNo; | 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({ | await confirmLotSubstitution({ | ||||
| pickOrderLineId: selectedLotForQr.pickOrderLineId, | pickOrderLineId: selectedLotForQr.pickOrderLineId, | ||||
| stockOutLineId: selectedLotForQr.stockOutLineId, | stockOutLineId: selectedLotForQr.stockOutLineId, | ||||
| originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, | originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId, | ||||
| newInventoryLotNo: newLotNo | |||||
| newInventoryLotNo: "", | |||||
| newStockInLineId: newStockInLineId | |||||
| }); | }); | ||||
| setQrScanError(false); | setQrScanError(false); | ||||
| @@ -1261,12 +1397,19 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId }; // ✅ 添加 byStockInLineId | return { byItemId, byItemCode, byLotId, byLotNo, byStockInLineId }; // ✅ 添加 byStockInLineId | ||||
| }, [combinedLotData]); | }, [combinedLotData]); | ||||
| const processOutsideQrCode = useCallback(async (latestQr: string) => { | 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 | // 1) Parse JSON safely | ||||
| const parseStartTime = performance.now(); | |||||
| let qrData: any = null; | let qrData: any = null; | ||||
| let parseTime = 0; // ✅ Declare parseTime in outer scope | |||||
| try { | try { | ||||
| qrData = JSON.parse(latestQr); | qrData = JSON.parse(latestQr); | ||||
| parseTime = performance.now() - parseStartTime; // ✅ Assign value | |||||
| console.log(`⏱️ JSON parse time: ${parseTime.toFixed(2)}ms`); | |||||
| } catch { | } catch { | ||||
| console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches."); | console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches."); | ||||
| setQrScanError(true); | setQrScanError(true); | ||||
| @@ -1287,12 +1430,15 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| const scannedStockInLineId = qrData.stockInLineId; | const scannedStockInLineId = qrData.stockInLineId; | ||||
| // ✅ OPTIMIZATION: 使用索引快速查找相同 item 的 lots | // ✅ OPTIMIZATION: 使用索引快速查找相同 item 的 lots | ||||
| const lookupStartTime = performance.now(); | |||||
| const sameItemLots: any[] = []; | const sameItemLots: any[] = []; | ||||
| // 使用索引快速查找 | // 使用索引快速查找 | ||||
| if (lotDataIndexes.byItemId.has(scannedItemId)) { | if (lotDataIndexes.byItemId.has(scannedItemId)) { | ||||
| sameItemLots.push(...lotDataIndexes.byItemId.get(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) { | if (sameItemLots.length === 0) { | ||||
| console.error("No item match in expected lots for scanned code"); | 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) | // ✅ OPTIMIZATION: 过滤出活跃的 lots(非 rejected) | ||||
| const filterStartTime = performance.now(); | |||||
| const rejectedStatuses = new Set(['rejected']); | const rejectedStatuses = new Set(['rejected']); | ||||
| const activeSuggestedLots = sameItemLots.filter(lot => | const activeSuggestedLots = sameItemLots.filter(lot => | ||||
| !rejectedStatuses.has(lot.lotAvailability) && | !rejectedStatuses.has(lot.lotAvailability) && | ||||
| !rejectedStatuses.has(lot.stockOutLineStatus) && | !rejectedStatuses.has(lot.stockOutLineStatus) && | ||||
| !rejectedStatuses.has(lot.processingStatus) | !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) { | if (activeSuggestedLots.length === 0) { | ||||
| console.error("No active suggested lots found for this item"); | console.error("No active suggested lots found for this item"); | ||||
| @@ -1317,10 +1466,13 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| } | } | ||||
| // ✅ OPTIMIZATION: 按优先级查找匹配的 lot | // ✅ OPTIMIZATION: 按优先级查找匹配的 lot | ||||
| const matchStartTime = performance.now(); | |||||
| // 1. 首先查找 stockInLineId 完全匹配的(正确的 lot) | // 1. 首先查找 stockInLineId 完全匹配的(正确的 lot) | ||||
| let exactMatch = activeSuggestedLots.find(lot => | let exactMatch = activeSuggestedLots.find(lot => | ||||
| lot.stockInLineId === scannedStockInLineId | lot.stockInLineId === scannedStockInLineId | ||||
| ); | ); | ||||
| const matchTime = performance.now() - matchStartTime; | |||||
| console.log(`⏱️ Find exact match time: ${matchTime.toFixed(2)}ms, found: ${exactMatch ? 'yes' : 'no'}`); | |||||
| if (exactMatch) { | if (exactMatch) { | ||||
| // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认 | // ✅ Case 1: stockInLineId 匹配 - 直接处理,不需要确认 | ||||
| @@ -1334,6 +1486,8 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| } | } | ||||
| try { | try { | ||||
| const apiStartTime = performance.now(); | |||||
| console.log(`⏱️ [API CALL START] Calling updateStockOutLineStatusByQRCodeAndLotNo`); | |||||
| const res = await updateStockOutLineStatusByQRCodeAndLotNo({ | const res = await updateStockOutLineStatusByQRCodeAndLotNo({ | ||||
| pickOrderLineId: exactMatch.pickOrderLineId, | pickOrderLineId: exactMatch.pickOrderLineId, | ||||
| inventoryLotNo: exactMatch.lotNo, | inventoryLotNo: exactMatch.lotNo, | ||||
| @@ -1341,8 +1495,11 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| itemId: exactMatch.itemId, | itemId: exactMatch.itemId, | ||||
| status: "checked", | 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") { | if (res.code === "checked" || res.code === "SUCCESS") { | ||||
| const stateUpdateStartTime = performance.now(); | |||||
| setQrScanError(false); | setQrScanError(false); | ||||
| setQrScanSuccess(true); | setQrScanSuccess(true); | ||||
| @@ -1371,7 +1528,13 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| } | } | ||||
| return lot; | 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"); | console.log("✅ Status updated locally, no full data refresh needed"); | ||||
| } else { | } else { | ||||
| console.warn("Unexpected response code from backend:", res.code); | console.warn("Unexpected response code from backend:", res.code); | ||||
| @@ -1379,6 +1542,8 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| } | } | ||||
| } catch (e) { | } 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); | console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e); | ||||
| setQrScanError(true); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| @@ -1417,6 +1582,8 @@ console.log(" DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos); | |||||
| } | } | ||||
| ); | ); | ||||
| } catch (error) { | } 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); | console.error("Error during QR code processing:", error); | ||||
| setQrScanError(true); | setQrScanError(true); | ||||
| setQrScanSuccess(false); | setQrScanSuccess(false); | ||||
| @@ -1811,13 +1978,16 @@ useEffect(() => { | |||||
| const newPageSize = parseInt(event.target.value, 10); | const newPageSize = parseInt(event.target.value, 10); | ||||
| setPaginationController({ | setPaginationController({ | ||||
| pageNum: 0, | pageNum: 0, | ||||
| pageSize: newPageSize, | |||||
| pageSize: newPageSize === -1 ? -1 : newPageSize, | |||||
| }); | }); | ||||
| }, []); | }, []); | ||||
| // Pagination data with sorting by routerIndex | // Pagination data with sorting by routerIndex | ||||
| // Remove the sorting logic and just do pagination | // Remove the sorting logic and just do pagination | ||||
| const paginatedData = useMemo(() => { | const paginatedData = useMemo(() => { | ||||
| if (paginationController.pageSize === -1) { | |||||
| return combinedLotData; // Show all items | |||||
| } | |||||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | const startIndex = paginationController.pageNum * paginationController.pageSize; | ||||
| const endIndex = startIndex + paginationController.pageSize; | const endIndex = startIndex + paginationController.pageSize; | ||||
| return combinedLotData.slice(startIndex, endIndex); // No sorting needed | return combinedLotData.slice(startIndex, endIndex); // No sorting needed | ||||
| @@ -2340,6 +2510,24 @@ const handleSubmitAllScanned = useCallback(async () => { | |||||
| > | > | ||||
| <FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
| <Stack spacing={2}> | <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 */} | {/* DO Header */} | ||||
| @@ -2543,7 +2731,7 @@ paginatedData.map((lot, index) => { | |||||
| }} | }} | ||||
| > | > | ||||
| {lot.lotNo || | {lot.lotNo || | ||||
| t('⚠️ No Stock Available')} | |||||
| t('No Stock Available')} | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -2698,7 +2886,7 @@ paginatedData.map((lot, index) => { | |||||
| }} | }} | ||||
| title="Report missing or bad items" | title="Report missing or bad items" | ||||
| > | > | ||||
| {t("Issue")} | |||||
| {t("Edit")} | |||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="outlined" | variant="outlined" | ||||
| @@ -2707,7 +2895,7 @@ paginatedData.map((lot, index) => { | |||||
| disabled={lot.stockOutLineStatus === 'completed'} | disabled={lot.stockOutLineStatus === 'completed'} | ||||
| sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} | sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }} | ||||
| > | > | ||||
| {t("Skip")} | |||||
| {t("Just Completed")} | |||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| ); | ); | ||||
| @@ -2729,7 +2917,7 @@ paginatedData.map((lot, index) => { | |||||
| rowsPerPage={paginationController.pageSize} | rowsPerPage={paginationController.pageSize} | ||||
| onPageChange={handlePageChange} | onPageChange={handlePageChange} | ||||
| onRowsPerPageChange={handlePageSizeChange} | onRowsPerPageChange={handlePageSizeChange} | ||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| rowsPerPageOptions={[10, 25, 50,-1]} | |||||
| labelRowsPerPage={t("Rows per page")} | labelRowsPerPage={t("Rows per page")} | ||||
| labelDisplayedRows={({ from, to, count }) => | labelDisplayedRows={({ from, to, count }) => | ||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | ||||
| @@ -2750,6 +2938,7 @@ paginatedData.map((lot, index) => { | |||||
| lot={selectedLotForQr} | lot={selectedLotForQr} | ||||
| combinedLotData={combinedLotData} | combinedLotData={combinedLotData} | ||||
| onQrCodeSubmit={handleQrCodeSubmitFromModal} | onQrCodeSubmit={handleQrCodeSubmitFromModal} | ||||
| lotConfirmationOpen={lotConfirmationOpen} // ✅ Add this prop | |||||
| /> | /> | ||||
| <ManualLotConfirmationModal | <ManualLotConfirmationModal | ||||
| open={manualLotConfirmationOpen} | open={manualLotConfirmationOpen} | ||||
| @@ -640,7 +640,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { | |||||
| variant="contained" | variant="contained" | ||||
| color="primary" | color="primary" | ||||
| onClick={() => handleRelease(jobOrderId)} | onClick={() => handleRelease(jobOrderId)} | ||||
| disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"} | |||||
| //disabled={stockCounts.insufficient > 0 || processData?.jobOrderStatus !== "planning"} | |||||
| > | > | ||||
| {t("Release")} | {t("Release")} | ||||
| </Button> | </Button> | ||||
| @@ -79,6 +79,25 @@ | |||||
| "Tomorrow": "Tomorrow", | "Tomorrow": "Tomorrow", | ||||
| "Day After Tomorrow": "Day After Tomorrow", | "Day After Tomorrow": "Day After Tomorrow", | ||||
| "Goods Receipt Status": "Goods Receipt Status", | "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", | "Filter": "Filter", | ||||
| "All": "All", | "All": "All", | ||||
| "Column 1": "Column 1", | "Column 1": "Column 1", | ||||
| @@ -79,6 +79,25 @@ | |||||
| "Tomorrow": "翌日", | "Tomorrow": "翌日", | ||||
| "Day After Tomorrow": "後日", | "Day After Tomorrow": "後日", | ||||
| "Goods Receipt Status": "貨物接收狀態", | "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": "篩選", | "Filter": "篩選", | ||||
| "All": "全部", | "All": "全部", | ||||
| "Column 1": "欄位1", | "Column 1": "欄位1", | ||||
| @@ -11,14 +11,24 @@ | |||||
| "Status": "來貨狀態", | "Status": "來貨狀態", | ||||
| "Order Date From": "訂單日期", | "Order Date From": "訂單日期", | ||||
| "Delivery Order Code": "送貨訂單編號", | "Delivery Order Code": "送貨訂單編號", | ||||
| "Select Remark": "選擇備註", | |||||
| "Confirm Assignment": "確認分配", | |||||
| "Required Date": "所需日期", | |||||
| "Store": "位置", | |||||
| "Lane Code": "車線號碼", | |||||
| "Available Orders": "可用訂單", | |||||
| "Just Complete": "已完成", | |||||
| "Order Date To": "訂單日期至", | "Order Date To": "訂單日期至", | ||||
| "Warning: Some delivery orders do not have matching trucks for the target date.": "警告:部分送貨訂單於目標日期沒有可匹配的車輛。", | "Warning: Some delivery orders do not have matching trucks for the target date.": "警告:部分送貨訂單於目標日期沒有可匹配的車輛。", | ||||
| "Truck Availability Warning": "車輛可用性警告", | "Truck Availability Warning": "車輛可用性警告", | ||||
| "Problem DO(s): ": "問題送貨訂單", | "Problem DO(s): ": "問題送貨訂單", | ||||
| "Fetching all matching records...": "正在獲取所有匹配的記錄...", | "Fetching all matching records...": "正在獲取所有匹配的記錄...", | ||||
| "Progress": "進度", | |||||
| "Loading...": "正在加載...", | "Loading...": "正在加載...", | ||||
| "Available Trucks": "可用車輛", | "Available Trucks": "可用車輛", | ||||
| "No trucks available": "沒有車輛可用", | "No trucks available": "沒有車輛可用", | ||||
| "Remark": "備註", | |||||
| "Just Completed": "已完成", | |||||
| "Code": "門店訂單編號", | "Code": "門店訂單編號", | ||||
| "code": "門店訂單編號", | "code": "門店訂單編號", | ||||
| "Create": "新增", | "Create": "新增", | ||||
| @@ -9,12 +9,22 @@ | |||||
| "Status": "來貨狀態", | "Status": "來貨狀態", | ||||
| "N/A": "不適用", | "N/A": "不適用", | ||||
| "Release Pick Orders": "放單", | "Release Pick Orders": "放單", | ||||
| "Remark": "備註", | |||||
| "Escalated": "上報狀態", | "Escalated": "上報狀態", | ||||
| "NotEscalated": "無上報", | "NotEscalated": "無上報", | ||||
| "Assigned To": "已分配", | "Assigned To": "已分配", | ||||
| "Progress": "進度", | |||||
| "Select Remark": "選擇備註", | |||||
| "Just Complete": "已完成", | |||||
| "Skip": "跳過", | "Skip": "跳過", | ||||
| "Confirm Assignment": "確認分配", | |||||
| "Required Date": "所需日期", | |||||
| "Store": "位置", | |||||
| "Available Orders": "可用訂單", | |||||
| "Lane Code": "車線號碼", | |||||
| "Fetching all matching records...": "正在獲取所有匹配的記錄...", | "Fetching all matching records...": "正在獲取所有匹配的記錄...", | ||||
| "Edit": "改數", | |||||
| "Just Completed": "已完成", | |||||
| "Do you want to start?": "確定開始嗎?", | "Do you want to start?": "確定開始嗎?", | ||||
| "Start": "開始", | "Start": "開始", | ||||
| "Pick Order Code(s)": "提料單編號", | "Pick Order Code(s)": "提料單編號", | ||||
| @@ -257,9 +267,11 @@ | |||||
| "Pick Execution Issue Form":"提料問題表單", | "Pick Execution Issue Form":"提料問題表單", | ||||
| "This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。", | "This form is for reporting issues only. You must report either missing items or bad items.":"此表單僅用於報告問題。您必須報告缺少的貨品或不良貨品。", | ||||
| "Bad item Qty":"不良貨品數量", | "Bad item Qty":"不良貨品數量", | ||||
| "Missing item Qty":"缺少貨品數量", | |||||
| "Missing item Qty":"貨品遺失數量", | |||||
| "Missing Item Qty":"貨品遺失數量", | |||||
| "Bad Item Qty":"不良貨品數量", | "Bad Item Qty":"不良貨品數量", | ||||
| "Missing Item Qty":"缺少貨品數量", | |||||
| "Bad Package Qty":"不良包裝數量", | |||||
| "Actual Pick Qty":"實際提料數量", | "Actual Pick Qty":"實際提料數量", | ||||
| "Required Qty":"所需數量", | "Required Qty":"所需數量", | ||||
| "Issue Remark":"問題描述", | "Issue Remark":"問題描述", | ||||