Browse Source

update pick0, search jo

master
CANCERYS\kw093 3 days ago
parent
commit
358a15b25c
15 changed files with 527 additions and 267 deletions
  1. +14
    -3
      src/app/api/jo/actions.ts
  2. +3
    -0
      src/app/api/po/actions.ts
  3. +14
    -9
      src/components/DoDetail/DoDetail.tsx
  4. +40
    -2
      src/components/DoSearch/DoSearch.tsx
  5. +66
    -57
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  6. +4
    -0
      src/components/JoSearch/JoCreateFormModal.tsx
  7. +88
    -125
      src/components/JoSearch/JoSearch.tsx
  8. +6
    -4
      src/components/Jodetail/JoPickOrderList.tsx
  9. +75
    -8
      src/components/Jodetail/JobPickExecution.tsx
  10. +47
    -19
      src/components/Jodetail/JobPickExecutionForm.tsx
  11. +11
    -8
      src/components/Jodetail/JodetailSearch.tsx
  12. +83
    -10
      src/components/Jodetail/newJobPickExecution.tsx
  13. +74
    -22
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  14. +1
    -0
      src/i18n/zh/common.json
  15. +1
    -0
      src/i18n/zh/jo.json

+ 14
- 3
src/app/api/jo/actions.ts View File

@@ -683,7 +683,14 @@ export const fetchProductProcessById = cache(async (id: number) => {
}
);
});

