Quellcode durchsuchen

update jo matching

production
CANCERYS\kw093 vor 3 Tagen
Ursprung
Commit
bded62fa3a
1 geänderte Dateien mit 204 neuen und 159 gelöschten Zeilen
  1. +204
    -159
      src/components/Jodetail/JobPickExecutionsecondscan.tsx

+ 204
- 159
src/components/Jodetail/JobPickExecutionsecondscan.tsx Datei anzeigen

@@ -354,8 +354,34 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
const [currentPickOrderId, setCurrentPickOrderId] = useState<number | null>(null);

const getItemKey = useCallback((lot: any) => {
return `${lot.pickOrderId}-${lot.pickOrderLineId}-${lot.itemId}`;
}, []);
// 添加:unassign 函数
const itemMatchSummaryMap = useMemo(() => {
return combinedLotData.reduce<
Record<string, { hasCompleted: boolean; completedMatchQty: number }>
>((acc, lot) => {
const key = getItemKey(lot);
const matchStatus = String(lot.matchStatus || "").toLowerCase();
const isCompleted = matchStatus === "completed";
// 來源優先:matchQty -> match_qty -> 0
const qty = Number(lot.matchQty ?? lot.match_qty ?? 0);
if (!acc[key]) {
acc[key] = { hasCompleted: false, completedMatchQty: 0 };
}
if (isCompleted) {
acc[key].hasCompleted = true;
// 同 item 若有 completed,取最大的 matchQty(避免被 0 覆蓋)
acc[key].completedMatchQty = Math.max(acc[key].completedMatchQty, qty);
}
return acc;
}, {});
}, [combinedLotData, getItemKey]);
const handleUnassign = useCallback(async (pickOrderId: number | null) => {
if (!pickOrderId || !currentUserId) {
console.log("No pickOrderId or userId to unassign");
@@ -482,7 +508,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
stockOutLineId: lot.stockOutLineId,
stockOutLineStatus: lot.stockOutLineStatus,
stockOutLineQty: lot.stockOutLineQty,
matchQty: lot.matchQty ?? lot.match_qty ?? null,
// Router info
routerIndex: lot.routerIndex,
matchStatus: lot.matchStatus,
@@ -1015,7 +1041,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
pageSize: newPageSize,
});
}, []);

