瀏覽代碼

update

MergeProblem1
CANCERYS\kw093 1 周之前
父節點
當前提交
0aedd3b83d
共有 15 個檔案被更改,包括 846 行新增313 行删除
  1. +12
    -0
      src/app/(main)/settings/qcItemAll/page.tsx
  2. +18
    -0
      src/app/api/dashboard/actions.ts
  3. +17
    -0
      src/app/api/dashboard/client.ts
  4. +2
    -0
      src/app/api/pickOrder/actions.ts
  5. +81
    -28
      src/components/DashboardPage/DashboardPage.tsx
  6. +134
    -72
      src/components/DashboardPage/goodsReceiptStatus/GoodsReceiptStatus.tsx
  7. +108
    -61
      src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx
  8. +2
    -1
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  9. +196
    -124
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  10. +212
    -23
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  11. +1
    -1
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  12. +19
    -0
      src/i18n/en/dashboard.json
  13. +19
    -0
      src/i18n/zh/dashboard.json
  14. +10
    -0
      src/i18n/zh/do.json
  15. +15
    -3
      src/i18n/zh/pickOrder.json

+ 12
- 0
src/app/(main)/settings/qcItemAll/page.tsx 查看文件

@@ -45,3 +45,15 @@ const qcItemAll: React.FC = async () => {

export default qcItemAll;














+ 18
- 0
src/app/api/dashboard/actions.ts 查看文件

@@ -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" });
});

+ 17
- 0
src/app/api/dashboard/client.ts 查看文件

@@ -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;


+ 2
- 0
src/app/api/pickOrder/actions.ts 查看文件

@@ -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>(


+ 81
- 28
src/components/DashboardPage/DashboardPage.tsx 查看文件

@@ -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}>


+ 134
- 72
src/components/DashboardPage/goodsReceiptStatus/GoodsReceiptStatus.tsx 查看文件

@@ -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>
))


+ 108
- 61
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx 查看文件

@@ -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',


+ 2
- 1
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx 查看文件

@@ -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>



+ 196
- 124
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx 查看文件

@@ -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>


+ 212
- 23
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx 查看文件

@@ -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}


+ 1
- 1
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx 查看文件

@@ -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>


+ 19
- 0
src/i18n/en/dashboard.json 查看文件

@@ -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",


+ 19
- 0
src/i18n/zh/dashboard.json 查看文件

@@ -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",


+ 10
- 0
src/i18n/zh/do.json 查看文件

@@ -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": "新增",


+ 15
- 3
src/i18n/zh/pickOrder.json 查看文件

@@ -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":"問題描述",


Loading…
取消
儲存