export const updateProductProcessPriority = cache(async (productProcessId: number, productionPriority: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/Process/update/priority/${productProcessId}/${productionPriority}`,
{
method: "POST",
}
);
});
// 根据 Job Order ID 查询
export const fetchProductProcessesByJobOrderId = cache(async (jobOrderId: number) => {
return serverFetchJson<ProductProcessWithLinesResponse[]>(
@@ -879,7 +886,10 @@ export const isCorrectMachineUsed = async (machineCode: string) => {
export const fetchJos = cache(async (data?: SearchJoResultRequest) => {
const queryStr = convertObjToURLSearchParams(data)
console.log("queryStr", queryStr)
const response = serverFetchJson<SearchJoResultResponse>(
const fullUrl = `${BASE_API_URL}/jo/getRecordByPage?${queryStr}`;
console.log("fetchJos full URL:", fullUrl);
console.log("fetchJos BASE_API_URL:", BASE_API_URL);
const response = await serverFetchJson<SearchJoResultResponse>(
`${BASE_API_URL}/jo/getRecordByPage?${queryStr}`,
{
method: "GET",
@@ -889,7 +899,8 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => {
}
}
)

console.log("fetchJos response:", response)
return response
})



+ 3
- 0
src/app/api/po/actions.ts View File

@@ -204,6 +204,9 @@ export const fetchPoListClient = cache(
async (queryParams?: Record<string, any>) => {
if (queryParams) {
const queryString = new URLSearchParams(queryParams).toString();
const fullUrl = `${BASE_API_URL}/po/list?${queryString}`;
console.log("fetchPoListClient full URL:", fullUrl);
console.log("fetchPoListClient BASE_API_URL:", BASE_API_URL);
return serverFetchJson<RecordsRes<PoResult[]>>(
`${BASE_API_URL}/po/list?${queryString}`,
{


+ 14
- 9
src/components/DoDetail/DoDetail.tsx View File

@@ -9,7 +9,7 @@ import { useCallback, useState } from "react";
import { Button, Stack, Typography, Box, Alert } from "@mui/material";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import StartIcon from "@mui/icons-material/Start";
import { releaseDo, assignPickOrderByStore, releaseAssignedPickOrderByStore } from "@/app/api/do/actions";
import { releaseDo,startBatchReleaseAsyncSingle, assignPickOrderByStore, releaseAssignedPickOrderByStore } from "@/app/api/do/actions";
import DoInfoCard from "./DoInfoCard";
import DoLineTable from "./DoLineTable";
import { useSession } from "next-auth/react";
@@ -41,7 +41,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);

const handleBack = useCallback(() => {
router.replace(`/do`)
}, [])
}, [router])

const handleRelease = useCallback(async () => {
try {
@@ -57,12 +57,16 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
// setServerError("User session not found. Please login again.");
// return;
//}
/*
const response = await releaseDo({
id: id,
//userId: currentUserId // Pass user ID from session
})
*/
const response = await startBatchReleaseAsyncSingle({
doId: id,
userId: currentUserId ?? 0
})
if (response) {
formProps.setValue("status", response.entity.status)
setSuccessMessage(t("DO released successfully! Pick orders created."))
@@ -168,8 +172,8 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
</Alert>
)}

{/*{
formProps.watch("status")?.toLowerCase() === "pending" && (
{formProps.watch("status")?.toLowerCase() === "pending" && (
<Stack direction="row" justifyContent="flex-start" gap={1}>
<Button
variant="outlined"
@@ -180,9 +184,10 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
{t("Release")}
</Button>
</Stack>
)}
*/}
)
}
{/* ADD STORE-BASED ASSIGNMENT BUTTONS */}
{/*
{
formProps.watch("status")?.toLowerCase() === "released" && (
<Box sx={{ mb: 2 }}>
@@ -232,7 +237,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
</Stack>
</Box>
)}
*/}
<DoInfoCard />
<DoLineTable />
<Stack direction="row" justifyContent="flex-end" gap={1}>


+ 40
- 2
src/components/DoSearch/DoSearch.tsx View File

@@ -76,7 +76,7 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear
pageNum: 1,
pageSize: 10,
});
const handlePageChange = useCallback((event: unknown, newPage: number) => {
const newPagingController = {
...pagingController,
@@ -175,6 +175,9 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear

const onDetailClick = useCallback(
(doResult: DoResult) => {
if (typeof window !== 'undefined') {
sessionStorage.setItem('doSearchParams', JSON.stringify(currentSearchParams));
}
router.push(`/do/edit?id=${doResult.id}`);
},
[router],
@@ -287,7 +290,7 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear
const handleSearch = useCallback(async (query: SearchBoxInputs) => {
try {
setCurrentSearchParams(query);
let orderStartDate = "";
let orderEndDate = "";
let estArrStartDate = query.estimatedArrivalDate;
@@ -328,13 +331,48 @@ const DoSearch: React.FC<Props> = ({filterArgs, searchQuery, onDeliveryOrderSear
setSearchAllDos(data);
setHasSearched(true);
setHasResults(data.length > 0);

} catch (error) {
console.error("Error: ", error);
setSearchAllDos([]);
setHasSearched(true);
setHasResults(false);

}
}, []);
useEffect(() => {
if (typeof window !== 'undefined') {
const savedSearchParams = sessionStorage.getItem('doSearchParams');
if (savedSearchParams) {
try {
const params = JSON.parse(savedSearchParams);
setCurrentSearchParams(params);
// 自动使用保存的搜索条件重新搜索,获取最新数据
const timer = setTimeout(async () => {
await handleSearch(params);
// 搜索完成后,清除 sessionStorage
if (typeof window !== 'undefined') {
sessionStorage.removeItem('doSearchParams');
sessionStorage.removeItem('doSearchResults');
sessionStorage.removeItem('doSearchHasSearched');
}
}, 100);
return () => clearTimeout(timer);
} catch (e) {
console.error('Error restoring search state:', e);
// 如果出错,也清除 sessionStorage
if (typeof window !== 'undefined') {
sessionStorage.removeItem('doSearchParams');
sessionStorage.removeItem('doSearchResults');
sessionStorage.removeItem('doSearchHasSearched');
}
}
}
}
}, [handleSearch]);
const debouncedSearch = useCallback((query: SearchBoxInputs) => {
if (searchTimeout) {
clearTimeout(searchTimeout);


+ 66
- 57
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx View File

@@ -27,7 +27,6 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";
import {
fetchALLPickOrderLineLotDetails,
updateStockOutLineStatus,
createStockOutLine,
updateStockOutLine,
@@ -634,6 +633,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
const flatLotData: any[] = [];
mergedPickOrder.pickOrderLines.forEach((line: any) => {
// ✅ FIXED: 处理 lots(如果有)
if (line.lots && line.lots.length > 0) {
// 修复:先对 lots 按 lotId 去重并合并 requiredQty
const lotMap = new Map<number, any>();
@@ -696,53 +696,54 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
noLot: false,
});
});
} else {
// 没有 lots 的情况(null stock)- 从 stockouts 数组中获取 id
const firstStockout = line.stockouts && line.stockouts.length > 0
? line.stockouts[0]
: null;
flatLotData.push({
pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "", // 修复:consoCodes 是数组
pickOrderTargetDate: mergedPickOrder.targetDate,
pickOrderStatus: mergedPickOrder.status,
pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId
pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
itemId: line.item.id,
itemCode: line.item.code,
itemName: line.item.name,
uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc,
// Null stock 字段 - 从 stockouts 数组中获取
lotId: firstStockout?.lotId || null,
lotNo: firstStockout?.lotNo || null,
expiryDate: null,
location: firstStockout?.location || null,
stockUnit: line.item.uomDesc,
availableQty: firstStockout?.availableQty || 0,
requiredQty: line.requiredQty,
actualPickQty: firstStockout?.qty || 0,
inQty: 0,
outQty: 0,
holdQty: 0,
lotStatus: 'unavailable',
lotAvailability: 'insufficient_stock',
processingStatus: firstStockout?.status || 'pending',
suggestedPickLotId: null,
stockOutLineId: firstStockout?.id || null, // 使用 stockouts 数组中的 id
stockOutLineStatus: firstStockout?.status || null,
stockOutLineQty: firstStockout?.qty || 0,
routerId: null,
routerIndex: 999999,
routerRoute: null,
routerArea: null,
noLot: true,
}
// ✅ FIXED: 同时处理 stockouts(无论是否有 lots)
if (line.stockouts && line.stockouts.length > 0) {
// ✅ FIXED: 处理所有 stockouts,而不仅仅是第一个
line.stockouts.forEach((stockout: any) => {
flatLotData.push({
pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "",
pickOrderTargetDate: mergedPickOrder.targetDate,
pickOrderStatus: mergedPickOrder.status,
pickOrderId: line.pickOrderId || mergedPickOrder.pickOrderIds?.[0] || 0,
pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
itemId: line.item.id,
itemCode: line.item.code,
itemName: line.item.name,
uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc,
// Null stock 字段 - 从 stockouts 数组中获取
lotId: stockout.lotId || null,
lotNo: stockout.lotNo || null,
expiryDate: null,
location: stockout.location || null,
stockUnit: line.item.uomDesc,
availableQty: stockout.availableQty || 0,
requiredQty: line.requiredQty,
actualPickQty: stockout.qty || 0,
inQty: 0,
outQty: 0,
holdQty: 0,
lotStatus: 'unavailable',
lotAvailability: 'insufficient_stock',
processingStatus: stockout.status || 'pending',
suggestedPickLotId: null,
stockOutLineId: stockout.id || null, // 使用 stockouts 数组中的 id
stockOutLineStatus: stockout.status || null,
stockOutLineQty: stockout.qty || 0,
routerId: null,
routerIndex: 999999,
routerRoute: null,
routerArea: null,
noLot: true,
});
});
}
});
@@ -1815,10 +1816,11 @@ const allItemsReady = useMemo(() => {
const isCompleted =
status === 'completed' || status === 'partially_completed' || status === 'partially_complete';
const isChecked = status === 'checked';
const isPending = status === 'pending';

// 无库存(noLot)行:只要状态不是 pending/rejected 即视为已处理
// ✅ FIXED: 无库存(noLot)行:pending 状态也应该被视为 ready(可以提交)
if (lot.noLot === true) {
return isChecked || isCompleted || isRejected;
return isChecked || isCompleted || isRejected || isPending;
}

// 正常 lot:必须已扫描/提交或者被拒收
@@ -2105,14 +2107,13 @@ const handleSubmitAllScanned = useCallback(async () => {
// Calculate scanned items count (should match handleSubmitAllScanned filter logic)
const scannedItemsCount = useMemo(() => {
const filtered = combinedLotData.filter(lot => {
// 如果是 noLot 情况,只要状态不是 completed 或 rejected,就包含
// ✅ FIXED: 使用与 handleSubmitAllScanned 相同的过滤逻辑
if (lot.noLot === true) {
const status = lot.stockOutLineStatus?.toLowerCase();
const include = status !== 'completed' && status !== 'rejected';
if (include) {
console.log(`📊 Including noLot item: ${lot.itemName || lot.itemCode}, status: ${lot.stockOutLineStatus}`);
}
return include;
// ✅ 只包含可以提交的状态(与 handleSubmitAllScanned 保持一致)
return lot.stockOutLineStatus === 'checked' ||
lot.stockOutLineStatus === 'pending' ||
lot.stockOutLineStatus === 'partially_completed' ||
lot.stockOutLineStatus === 'PARTIALLY_COMPLETE';
}
// 正常情况:只包含 checked 状态
return lot.stockOutLineStatus === 'checked';
@@ -2601,6 +2602,14 @@ paginatedData.map((lot, index) => {
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
if (lastProcessedQr) {
setProcessedQrCodes(prev => {
const newSet = new Set(prev);
newSet.delete(lastProcessedQr);
return newSet;
});
setLastProcessedQr('');
}
}}
onConfirm={handleLotConfirmation}
expectedLot={expectedLotData}


+ 4
- 0
src/components/JoSearch/JoCreateFormModal.tsx View File

@@ -174,6 +174,10 @@ const JoCreateFormModal: React.FC<Props> = ({
error={Boolean(error)}
variant="outlined"
type="number"
disabled={true}
// sx={{
// backgroundColor: "background.paper",
// }}
value={field.value ?? ""}
onChange={(e) => {
const val = e.target.value === "" ? undefined : Number(e.target.value);


+ 88
- 125
src/components/JoSearch/JoSearch.tsx View File

@@ -22,11 +22,11 @@ import { SessionWithTokens } from "@/config/authConfig";
import { createStockInLine } from "@/app/api/stockIn/actions";
import { msg } from "../Swal/CustomAlerts";
import dayjs from "dayjs";

import { fetchInventories } from "@/app/api/inventory/actions";
import { InventoryResult } from "@/app/api/inventory";
import { PrinterCombo } from "@/app/api/settings/printer";
import { JobTypeResponse } from "@/app/api/jo/actions";

interface Props {
defaultInputs: SearchJoResultRequest,
bomCombo: BomCombo[]
@@ -35,7 +35,6 @@ interface Props {
}

type SearchQuery = Partial<Omit<JobOrder, "id">>;

type SearchParamNames = keyof SearchQuery;

const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobTypes }) => {
@@ -49,9 +48,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
const [totalCount, setTotalCount] = useState(0)
const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false)

// console.log(inputs)
const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);

const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map());

const fetchJoDetailClient = async (id: number): Promise<JobOrder> => {
@@ -68,7 +65,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
for (const jo of filteredJos) {
try {
const detailedJo = await fetchJoDetailClient(jo.id); // Use client function
const detailedJo = await fetchJoDetailClient(jo.id);
detailedMap.set(jo.id, detailedJo);
} catch (error) {
console.error(`Error fetching detail for JO ${jo.id}:`, error);
@@ -84,20 +81,20 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
}, [filteredJos]);

useEffect(() => {
const fetchInventoryData = async () => {
try {
const inventoryResponse = await fetchInventories({
code: "",
name: "",
type: "",
pageNum: 0,
pageSize: 1000
});
setInventoryData(inventoryResponse.records);
} catch (error) {
console.error("Error fetching inventory data:", error);
}
};
const fetchInventoryData = async () => {
try {
const inventoryResponse = await fetchInventories({
code: "",
name: "",
type: "",
pageNum: 0,
pageSize: 1000
});
setInventoryData(inventoryResponse.records);
} catch (error) {
console.error("Error fetching inventory data:", error);
}
};

fetchInventoryData();
}, []);
@@ -120,7 +117,6 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
};

const getStockCounts = (jo: JobOrder) => {

return {
sufficient: jo.sufficientCount,
insufficient: jo.insufficientCount
@@ -140,7 +136,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
type: "select",
options: jobTypes.map(jt => jt.name)
},
], [t])
], [t, jobTypes])

const columns = useMemo<Column<JobOrder>[]>(
() => [
@@ -177,7 +173,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
{
name: "status",
label: t("Status"),
renderCell: (row) => { // TODO improve
renderCell: (row) => {
return <span style={{color: row.stockInLineStatus == "escalated" ? "red" : "inherit"}}>
{t(upperFirst(row.status))}
</span>
@@ -213,36 +209,62 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
}
},
{
// TODO put it inside Action Buttons
name: "id",
label: t("Actions"),
// onClick: (record) => onDetailClick(record),
// buttonIcon: <EditNote />,
renderCell: (row) => {
//const btnSx = getButtonSx(row);
return (
<Button
id="emailSupplier"
type="button"
variant="contained"
color="primary"
// sx={{ width: "150px", backgroundColor: btnSx.color }}
sx={{ width: "150px" }}
// disabled={params.row.status != "rejected" && params.row.status != "partially_completed"}
onClick={() => onDetailClick(row)}
// >{btnSx.label}
>{t("View")}
id="emailSupplier"
type="button"
variant="contained"
color="primary"
sx={{ width: "150px" }}
onClick={() => onDetailClick(row)}
>
{t("View")}
</Button>
)
}
},
], [inventoryData, detailedJos]
], [t, inventoryData, detailedJos]
)

// 按照 PoSearch 的模式:创建 newPageFetch 函数
const newPageFetch = useCallback(
async (
pagingController: { pageNum: number; pageSize: number },
filterArgs: SearchJoResultRequest,
) => {
const params: SearchJoResultRequest = {
...filterArgs,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
};
const response = await fetchJos(params);
console.log("newPageFetch params:", params)
console.log("newPageFetch response:", response)
if (response && response.records) {
console.log("newPageFetch - setting filteredJos with", response.records.length, "records");
setTotalCount(response.total);
// 后端已经按 id DESC 排序,不需要再次排序
setFilteredJos(response.records);
console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id);
} else {
console.warn("newPageFetch - no response or no records");
setFilteredJos([]);
}
},
[],
);

// 按照 PoSearch 的模式:使用相同的 useEffect 逻辑
useEffect(() => {
newPageFetch(pagingController, inputs);
}, [newPageFetch, pagingController, inputs]);

const handleUpdate = useCallback(async (jo: JobOrder) => {
console.log(jo);
try {
// setIsUploading(true)
if (jo.id) {
const response = await updateJo({ id: jo.id, status: "storing" });
console.log(`%c Updated JO:`, "color:lime", response);
@@ -252,64 +274,22 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
productLotNo: jo?.code,
productionDate: arrayToDateString(dayjs(), "input"),
jobOrderId: jo?.id,
// acceptedQty: secondReceiveQty || 0,
// acceptedQty: row.acceptedQty,
};
const res = await createStockInLine(postData);
console.log(`%c Created Stock In Line`, "color:lime", res);
msg(t("update success"));
refetchData(defaultInputs, "search");
// 重置为默认输入,让 useEffect 自动触发
setInputs(defaultInputs);
setPagingController(defaultPagingController);
}

} catch (e) {
// backend error
// setServerError(t("An error has occurred. Please try again later."));
console.log(e);
} finally {
// setIsUploading(false)
}
}, [])
}, [defaultInputs, t])

const refetchData = useCallback(async (
query: Record<SearchParamNames, string> | SearchJoResultRequest,
actionType: "reset" | "search" | "paging",
) => {
const params: SearchJoResultRequest = {
code: query.code,
itemName: query.itemName,
planStart: query.planStart,
planStartTo: query.planStartTo,
pageNum: pagingController.pageNum - 1,
pageSize: pagingController.pageSize,
jobTypeName: query.jobTypeName||"",
}
const response = await fetchJos(params)

if (response) {
setTotalCount(response.total);
switch (actionType) {
case "reset":
case "search":
setFilteredJos(() => orderBy(response.records, ["id"], ["desc"]));
break;
case "paging":
setFilteredJos((fs) =>
orderBy(uniqBy([...fs, ...response.records], "id"), ["id"], ["desc"]),
);
break;
}
}
}, [pagingController, setPagingController])

const searchDataByPage = useCallback(() => {
refetchData(inputs, "paging");
}, [inputs,refetchData])
/*
useEffect(() => {
searchDataByPage();
}, [pagingController,searchDataByPage ]);
*/
const getButtonSx = (jo : JobOrder) => { // TODO put it in ActionButtons.ts
const getButtonSx = (jo : JobOrder) => {
const joStatus = jo.status?.toLowerCase();
const silStatus = jo.stockInLineStatus?.toLowerCase();
let btnSx = {label:"", color:""};
@@ -317,8 +297,6 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
case "planning": btnSx = {label: t("release jo"), color:"primary.main"}; break;
case "pending": btnSx = {label: t("scan picked material"), color:"error.main"}; break;
case "processing": btnSx = {label: t("complete jo"), color:"warning.main"}; break;
// case "packaging":
// case "storing": btnSx = {label: t("view putaway"), color:"secondary.main"}; break;
case "storing":
switch (silStatus) {
case "pending": btnSx = {label: t("process epqc"), color:"success.main"}; break;
@@ -342,60 +320,44 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT

const [openModal, setOpenModal] = useState<boolean>(false);
const [modalInfo, setModalInfo] = useState<StockInLineInput>();
/*
const onDetailClick = useCallback((record: JobOrder) => {

if (record.status == "processing") {
handleUpdate(record)
} else if (record.status == "storing" || record.status == "completed") {
if (record.stockInLineId != null) {
const data = {
id: record.stockInLineId,
expiryDate: arrayToDateString(dayjs().add(1, "month"), "input"),
}
setModalInfo(data);
setOpenModal(true);
} else { alert('Invalid Stock In Line Id'); }
} else {
router.push(`/jo/edit?id=${record.id}`)
}
}, [])
*/
const onDetailClick = useCallback((record: JobOrder) => {
router.push(`/jo/edit?id=${record.id}`)
}, [])
const closeNewModal = useCallback(() => {
// const response = updateJo({ id: 1, status: "storing" });
setOpenModal(false); // Close the modal first
// setTimeout(() => {
// }, 300); // Add a delay to avoid immediate re-trigger of useEffect
refetchData(defaultInputs, "search");
}, []);
}, [router])

const closeNewModal = useCallback(() => {
setOpenModal(false);
setInputs(defaultInputs);
setPagingController(defaultPagingController);
}, [defaultInputs]);

const onSearch = useCallback((query: Record<SearchParamNames, string>) => {
const transformedQuery = {
...query,
planStart: query.planStart ? `${query.planStart}T00:00:00` : query.planStart,
planStart: query.planStart ? `${query.planStart}T00:00` : query.planStart,
planStartTo: query.planStartTo ? `${query.planStartTo}T23:59:59` : query.planStartTo,
jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : ""
};
setInputs(() => ({
setInputs({
code: transformedQuery.code,
itemName: transformedQuery.itemName,
planStart: transformedQuery.planStart,
planStartTo: transformedQuery.planStartTo,
jobTypeName: transformedQuery.jobTypeName
}))
refetchData(transformedQuery, "search");
}, [])
});
setPagingController(defaultPagingController);
}, [defaultInputs])

const onReset = useCallback(() => {
refetchData(defaultInputs, "paging");
}, [])
setInputs(defaultInputs);
setPagingController(defaultPagingController);
}, [defaultInputs])

// Manual Create Jo Related

const onOpenCreateJoModal = useCallback(() => {
setIsCreateJoModalOpen(() => true)
@@ -425,19 +387,21 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
onSearch={onSearch}
onReset={onReset}
/>
<SearchResults<JobOrder>
<SearchResults<JobOrder>
items={filteredJos}
columns={columns}
setPagingController={setPagingController}
pagingController={pagingController}
totalCount={totalCount}
// isAutoPaging={false}
isAutoPaging={false}
/>
<JoCreateFormModal
open={isCreateJoModalOpen}
bomCombo={bomCombo}
onClose={onCloseCreateJoModal}
onSearch={searchDataByPage}
onSearch={() => {
}}
/>

<QcStockInModal
@@ -446,7 +410,6 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
onClose={closeNewModal}
inputDetail={modalInfo}
printerCombo={printerCombo}
// skipQc={true}
/>
</>
}


+ 6
- 4
src/components/Jodetail/JoPickOrderList.tsx View File

@@ -17,17 +17,19 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import { useTranslation } from "react-i18next";
import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actions";
import JobPickExecution from "./newJobPickExecution";

interface Props {
onSwitchToRecordTab?: () => void;
}
const PER_PAGE = 6;

const JoPickOrderList: React.FC = () => {
const JoPickOrderList: React.FC<Props> = ({ onSwitchToRecordTab }) =>{
const { t } = useTranslation(["common", "jo"]);
const [loading, setLoading] = useState(false);
const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]);
const [page, setPage] = useState(0);
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined);
const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined);
const fetchPickOrders = useCallback(async () => {
setLoading(true);
try {
@@ -62,7 +64,7 @@ const JoPickOrderList: React.FC = () => {
{t("Back to List")}
</Button>
</Box>
<JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} />
<JobPickExecution filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }} onSwitchToRecordTab={onSwitchToRecordTab} />
</Box>
);
}


+ 75
- 8
src/components/Jodetail/JobPickExecution.tsx View File

@@ -1236,17 +1236,73 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
console.log('=== handleSubmitPickQtyWithQty called ===');
console.log('Lot:', lot);
console.log('submitQty:', submitQty);
console.log('stockOutLineId:', lot.stockOutLineId);
if (!lot.stockOutLineId) {
console.error("No stock out line found for this lot");
console.error("No stock out line found for this lot:", lot);
alert(`Error: No stock out line ID found for lot ${lot.lotNo}. Cannot update status.`);
return;
}
try {
// FIXED: Calculate cumulative quantity correctly
// 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 ===`);
console.log(`Lot: ${lot.lotNo}`);
console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
console.log(`Setting status to 'completed' with qty: 0`);
const updateResult = await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: 'completed',
qty: 0
});
console.log('Update result:', updateResult);
if (!updateResult || (updateResult as any).code !== 'SUCCESS') {
console.error('Failed to update stock out line status:', updateResult);
throw new Error('Failed to update stock out line status');
}
// Check if pick order is completed
if (lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
try {
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(` Pick order completion check result:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
} else if (completionResponse.message === "not completed") {
console.log(`⏳ Pick order not completed yet, more lines remaining`);
} else {
console.error(`❌ Error checking completion: ${completionResponse.message}`);
}
} catch (error) {
console.error("Error checking pick order completion:", error);
}
}
await fetchJobOrderData();
console.log("All zeros submission completed successfully!");
setTimeout(() => {
checkAndAutoAssignNext();
}, 1000);
return;
}
// Normal case: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
// FIXED: Determine status based on cumulative quantity vs required quantity
// Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
@@ -1269,7 +1325,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty // Use cumulative quantity
qty: cumulativeQty
});
if (submitQty > 0) {
@@ -1281,7 +1337,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}
// Check if pick order is completed when lot status becomes 'completed'
// Check if pick order is completed when lot status becomes 'completed'
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
@@ -1910,13 +1966,24 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
// Add missing required properties from GetPickOrderLineInfo interface
availableQty: selectedLotForExecutionForm.availableQty || 0,
requiredQty: selectedLotForExecutionForm.requiredQty || 0,
uomCode: selectedLotForExecutionForm.uomCode || '',
uomDesc: selectedLotForExecutionForm.uomDesc || '',
pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty
suggestedList: [] // Add required suggestedList property
uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
suggestedList: [],
noLotLines: []
}}
pickOrderId={selectedLotForExecutionForm.pickOrderId}
pickOrderCreateDate={new Date()}
onNormalPickSubmit={async (lot, submitQty) => {
console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty });
if (!lot) {
console.error('Lot is null or undefined');
return;
}
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
handlePickQtyChange(lotKey, submitQty);
await handleSubmitPickQtyWithQty(lot, submitQty);
}}
/>
)}
</FormProvider>


+ 47
- 19
src/components/Jodetail/JobPickExecutionForm.tsx View File

@@ -44,6 +44,9 @@ interface LotPickData {
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
pickOrderLineId?: number;
pickOrderId?: number;
pickOrderCode?: string;
}

interface PickExecutionFormProps {
@@ -54,6 +57,7 @@ interface PickExecutionFormProps {
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
onNormalPickSubmit?: (lot: LotPickData, submitQty: number) => Promise<void>;
// Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
@@ -76,9 +80,8 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// Remove these props
// onNormalPickSubmit,
// selectedRowId,
onNormalPickSubmit,
}) => {
const { t } = useTranslation();
const [formData, setFormData] = useState<Partial<PickExecutionIssueData>>({});
@@ -87,6 +90,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
const [verifiedQty, setVerifiedQty] = useState<number>(0);
const { data: session } = useSession() as { data: SessionWithTokens | null };
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
return lot.availableQty || 0;
}, []);
@@ -95,7 +99,15 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
useEffect(() => {
console.log('PickExecutionForm props:', {
open,
onNormalPickSubmit: typeof onNormalPickSubmit,
hasOnNormalPickSubmit: !!onNormalPickSubmit,
onSubmit: typeof onSubmit,
});
}, [open, onNormalPickSubmit, onSubmit]);

// 获取处理人员列表
useEffect(() => {
const fetchHandlers = async () => {
@@ -184,36 +196,52 @@ useEffect(() => {
if (verifiedQty === undefined || verifiedQty < 0) {
newErrors.actualPickQty = t('Qty is required');
}
// 移除接收数量检查,因为在 JobPickExecution 阶段 receivedQty 总是 0
// if (verifiedQty > receivedQty) { ... } ← 删除
// 只检查总和是否等于需求数量

const totalQty = verifiedQty + badItemQty + missQty;
if (totalQty !== requiredQty) {
const hasAnyValue = verifiedQty > 0 || badItemQty > 0 || missQty > 0;
if (hasAnyValue && totalQty !== requiredQty) {
newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');
}
// Require either missQty > 0 OR badItemQty > 0
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
if (!hasMissQty && !hasBadItemQty) {
newErrors.missQty = t('At least one issue must be reported');
newErrors.badItemQty = t('At least one issue must be reported');
}

setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async () => {
if (!formData.pickOrderId || !selectedLot) {
return;
}
// Handle normal pick submission: verifiedQty > 0 with no issues, OR all zeros (verifiedQty=0, missQty=0, badItemQty=0)
const isNormalPick = (verifiedQty > 0 || (verifiedQty === 0 && formData.missQty == 0 && formData.badItemQty == 0))
&& formData.missQty == 0 && formData.badItemQty == 0;
if (isNormalPick) {
if (onNormalPickSubmit) {
setLoading(true);
try {
console.log('Calling onNormalPickSubmit with:', { lot: selectedLot, submitQty: verifiedQty });
await onNormalPickSubmit(selectedLot, verifiedQty);
onClose();
} catch (error) {
console.error('Error submitting normal pick:', error);
} finally {
setLoading(false);
}
} else {
console.warn('onNormalPickSubmit callback not provided');
}
return;
}

if (!validateForm() || !formData.pickOrderId) {
return;
}

setLoading(true);
try {
// Use the verified quantity in the submission
const submissionData = {
...formData,
actualPickQty: verifiedQty,


+ 11
- 8
src/components/Jodetail/JodetailSearch.tsx View File

@@ -387,7 +387,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
},
[pickOrders, t, tabIndex, items],
);

const handleSwitchToRecordTab = useCallback(() => {
setTabIndex(1); // 切换到 CompleteJobOrderRecord 标签页(tabIndex 1)
}, []);
const fetchNewPagePickOrder = useCallback(
async (
pagingController: Record<string, number>,
@@ -438,10 +440,10 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
<Grid item xs={8}>

</Grid>
{/* Last 2 buttons aligned right

{/* Last 2 buttons aligned right */}
<Grid item xs={6} >
{/* Unassigned Job Orders */}
{!hasAnyAssignedData && unassignedOrders && unassignedOrders.length > 0 && (
<Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: 1 }}>
<Typography variant="h6" gutterBottom>
@@ -463,7 +465,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
</Box>
)}
</Grid>
*/}

</Grid>
</Stack>
@@ -474,9 +476,10 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
borderBottom: '1px solid #e0e0e0'
}}>
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable">
<Tab label={t("Pick Order Detail")} iconPosition="end" />
{/* <Tab label={t("Pick Order Detail")} iconPosition="end" /> */}
<Tab label={t("Jo Pick Order Detail")} iconPosition="end" />
<Tab label={t("Complete Job Order Record")} iconPosition="end" />
{/* <Tab label={t("Jo Pick Order Detail")} iconPosition="end" /> */}
{/* <Tab label={t("Job Order Match")} iconPosition="end" /> */}
{/* <Tab label={t("Finished Job Order Record")} iconPosition="end" /> */}
</Tabs>
@@ -487,9 +490,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
<Box sx={{
p: 2
}}>
{tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />}
{/* {tabIndex === 0 && <JobPickExecution filterArgs={filterArgs} />} */}
{tabIndex === 1 && <CompleteJobOrderRecord filterArgs={filterArgs} printerCombo={printerCombo} />}
{/* {tabIndex === 2 && <JoPickOrderList />} */}
{tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />}
{/* {tabIndex === 2 && <JobPickExecutionsecondscan filterArgs={filterArgs} />} */}
{/* {tabIndex === 3 && <FInishedJobOrderRecord filterArgs={filterArgs} />} */}
</Box>


+ 83
- 10
src/components/Jodetail/newJobPickExecution.tsx View File

@@ -67,6 +67,7 @@ import FGPickOrderCard from "./FGPickOrderCard";
import LotConfirmationModal from "./LotConfirmationModal";
interface Props {
filterArgs: Record<string, any>;
onSwitchToRecordTab: () => void;
}

// QR Code Modal Component (from GoodPickExecution)
@@ -323,7 +324,7 @@ const QrCodeModal: React.FC<{
);
};

const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const JobPickExecution: React.FC<Props> = ({ filterArgs, onSwitchToRecordTab }) => {
const { t } = useTranslation("jo");
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -1180,11 +1181,69 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
try {
// FIXED: Calculate cumulative quantity correctly
// 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 ===`);
console.log(`Lot: ${lot.lotNo}`);
console.log(`Stock Out Line ID: ${lot.stockOutLineId}`);
console.log(`Setting status to 'completed' with qty: 0`);
const updateResult = await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: 'completed',
qty: 0
});
console.log('Update result:', updateResult);
const r: any = updateResult as any;
const updateOk =
r?.code === 'SUCCESS' ||
r?.type === 'completed' ||
typeof r?.id === 'number' ||
typeof r?.entity?.id === 'number' ||
(r?.message && r.message.includes('successfully'));
if (!updateResult || !updateOk) {
console.error('Failed to update stock out line status:', updateResult);
throw new Error('Failed to update stock out line status');
}
// Check if pick order is completed
if (lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed (all zeros), checking if pick order ${lot.pickOrderConsoCode} is complete...`);
try {
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(` Pick order completion check result:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
} else if (completionResponse.message === "not completed") {
console.log(`⏳ Pick order not completed yet, more lines remaining`);
} else {
console.error(`❌ Error checking completion: ${completionResponse.message}`);
}
} catch (error) {
console.error("Error checking pick order completion:", error);
}
}
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
await fetchJobOrderData(pickOrderId);
console.log("All zeros submission completed successfully!");
setTimeout(() => {
checkAndAutoAssignNext();
}, 1000);
return;
}
// Normal case: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
// FIXED: Determine status based on cumulative quantity vs required quantity
// Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
@@ -1207,7 +1266,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty // Use cumulative quantity
qty: cumulativeQty
});
if (submitQty > 0) {
@@ -1219,7 +1278,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}
// Check if pick order is completed when lot status becomes 'completed'
// Check if pick order is completed when lot status becomes 'completed'
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
@@ -1250,7 +1309,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} catch (error) {
console.error("Error submitting pick quantity:", error);
}
}, [fetchJobOrderData, checkAndAutoAssignNext]);
}, [fetchJobOrderData, checkAndAutoAssignNext, filterArgs]);
const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot =>
lot.stockOutLineStatus === 'checked'
@@ -1306,6 +1365,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setTimeout(() => {
setQrScanSuccess(false);
checkAndAutoAssignNext();
if (onSwitchToRecordTab) {
onSwitchToRecordTab();
}
}, 2000);
} else {
console.error("Batch submit failed:", result);
@@ -1318,7 +1380,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} finally {
setIsSubmittingAll(false);
}
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId])
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId, onSwitchToRecordTab])

