Bläddra i källkod

update

MergeProblem1
CANCERYS\kw093 3 dagar sedan
förälder
incheckning
6bec9ce850
6 ändrade filer med 148 tillägg och 59 borttagningar
  1. +1
    -0
      src/app/api/jo/actions.ts
  2. +34
    -6
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  3. +32
    -20
      src/components/JoSearch/JoCreateFormModal.tsx
  4. +18
    -18
      src/components/Jodetail/completeJobOrderRecord.tsx
  5. +62
    -15
      src/components/Jodetail/newJobPickExecution.tsx
  6. +1
    -0
      src/i18n/zh/jo.json

+ 1
- 0
src/app/api/jo/actions.ts Visa fil

@@ -576,6 +576,7 @@ export interface PickOrderLineWithLotsResponse {
itemCode: string | null;
itemName: string | null;
requiredQty: number | null;
totalAvailableQty?: number | null;
uomCode: string | null;
uomDesc: string | null;
status: string | null;


+ 34
- 6
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Visa fil

@@ -513,6 +513,8 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
// issue form 里填的 actualPickQty(用于 batch submit 只提交实际拣到数量,而不是补拣到 required)
const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({});
// 防止重复点击(Submit / Just Completed / Issue)
const [actionBusyBySolId, setActionBusyBySolId] = useState<Record<number, boolean>>({});
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
@@ -2398,8 +2400,14 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
console.error("No stock out line found for this lot");
return;
}
const solId = Number(lot.stockOutLineId);
if (solId > 0 && actionBusyBySolId[solId]) {
console.warn("Action already in progress for stockOutLineId:", solId);
return;
}
try {
if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
// Special case: If submitQty is 0 and all values are 0, mark as completed with qty: 0
if (submitQty === 0) {
console.log(`=== SUBMITTING ALL ZEROS CASE ===`);
@@ -2524,8 +2532,10 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
} catch (error) {
console.error("Error submitting pick quantity:", error);
} finally {
if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: false }));
}
}, [fetchAllCombinedLotData, checkAndAutoAssignNext]);
}, [fetchAllCombinedLotData, checkAndAutoAssignNext, actionBusyBySolId]);