const itemActualPickQtyMap = useMemo(() => {
return combinedLotData.reduce<Record<string, number>>((acc, lot) => {
const key = getItemKey(lot); // 一定要跟下方讀取同一套
const qty = Number(lot.actualPickQty ?? 0);
acc[key] = (acc[key] ?? 0) + (Number.isFinite(qty) ? qty : 0);
return acc;
}, {});
}, [combinedLotData, getItemKey]);
const paginatedData = useMemo(() => {
const sortedData = [...combinedLotData].sort((a, b) => {
const aIndex = a.routerIndex || 0;
@@ -1081,7 +1114,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
return t("Please finish QR code scan and pick order.");
}
}, [t]);
const [submitQtyFieldEnabledByLotKey, setSubmitQtyFieldEnabledByLotKey] =useState<Record<string, boolean>>({});
return (
<TestQrCodeProvider
lotData={combinedLotData}
@@ -1142,61 +1175,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
t("Confirm All")
)}
</Button>
{/*
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
{!isManualScanning ? (
<Button
variant="contained"
startIcon={<QrCodeIcon />}
onClick={handleStartScan}
color="primary"
sx={{ minWidth: '120px' }}
>
{t("Start QR Scan")}
</Button>
) : (
<Button
variant="outlined"
startIcon={<QrCodeIcon />}
onClick={handleStopScan}
color="secondary"
sx={{ minWidth: '120px' }}
>
{t("Stop QR Scan")}
</Button>
)}
<Button
variant="contained"
color="success"
onClick={handleSubmitAllScanned}
disabled={scannedItemsCount === 0 || isSubmittingAll}
sx={{ minWidth: '160px' }}
>
{isSubmittingAll ? (
<>
<CircularProgress size={16} sx={{ mr: 1 }} />
{t("Submitting...")}
</>
) : (
`${t("Submit All Scanned")} (${scannedItemsCount})`
)}
</Button>
</Box>
</Box>

{qrScanError && !qrScanSuccess && (
<Alert severity="error" sx={{ mb: 2 }}>
{t("QR code does not match any item in current orders.")}
</Alert>
)}
{qrScanSuccess && (
<Alert severity="success" sx={{ mb: 2 }}>
{t("QR code verified.")}
</Alert>
)}
*/}
<TableContainer component={Paper}>
<Table>
<TableHead>
@@ -1207,8 +1186,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
<TableCell>{t("Item Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
{/* <TableCell align="center">{t("Scan Result")}</TableCell> */}
<TableCell align="right">{t("Actual Pick Qty")}</TableCell>
<TableCell align="center">{t("Scan Result")}</TableCell>
<TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
</TableRow>
</TableHead>
@@ -1222,108 +1201,174 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBack }) => {
</TableCell>
</TableRow>
) : (
paginatedData.map((lot, index) => (
<TableRow
key={`${lot.pickOrderLineId}-${lot.lotId}`}
sx={{
backgroundColor: lot.lotAvailability === 'rejected' ? 'grey.100' : 'inherit',
opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1,
'& .MuiTableCell-root': {
color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit'
}
}}
>
<TableCell>
<Typography variant="body2" fontWeight="bold">
{index + 1}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">
{lot.routerRoute || '-'}
</Typography>
</TableCell>
<TableCell>{lot.handler || '-'}</TableCell>
<TableCell>{lot.itemCode}</TableCell>
<TableCell>{lot.itemName+'('+lot.uomDesc+')'}</TableCell>
<TableCell>
<Box>
<Typography
sx={{
color: lot.lotAvailability === 'rejected' ? 'text.disabled' : 'inherit',
opacity: lot.lotAvailability === 'rejected' ? 0.6 : 1
}}
>
{lot.lotNo}
</Typography>
</Box>
</TableCell>
<TableCell align="right">
{(() => {
const requiredQty = lot.requiredQty || 0;
return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
})()}
</TableCell>
paginatedData.map((lot, index) => {
const itemKey = getItemKey(lot);
const itemSummary = itemMatchSummaryMap[itemKey] ?? {
hasCompleted: false,
completedMatchQty: 0,
};
const itemCompleted = itemSummary.hasCompleted;
const itemCompletedQty = itemSummary.completedMatchQty;
const isFirstRowOfItem =
index === 0 || getItemKey(paginatedData[index - 1]) !== itemKey;
const itemTotalActualPickQty = Number(itemActualPickQtyMap[itemKey] ?? 0);

<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Stack direction="row" spacing={1} alignItems="center">
<Button
variant="contained"
onClick={async () => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
handlePickQtyChange(lotKey, submitQty);
// 先更新 matching 狀態(可選,依你後端流程)
await updateSecondQrScanStatus(lot.pickOrderId, lot.itemId, currentUserId || 0, submitQty);
// 再提交數量並 await refetch,表格會即時更新提料員
await handleSubmitPickQtyWithQty(lot, submitQty);
}}
disabled={
lot.matchStatus === 'completed' ||
lot.matchStatus == 'scanned' ||
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected'
}
sx={{
fontSize: '0.75rem',
py: 0.5,
minHeight: '28px',
minWidth: '70px'
}}
>
{t("Confirm")}
</Button>
<Button
variant="outlined"
size="small"
onClick={() => handlePickExecutionForm(lot)}
disabled={
lot.matchStatus === 'completed' ||
lot.matchStatus == 'scanned' ||
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected'
}
sx={{
fontSize: '0.7rem',
py: 0.5,
minHeight: '28px',
minWidth: '60px',
borderColor: 'warning.main',
color: 'warning.main'
const status = String(lot.stockOutLineStatus || "").toLowerCase();
const matchStatus = String(lot.matchStatus || "").toLowerCase();
const completedMatchQty = Number(lot.matchQty ?? 0);
const isSubmitted = lot.matchQty > 0;
const isUnavailable =
lot.lotAvailability === "expired" ||
lot.lotAvailability === "status_unavailable" ||
lot.lotAvailability === "rejected";
const rowLocked = itemCompleted || isUnavailable;
return (
<TableRow
key={`${lot.pickOrderLineId}-${lot.lotId}`}
sx={{
backgroundColor: lot.lotAvailability === "rejected" ? "grey.100" : "inherit",
opacity: lot.lotAvailability === "rejected" ? 0.6 : 1,
"& .MuiTableCell-root": {
color: lot.lotAvailability === "rejected" ? "text.disabled" : "inherit",
},
}}
>
<TableCell>
<Typography variant="body2" fontWeight="bold">
{index + 1}
</Typography>
</TableCell>
<TableCell>
<Typography variant="body2">{lot.routerRoute || "-"}</Typography>
</TableCell>
<TableCell>{lot.handler || "-"}</TableCell>
<TableCell>{lot.itemCode}</TableCell>
<TableCell>{`${lot.itemName}(${lot.uomDesc})`}</TableCell>
<TableCell>
<Box>
<Typography
sx={{
color: lot.lotAvailability === "rejected" ? "text.disabled" : "inherit",
opacity: lot.lotAvailability === "rejected" ? 0.6 : 1,
}}
title="Report missing or bad items"
>
{t("Issue")}
</Button>
</Stack>
</Box>
{lot.lotNo}
</Typography>
</Box>
</TableCell>
<TableCell align="right">
{`${itemTotalActualPickQty.toLocaleString()}(${lot.uomShortDesc || ""})`}
</TableCell>
<TableCell align="center">
{(() => {
const matchStatus = String(lot.matchStatus || "").toLowerCase();
const isRejected = lot.lotAvailability === "rejected";
const isCompleted = matchStatus === "completed";
if (isRejected) {
return <Checkbox checked disabled readOnly size="small" sx={{ color: "error.main", "&.Mui-checked": { color: "error.main" } }} />;
}
if (isCompleted) {
return <Checkbox checked disabled readOnly size="small" sx={{ color: "success.main", "&.Mui-checked": { color: "success.main" } }} />;
}
return <Checkbox checked={false} disabled readOnly size="small" />;
})()}
</TableCell>
</TableRow>
))
<TableCell align="center">
<Box sx={{ display: "flex", justifyContent: "center" }}>
{!isFirstRowOfItem ? (
<Typography variant="body2" color="text.secondary">
-
</Typography>
) : (
<Stack direction="row" spacing={1} alignItems="center">
<TextField
type="number"
size="small"
disabled={rowLocked || !submitQtyFieldEnabledByLotKey[itemKey]}
value={String(
matchStatus === "completed"
? completedMatchQty
: Object.prototype.hasOwnProperty.call(pickQtyData, itemKey)
? pickQtyData[itemKey]
: itemTotalActualPickQty
)}
onChange={(e) => handlePickQtyChange(itemKey, e.target.value)}
inputProps={{ min: 0, step: 1 }}
sx={{
width: 96,
"& .MuiInputBase-input": {
fontSize: "0.75rem",
py: 0.5,
textAlign: "center",
},
}}
/>
<Button
variant="outlined"
size="small"
onClick={() =>
setSubmitQtyFieldEnabledByLotKey((prev) => ({
...prev,
[itemKey]: !(prev[itemKey] === true),
}))
}
disabled={rowLocked}
sx={{
fontSize: "0.7rem",
py: 0.5,
minHeight: "28px",
minWidth: "60px",
borderColor: "warning.main",
color: "warning.main",
}}
>
{t("Edit")}
</Button>
<Button
variant="contained"
size="small"
onClick={async () => {
const submitQty = Number(
Object.prototype.hasOwnProperty.call(pickQtyData, itemKey)
? pickQtyData[itemKey]
: itemTotalActualPickQty
);
await updateSecondQrScanStatus(
lot.pickOrderId,
lot.itemId,
currentUserId || 0,
submitQty
);
await handleSubmitPickQtyWithQty(lot, submitQty);
}}
disabled={rowLocked}
sx={{
fontSize: "0.7rem",
py: 0.5,
minHeight: "28px",
minWidth: "70px",
}}
>
{t("Submit")}
</Button>
</Stack>
)}
</Box>
</TableCell>
</TableRow>
);
})
)}
</TableBody>
</Table>


Laden…
Abbrechen
Speichern