// Calculate scanned items count
const scannedItemsCount = useMemo(() => {
@@ -1852,13 +1914,24 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
// Add missing required properties from GetPickOrderLineInfo interface
availableQty: selectedLotForExecutionForm.availableQty || 0,
requiredQty: selectedLotForExecutionForm.requiredQty || 0,
uomCode: selectedLotForExecutionForm.uomCode || '',
uomDesc: selectedLotForExecutionForm.uomDesc || '',
pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty
suggestedList: [] // Add required suggestedList property
uomShortDesc: selectedLotForExecutionForm.uomShortDesc || '',
pickedQty: selectedLotForExecutionForm.actualPickQty || 0,
suggestedList: [],
noLotLines: []
}}
pickOrderId={selectedLotForExecutionForm.pickOrderId}
pickOrderCreateDate={new Date()}
onNormalPickSubmit={async (lot, submitQty) => {
console.log('onNormalPickSubmit called in newJobPickExecution:', { lot, submitQty });
if (!lot) {
console.error('Lot is null or undefined');
return;
}
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
handlePickQtyChange(lotKey, submitQty);
await handleSubmitPickQtyWithQty(lot, submitQty);
}}
/>
)}
</FormProvider>


+ 74
- 22
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx View File

@@ -14,10 +14,16 @@ import {
Tabs,
Tab,
TabsProps,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
InputAdornment
} from "@mui/material";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useTranslation } from "react-i18next";
import { fetchProductProcessesByJobOrderId ,deleteJobOrder} from "@/app/api/jo/actions";
import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority} from "@/app/api/jo/actions";
import ProductionProcessDetail from "./ProductionProcessDetail";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil";
@@ -31,6 +37,7 @@ import { InventoryResult } from "@/app/api/inventory";
import { releaseJo, startJo } from "@/app/api/jo/actions";
import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan";
import ProcessSummaryHeader from "./ProcessSummaryHeader";
import EditIcon from "@mui/icons-material/Edit";
interface JobOrderLine {
id: number;
jobOrderId: number;
@@ -64,8 +71,9 @@ const ProductionProcessJobOrderDetail: React.FC<ProductProcessJobOrderDetailProp
const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]);
const [tabIndex, setTabIndex] = useState(0);
const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);