const handleSkip = useCallback(async (lot: any) => {
try {
@@ -2592,8 +2602,14 @@ const handleStartScan = useCallback(() => {
console.error(" No stockOutLineId found for lot:", lot);
return;
}
const solId = Number(stockOutLineId);
if (solId > 0 && actionBusyBySolId[solId]) {
console.warn("Action already in progress for stockOutLineId:", solId);
return;
}
try {
if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
// Step 1: Update stock out line status
await updateStockOutLineStatus({
id: stockOutLineId,
@@ -2641,8 +2657,10 @@ const handleStartScan = useCallback(() => {
await fetchAllCombinedLotData();
} catch (error) {
console.error(" Error in handlelotnull:", error);
} finally {
if (solId > 0) setActionBusyBySolId(prev => ({ ...prev, [solId]: false }));
}
}, [fetchAllCombinedLotData, session, currentUserId, fgPickOrders]);
}, [fetchAllCombinedLotData, session, currentUserId, fgPickOrders, actionBusyBySolId]);
const handleBatchScan = useCallback(async () => {
const startTime = performance.now();
console.log(`⏱️ [BATCH SCAN START]`);
@@ -3299,7 +3317,10 @@ paginatedData.map((lot, index) => {
variant="outlined"
size="small"
onClick={() => handlelotnull(lot)}
disabled={status === 'completed'}
disabled={
status === 'completed' ||
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
}
sx={{
fontSize: '0.7rem',
py: 0.5,
@@ -3330,7 +3351,8 @@ paginatedData.map((lot, index) => {
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected' ||
lot.stockOutLineStatus === 'completed' ||
lot.stockOutLineStatus === 'pending'
lot.stockOutLineStatus === 'pending' ||
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
}
sx={{ fontSize: '0.75rem', py: 0.5, minHeight: '28px', minWidth: '70px' }}
>
@@ -3343,7 +3365,8 @@ paginatedData.map((lot, index) => {
onClick={() => handlePickExecutionForm(lot)}
disabled={
lot.lotAvailability === 'expired' ||
lot.stockOutLineStatus === 'completed'
lot.stockOutLineStatus === 'completed' ||
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
}
sx={{
fontSize: '0.7rem',
@@ -3361,7 +3384,12 @@ paginatedData.map((lot, index) => {
variant="outlined"
size="small"
onClick={() => handleSkip(lot)}
disabled={lot.stockOutLineStatus === 'completed'}
disabled={
lot.stockOutLineStatus === 'completed' ||
// 使用 issue form 後,禁用「Just Completed」(避免再次点击造成重复提交)
(Number(lot.stockOutLineId) > 0 && issuePickedQtyBySolId[Number(lot.stockOutLineId)] !== undefined) ||
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true)
}
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '60px' }}
>
{t("Just Completed")}


+ 32
- 20
src/components/JoSearch/JoCreateFormModal.tsx Visa fil

@@ -3,7 +3,7 @@ import { JoDetail } from "@/app/api/jo";
import { SaveJo, manualCreateJo } from "@/app/api/jo/actions";
import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT, dateStringToDayjs, dayjsToDateString, dayjsToDateTimeString } from "@/app/utils/formatUtil";
import { Check } from "@mui/icons-material";
import { Autocomplete, Box, Button, Card, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material";
import { Autocomplete, Box, Button, Card, CircularProgress, Grid, Modal, Stack, TextField, Typography ,FormControl, InputLabel, Select, MenuItem,InputAdornment} from "@mui/material";
import { DatePicker, DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
@@ -31,6 +31,7 @@ const JoCreateFormModal: React.FC<Props> = ({
}) => {
const { t } = useTranslation("jo");
const [multiplier, setMultiplier] = useState<number>(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const formProps = useForm<SaveJo>({
mode: "onChange",
defaultValues: {
@@ -73,10 +74,11 @@ const JoCreateFormModal: React.FC<Props> = ({
}
}, [multiplier, selectedBomId, bomCombo, formProps]);
const onModalClose = useCallback(() => {
if (isSubmitting) return;
reset()
onClose()
setMultiplier(1);
}, [reset, onClose])
}, [reset, onClose, isSubmitting])
const duplicateLabels = useMemo(() => {
const count = new Map<string, number>();
bomCombo.forEach((b) => count.set(b.label, (count.get(b.label) ?? 0) + 1));
@@ -149,23 +151,32 @@ const JoCreateFormModal: React.FC<Props> = ({
}, [])

const onSubmit = useCallback<SubmitHandler<SaveJo>>(async (data) => {
data.type = "manual"
if (data.planStart) {
const dateDayjs = dateStringToDayjs(data.planStart)
data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day'))
}
data.jobTypeId = Number(data.jobTypeId);
// 如果 productionPriority 为空或无效,使用默认值 50
data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority)
? Number(data.productionPriority)
: 50;
const response = await manualCreateJo(data)
if (response) {
onSearch();
msg(t("update success"));
onModalClose();
if (isSubmitting) return;
setIsSubmitting(true);
try {
data.type = "manual"
if (data.planStart) {
const dateDayjs = dateStringToDayjs(data.planStart)
data.planStart = dayjsToDateTimeString(dateDayjs.startOf('day'))
}
data.jobTypeId = Number(data.jobTypeId);
// 如果 productionPriority 为空或无效,使用默认值 50
data.productionPriority = data.productionPriority != null && !isNaN(data.productionPriority)
? Number(data.productionPriority)
: 50;
const response = await manualCreateJo(data)
if (response) {
onSearch();
msg(t("update success"));
onModalClose();
}
} catch (e) {
console.error(e);
msg(t("update failed"));
} finally {
setIsSubmitting(false);
}
}, [onSearch, onModalClose, t])
}, [onSearch, onModalClose, t, isSubmitting])

const onSubmitError = useCallback<SubmitErrorHandler<SaveJo>>((error) => {
console.log(error)
@@ -505,10 +516,11 @@ const JoCreateFormModal: React.FC<Props> = ({
<Button
name="submit"
variant="contained"
startIcon={<Check />}
startIcon={isSubmitting ? <CircularProgress size={16} color="inherit" /> : <Check />}
type="submit"
disabled={isSubmitting}
>
{t("Create")}
{isSubmitting ? t("Creating...") : t("Create")}
</Button>
</Stack>
</LocalizationProvider>


+ 18
- 18
src/components/Jodetail/completeJobOrderRecord.tsx Visa fil

@@ -76,32 +76,32 @@ interface CompletedJobOrderPickOrder {

// 新增:Lot 详情接口
interface LotDetail {
lotId: number;
lotNo: string;
expiryDate: string;
location: string;
availableQty: number;
requiredQty: number;
actualPickQty: number;
lotId: number | null;
lotNo: string | null;
expiryDate: string | null;
location: string | null;
availableQty: number | null;
requiredQty: number | null;
actualPickQty: number | null;
processingStatus: string;
lotAvailability: string;
pickOrderId: number;
pickOrderCode: string;
pickOrderConsoCode: string;
pickOrderLineId: number;
stockOutLineId: number;
stockOutLineId: number | null;
stockOutLineStatus: string;
routerIndex: number;
routerArea: string;
routerRoute: string;
uomShortDesc: string;
routerIndex: number | null;
routerArea: string | null;
routerRoute: string | null;
uomShortDesc: string | null;
secondQrScanStatus: string;
itemId: number;
itemCode: string;
itemName: string;
uomCode: string;
uomDesc: string;
match_status: string;
match_status: string | null;
}

const CompleteJobOrderRecord: React.FC<Props> = ({
@@ -176,7 +176,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
const lotDetails = await fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId);
setDetailLotData(lotDetails);
setDetailLotData(Array.isArray(lotDetails) ? lotDetails : []);
console.log(" Fetched lot details:", lotDetails);
} catch (error) {
console.error("❌ Error fetching lot details:", error);
@@ -481,7 +481,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
</TableRow>
) : (
detailLotData.map((lot, index) => (
<TableRow key={lot.lotId}>
<TableRow key={lot.stockOutLineId ?? lot.lotId ?? index}>
<TableCell>
<Typography variant="body2" fontWeight="bold">
{index + 1}
@@ -494,13 +494,13 @@ const CompleteJobOrderRecord: React.FC<Props> = ({
</TableCell>
<TableCell>{lot.itemCode}</TableCell>
<TableCell>{lot.itemName}</TableCell>
<TableCell>{lot.lotNo}</TableCell>
<TableCell>{lot.lotNo || '-'}</TableCell>
<TableCell align="right">
{lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc})
{lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc || ''})
</TableCell>
<TableCell align="right">
{lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc})
{lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc || ''})
</TableCell>
{/* 修改:Processing Status 使用复选框 */}
<TableCell align="center">


+ 62
- 15
src/components/Jodetail/newJobPickExecution.tsx Visa fil

@@ -464,6 +464,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
// issue form 里填的 actualPickQty(用于 submit/batch submit 不补拣到 required)
const [issuePickedQtyBySolId, setIssuePickedQtyBySolId] = useState<Record<number, number>>({});
// 防止同一行(以 stockOutLineId/solId 识别)被重复点击提交/完成
const [actionBusyBySolId, setActionBusyBySolId] = useState<Record<number, boolean>>({});

const [paginationController, setPaginationController] = useState({
pageNum: 0,
@@ -553,6 +555,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
itemName: line.itemName,
uomCode: line.uomCode,
uomDesc: line.uomDesc,
itemTotalAvailableQty: line.totalAvailableQty ?? null,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
jobOrderId: data.pickOrder.jobOrder.id,
@@ -588,6 +591,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
itemName: line.itemName,
uomCode: line.uomCode,
uomDesc: line.uomDesc,
itemTotalAvailableQty: line.totalAvailableQty ?? null,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
jobOrderId: data.pickOrder.jobOrder.id,
@@ -2505,6 +2509,7 @@ const sortedData = [...sourceData].sort((a, b) => {
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Lot No")}</TableCell>
<TableCell align="right">{t("Lot Required Pick Qty")}</TableCell>
<TableCell align="right">{t("Available Qty")}</TableCell>
<TableCell align="center">{t("Scan Result")}</TableCell>
<TableCell align="center">{t("Submit Required Pick Qty")}</TableCell>
</TableRow>
@@ -2512,7 +2517,7 @@ const sortedData = [...sourceData].sort((a, b) => {
<TableBody>
{paginatedData.length === 0 ? (
<TableRow>
<TableCell colSpan={8} align="center">
<TableCell colSpan={9} align="center">
<Typography variant="body2" color="text.secondary">
{t("No data available")}
</Typography>
@@ -2551,7 +2556,18 @@ const sortedData = [...sourceData].sort((a, b) => {
<TableCell align="right">
{(() => {
const requiredQty = lot.requiredQty || 0;
return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
const unit = (lot.noLot === true || !lot.lotId)
? (lot.uomDesc || "")
: ( lot.uomDesc || "");
return `${requiredQty.toLocaleString()}(${unit})`;
})()}
</TableCell>
<TableCell align="right">
{(() => {
const avail = lot.itemTotalAvailableQty;
if (avail == null) return "-";
const unit = lot.uomDesc || "";
return `${Number(avail).toLocaleString()}(${unit})`;
})()}
</TableCell>
@@ -2636,13 +2652,24 @@ const sortedData = [...sourceData].sort((a, b) => {
<Button
variant="contained"
onClick={async () => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
handlePickQtyChange(lotKey, submitQty);
await handleSubmitPickQtyWithQty(lot, submitQty);
const solId = Number(lot.stockOutLineId) || 0;
if (solId > 0) {
setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
}
try {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
handlePickQtyChange(lotKey, submitQty);

await handleSubmitPickQtyWithQty(lot, submitQty);
} finally {
if (solId > 0) {
setActionBusyBySolId(prev => ({ ...prev, [solId]: false }));
}
}
}}
disabled={
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) ||
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
@@ -2682,17 +2709,37 @@ const sortedData = [...sourceData].sort((a, b) => {
variant="outlined"
size="small"
onClick={async () => {
// ✅ 更新 handler 后再提交
if (currentUserId && lot.pickOrderId && lot.itemId) {
try {
await updateHandledBy(lot.pickOrderId, lot.itemId);
} catch (error) {
console.error("❌ Error updating handler (non-critical):", error);
const solId = Number(lot.stockOutLineId) || 0;
if (solId > 0) {
setActionBusyBySolId(prev => ({ ...prev, [solId]: true }));
}
try {
// ✅ 更新 handler 后再提交
if (currentUserId && lot.pickOrderId && lot.itemId) {
try {
await updateHandledBy(lot.pickOrderId, lot.itemId);
} catch (error) {
console.error("❌ Error updating handler (non-critical):", error);
}
}
await handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
} finally {
if (solId > 0) {
setActionBusyBySolId(prev => ({ ...prev, [solId]: false }));
}
}
await handleSubmitPickQtyWithQty(lot, lot.requiredQty || lot.pickOrderLineRequiredQty || 0);
}}
disabled={lot.stockOutLineStatus === 'completed' || lot.noLot === true || !lot.lotId}
disabled={
(Number(lot.stockOutLineId) > 0 && actionBusyBySolId[Number(lot.stockOutLineId)] === true) ||
lot.stockOutLineStatus === 'completed' ||
lot.noLot === true ||
!lot.lotId ||
(Number(lot.stockOutLineId) > 0 &&
Object.prototype.hasOwnProperty.call(
issuePickedQtyBySolId,
Number(lot.stockOutLineId)
))
}
sx={{ fontSize: '0.7rem', py: 0.5, minHeight: '28px', minWidth: '90px' }}
>
{t("Just Complete")}


+ 1
- 0
src/i18n/zh/jo.json Visa fil

@@ -139,6 +139,7 @@
"Expiry Date": "有效期",
"Target Date": "需求日期",
"Lot Required Pick Qty": "批號需求數",
"Available Qty": "可用數量",
"Job Order Match": "工單對料",
"Lot No": "批號",
"Submit Required Pick Qty": "提交需求數",


Laddar…
Avbryt
Spara