// 获取数据
const [operationPriority, setOperationPriority] = useState<number>(50);
const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false);
const fetchData = useCallback(async () => {
setLoading(true);
try {
@@ -117,7 +125,25 @@ const getStockAvailable = (line: JobOrderLine) => {
}
return line.stockQty || 0;
};
const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => {
const response = await updateProductProcessPriority(productProcessId, productionPriority)
if (response) {
await fetchData();
}
}, [jobOrderId]);
const handleOpenPriorityDialog = () => {
setOperationPriority(processData?.productionPriority ?? 50);
setOpenOperationPriorityDialog(true);
};

const handleClosePriorityDialog = (_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => {
setOpenOperationPriorityDialog(false);
};
const handleConfirmPriority = async () => {
if (!processData?.id) return;
await handleUpdateOperationPriority(processData.id, Number(operationPriority));
setOpenOperationPriorityDialog(false);
};
const isStockSufficient = (line: JobOrderLine) => {
if (line.type?.toLowerCase() === "consumables") {
return false;
@@ -248,12 +274,21 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Production Priority")}
fullWidth
disabled={true}
value={processData?.productionPriority ||processData?.isDense === 0 ? "50" : processData?.productionPriority || "0"}
/>
<TextField
label={t("Production Priority")}
fullWidth
disabled={true}
value={processData?.productionPriority ?? "50"}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton size="small" onClick={handleOpenPriorityDialog}>
<EditIcon fontSize="small" />
</IconButton>
</InputAdornment>
),
}}
/>
</Grid>
<Grid item xs={6}>
<TextField
@@ -334,9 +369,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
align: "right",
headerAlign: "right",
renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb" ) {
return t("N/A");
}
return `${decimalFormatter.format(params.value)} (${params.row.shortUom})`;
},
@@ -350,14 +383,10 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
type: "number",
renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
// 如果是 consumables,显示 N/A
if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
return t("N/A");
}
const stockAvailable = getStockAvailable(params.row);
if (stockAvailable === null) {
return t("N/A");
}
return `${decimalFormatter.format(stockAvailable)} (${params.row.shortUom})`;
return `${decimalFormatter.format(stockAvailable || 0)} (${params.row.shortUom})`;
},
},
{
@@ -386,9 +415,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
headerAlign: "center",
type: "boolean",
renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
return <Typography>{t("N/A")}</Typography>;
}
return isStockSufficient(params.row)
? <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" />
: <DoDisturbAltRoundedIcon fontSize={"large"} color="error" />;
@@ -520,11 +547,36 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
{tabIndex === 4 && <JobPickExecutionsecondscan filterArgs={{ jobOrderId: jobOrderId }} />}
<Dialog
open={openOperationPriorityDialog}
onClose={handleClosePriorityDialog}
fullWidth
maxWidth="xs"
>
<DialogTitle>{t("Update Production Priority")}</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label={t("Production Priority")}
type="number"
fullWidth
value={operationPriority}
onChange={(e) => setOperationPriority(Number(e.target.value))}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClosePriorityDialog}>{t("Cancel")}</Button>
<Button variant="contained" onClick={handleConfirmPriority}>{t("Save")}</Button>
</DialogActions>
</Dialog>


</Box>
</Box>
);
};

export default ProductionProcessJobOrderDetail;

+ 1
- 0
src/i18n/zh/common.json View File

@@ -195,6 +195,7 @@
"Remark": "明細",
"Req. Qty": "需求數量",
"Seq No": "加入步驟",
"Total pick orders": "總提料單數量",
"Seq No Remark": "序號明細",
"Stock Available": "庫存可用",
"Confirm": "確認",


+ 1
- 0
src/i18n/zh/jo.json View File

@@ -43,6 +43,7 @@
"Item Code": "成品/半成品編號",
"Paused": "已暫停",
"paused": "已暫停",
"Total pick orders": "總提料單數量",
"Pause Reason": "暫停原因",
"Reason": "原因",
"Stock Available": "倉庫可用數",


Loading…
Cancel
Save