diff --git a/src/app/(main)/jodetail/edit/not-found.tsx b/src/app/(main)/jodetail/edit/not-found.tsx
new file mode 100644
index 0000000..6561158
--- /dev/null
+++ b/src/app/(main)/jodetail/edit/not-found.tsx
@@ -0,0 +1,19 @@
+import { getServerI18n } from "@/i18n";
+import { Stack, Typography, Link } from "@mui/material";
+import NextLink from "next/link";
+
+export default async function NotFound() {
+ const { t } = await getServerI18n("schedule", "common");
+
+ return (
+
+ {t("Not Found")}
+
+ {t("The job order page was not found!")}
+
+
+ {t("Return to all job orders")}
+
+
+ );
+}
diff --git a/src/app/(main)/jodetail/edit/page.tsx b/src/app/(main)/jodetail/edit/page.tsx
new file mode 100644
index 0000000..5172798
--- /dev/null
+++ b/src/app/(main)/jodetail/edit/page.tsx
@@ -0,0 +1,49 @@
+import { fetchJoDetail } from "@/app/api/jo";
+import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil";
+import JoSave from "@/components/JoSave/JoSave";
+import { I18nProvider, getServerI18n } from "@/i18n";
+import { Typography } from "@mui/material";
+import { isArray } from "lodash";
+import { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { Suspense } from "react";
+import GeneralLoading from "@/components/General/GeneralLoading";
+
+export const metadata: Metadata = {
+ title: "Edit Job Order Detail"
+}
+
+type Props = SearchParams;
+
+const JoEdit: React.FC = async ({ searchParams }) => {
+ const { t } = await getServerI18n("jo");
+ const id = searchParams["id"];
+
+ if (!id || isArray(id) || !isFinite(parseInt(id))) {
+ notFound();
+ }
+
+ try {
+ await fetchJoDetail(parseInt(id))
+ } catch (e) {
+ if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) {
+ console.log(e)
+ notFound();
+ }
+ }
+
+ return (
+ <>
+
+ {t("Edit Job Order Detail")}
+
+
+ }>
+
+
+
+ >
+ );
+}
+
+export default JoEdit;
\ No newline at end of file
diff --git a/src/app/(main)/jodetail/page.tsx b/src/app/(main)/jodetail/page.tsx
new file mode 100644
index 0000000..3f5b222
--- /dev/null
+++ b/src/app/(main)/jodetail/page.tsx
@@ -0,0 +1,39 @@
+import { preloadBomCombo } from "@/app/api/bom";
+import JodetailSearch from "@/components/Jodetail/JodetailSearch";
+import { I18nProvider, getServerI18n } from "@/i18n";
+import { Stack, Typography } from "@mui/material";
+import { Metadata } from "next";
+import React, { Suspense } from "react";
+import GeneralLoading from "@/components/General/GeneralLoading";
+
+export const metadata: Metadata = {
+ title: "Job Order Pickexcution"
+}
+
+const jo: React.FC = async () => {
+ const { t } = await getServerI18n("jo");
+
+ preloadBomCombo()
+
+ return (
+ <>
+
+
+ {t("Job Order Pickexcution")}
+
+
+
+ }>
+
+
+
+ >
+ )
+}
+
+export default jo;
\ No newline at end of file
diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts
index 135f2d9..217bbc0 100644
--- a/src/app/api/jo/actions.ts
+++ b/src/app/api/jo/actions.ts
@@ -38,15 +38,17 @@ export interface SearchJoResult {
status: JoStatus;
}
-export interface ReleaseJoRequest {
+// For Jo Button Actions
+export interface CommonActionJoRequest {
id: number;
}
-export interface ReleaseJoResponse {
+export interface CommonActionJoResponse {
id: number;
entity: { status: JoStatus }
}
+// For Jo Process
export interface IsOperatorExistResponse {
id: number | null;
name: string;
@@ -76,7 +78,129 @@ export interface JobOrderDetail {
pickLines: any[];
status: string;
}
+export interface UnassignedJobOrderPickOrder {
+ pickOrderId: number;
+ pickOrderCode: string;
+ pickOrderConsoCode: string;
+ pickOrderTargetDate: string;
+ pickOrderStatus: string;
+ jobOrderId: number;
+ jobOrderCode: string;
+ jobOrderName: string;
+ reqQty: number;
+ uom: string;
+ planStart: string;
+ planEnd: string;
+}
+
+export interface AssignJobOrderResponse {
+ id: number | null;
+ code: string | null;
+ name: string | null;
+ type: string | null;
+ message: string | null;
+ errorPosition: string | null;
+}
+export const recordSecondScanIssue = cache(async (
+ pickOrderId: number,
+ itemId: number,
+ data: {
+ qty: number;
+ isMissing: boolean;
+ isBad: boolean;
+ reason: string;
+ createdBy: number;
+ }
+) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/jo/second-scan-issue/${pickOrderId}/${itemId}`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ next: { tags: ["jo-second-scan"] },
+ },
+ );
+});
+export const updateSecondQrScanStatus = cache(async (pickOrderId: number, itemId: number) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/jo/second-scan-qr/${pickOrderId}/${itemId}`,
+ {
+ method: "POST",
+ next: { tags: ["jo-second-scan"] },
+ },
+ );
+});
+
+export const submitSecondScanQuantity = cache(async (
+ pickOrderId: number,
+ itemId: number,
+ data: { qty: number; isMissing?: boolean; isBad?: boolean; reason?: string }
+) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/jo/second-scan-submit/${pickOrderId}/${itemId}`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(data),
+ next: { tags: ["jo-second-scan"] },
+ },
+ );
+});
+// 获取未分配的 Job Order pick orders
+export const fetchUnassignedJobOrderPickOrders = cache(async () => {
+ return serverFetchJson(
+ `${BASE_API_URL}/jo/unassigned-job-order-pick-orders`,
+ {
+ method: "GET",
+ next: { tags: ["jo-unassigned"] },
+ },
+ );
+});
+
+// 分配 Job Order pick order 给用户
+export const assignJobOrderPickOrder = async (pickOrderId: number, userId: number) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/jo/assign-job-order-pick-order/${pickOrderId}/${userId}`,
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ }
+ );
+};
+
+// 获取 Job Order 分层数据
+export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`,
+ {
+ method: "GET",
+ next: { tags: ["jo-hierarchical"] },
+ },
+ );
+});
+// 获取已完成的 Job Order pick orders
+export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/jo/completed-job-order-pick-orders/${userId}`,
+ {
+ method: "GET",
+ next: { tags: ["jo-completed"] },
+ },
+ );
+});
+
+// 获取已完成的 Job Order pick order records
+export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => {
+ return serverFetchJson(
+ `${BASE_API_URL}/jo/completed-job-order-pick-order-records/${userId}`,
+ {
+ method: "GET",
+ next: { tags: ["jo-records"] },
+ },
+ );
+});
export const fetchJobOrderDetailByCode = cache(async (code: string) => {
return serverFetchJson(
`${BASE_API_URL}/jo/detailByCode/${code}`,
@@ -129,8 +253,17 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => {
return response
})
-export const releaseJo = cache(async (data: ReleaseJoRequest) => {
- return serverFetchJson(`${BASE_API_URL}/jo/release`,
+export const releaseJo = cache(async (data: CommonActionJoRequest) => {
+ return serverFetchJson(`${BASE_API_URL}/jo/release`,
+ {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ })
+})
+
+export const startJo = cache(async (data: CommonActionJoRequest) => {
+ return serverFetchJson(`${BASE_API_URL}/jo/start`,
{
method: "POST",
body: JSON.stringify(data),
@@ -139,10 +272,22 @@ export const releaseJo = cache(async (data: ReleaseJoRequest) => {
})
export const manualCreateJo = cache(async (data: SaveJo) => {
- console.log(data)
return serverFetchJson(`${BASE_API_URL}/jo/manualCreate`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" }
})
+})
+
+export const fetchCompletedJobOrderPickOrdersWithCompletedSecondScan = cache(async (userId: number) => {
+ return serverFetchJson(`${BASE_API_URL}/jo/completed-job-order-pick-orders-with-completed-second-scan/${userId}`, {
+ method: "GET",
+ headers: { "Content-Type": "application/json" }
+ })
+})
+export const fetchCompletedJobOrderPickOrderLotDetails = cache(async (pickOrderId: number) => {
+ return serverFetchJson(`${BASE_API_URL}/jo/completed-job-order-pick-order-lot-details/${pickOrderId}`, {
+ method: "GET",
+ headers: { "Content-Type": "application/json" }
+ })
})
\ No newline at end of file
diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
index 038a907..2e283cd 100644
--- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
+++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
@@ -690,8 +690,8 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => {
}}>
-
-
+
+
diff --git a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
index b7fe86d..89d50b6 100644
--- a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
+++ b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
@@ -20,7 +20,7 @@ import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions";
import { fetchEscalationCombo } from "@/app/api/user/actions";
-
+import { useRef } from "react";
interface LotPickData {
id: number;
lotId: number;
@@ -81,7 +81,6 @@ const PickExecutionForm: React.FC = ({
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
const [handlers, setHandlers] = useState>([]);
-
// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
const remainingQty = lot.inQty - lot.outQty;
@@ -92,7 +91,18 @@ const PickExecutionForm: React.FC = ({
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
-
+ const remaining = selectedLot ? calculateRemainingAvailableQty(selectedLot) : 0;
+ const req = selectedLot ? calculateRequiredQty(selectedLot) : 0;
+
+ const ap = Number(formData.actualPickQty) || 0;
+ const miss = Number(formData.missQty) || 0;
+ const bad = Number(formData.badItemQty) || 0;
+
+ // Max the user can type
+ const maxPick = Math.min(remaining, req);
+ const maxIssueTotal = Math.max(0, req - ap); // remaining room for miss+bad
+
+ const clamp0 = (v: any) => Math.max(0, Number(v) || 0);
// 获取处理人员列表
useEffect(() => {
const fetchHandlers = async () => {
@@ -107,55 +117,49 @@ const PickExecutionForm: React.FC = ({
fetchHandlers();
}, []);
- // 初始化表单数据 - 每次打开时都重新初始化
+ const initKeyRef = useRef(null);
+
useEffect(() => {
- if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
- const getSafeDate = (dateValue: any): string => {
- if (!dateValue) return new Date().toISOString().split('T')[0];
- try {
- const date = new Date(dateValue);
- if (isNaN(date.getTime())) {
- return new Date().toISOString().split('T')[0];
- }
- return date.toISOString().split('T')[0];
- } catch {
- return new Date().toISOString().split('T')[0];
- }
- };
+ if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return;
- // 计算剩余可用数量
- const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
- const requiredQty = calculateRequiredQty(selectedLot);
- console.log("=== PickExecutionForm Debug ===");
- console.log("selectedLot:", selectedLot);
- console.log("inQty:", selectedLot.inQty);
- console.log("outQty:", selectedLot.outQty);
- console.log("holdQty:", selectedLot.holdQty);
- console.log("availableQty:", selectedLot.availableQty);
- console.log("calculated remainingAvailableQty:", remainingAvailableQty);
- console.log("=== End Debug ===");
- setFormData({
- pickOrderId: pickOrderId,
- pickOrderCode: selectedPickOrderLine.pickOrderCode,
- pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
- pickExecutionDate: new Date().toISOString().split('T')[0],
- pickOrderLineId: selectedPickOrderLine.id,
- itemId: selectedPickOrderLine.itemId,
- itemCode: selectedPickOrderLine.itemCode,
- itemDescription: selectedPickOrderLine.itemName,
- lotId: selectedLot.lotId,
- lotNo: selectedLot.lotNo,
- storeLocation: selectedLot.location,
- requiredQty: selectedLot.requiredQty,
- actualPickQty: selectedLot.actualPickQty || 0,
- missQty: 0,
- badItemQty: 0, // 初始化为 0,用户需要手动输入
- issueRemark: '',
- pickerName: '',
- handledBy: undefined,
- });
- }
- }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]);
+ // Only initialize once per (pickOrderLineId + lotId) while dialog open
+ const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`;
+ if (initKeyRef.current === key) return;
+
+ const getSafeDate = (dateValue: any): string => {
+ if (!dateValue) return new Date().toISOString().split('T')[0];
+ try {
+ const d = new Date(dateValue);
+ return isNaN(d.getTime()) ? new Date().toISOString().split('T')[0] : d.toISOString().split('T')[0];
+ } catch {
+ return new Date().toISOString().split('T')[0];
+ }
+ };
+
+ setFormData({
+ pickOrderId: pickOrderId,
+ pickOrderCode: selectedPickOrderLine.pickOrderCode,
+ pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
+ pickExecutionDate: new Date().toISOString().split('T')[0],
+ pickOrderLineId: selectedPickOrderLine.id,
+ itemId: selectedPickOrderLine.itemId,
+ itemCode: selectedPickOrderLine.itemCode,
+ itemDescription: selectedPickOrderLine.itemName,
+ lotId: selectedLot.lotId,
+ lotNo: selectedLot.lotNo,
+ storeLocation: selectedLot.location,
+ requiredQty: selectedLot.requiredQty,
+ actualPickQty: selectedLot.actualPickQty || 0,
+ missQty: 0,
+ badItemQty: 0,
+ issueRemark: '',
+ pickerName: '',
+ handledBy: undefined,
+ });
+
+ initKeyRef.current = key;
+ }, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]);
+ // Mutually exclusive inputs: picking vs reporting issues
const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
@@ -168,30 +172,23 @@ const PickExecutionForm: React.FC = ({
// ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
-
- if (formData.actualPickQty === undefined || formData.actualPickQty < 0) {
- newErrors.actualPickQty = t('Qty is required');
- }
-
- // ✅ FIXED: Check if actual pick qty exceeds remaining available qty
- if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) {
- newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty');
- }
-
- // ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty)
- if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) {
- newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
+ const req = selectedLot?.requiredQty || 0;
+ const ap = formData.actualPickQty || 0;
+ const miss = formData.missQty || 0;
+ const bad = formData.badItemQty || 0;
+
+ if (ap < 0) newErrors.actualPickQty = t('Qty is required');
+ if (ap > Math.min(remainingAvailableQty, req)) newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty');
+ if (miss < 0) newErrors.missQty = t('Invalid qty');
+ if (bad < 0) newErrors.badItemQty = t('Invalid qty');
+ if (ap + miss + bad > req) {
+ newErrors.actualPickQty = t('Total exceeds required qty');
+ newErrors.missQty = t('Total exceeds required qty');
}
-
- // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
- 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');
+ if (ap === 0 && miss === 0 && bad === 0) {
+ newErrors.actualPickQty = t('Enter pick qty or issue qty');
+ newErrors.missQty = t('Enter pick qty or issue qty');
}
-
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
@@ -266,42 +263,42 @@ const PickExecutionForm: React.FC = ({
- handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)}
- error={!!errors.actualPickQty}
- helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
- variant="outlined"
- />
+ handleInputChange('actualPickQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
+ error={!!errors.actualPickQty}
+ helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`}
+ variant="outlined"
+ />
- handleInputChange('missQty', parseFloat(e.target.value) || 0)}
- error={!!errors.missQty}
- // helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')}
- variant="outlined"
- />
+ handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
+ error={!!errors.missQty}
+ variant="outlined"
+ //disabled={(formData.actualPickQty || 0) > 0}
+ />
- handleInputChange('badItemQty', parseFloat(e.target.value) || 0)}
- error={!!errors.badItemQty}
- // helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')}
- variant="outlined"
- />
+ handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))}
+ error={!!errors.badItemQty}
+ variant="outlined"
+ //disabled={(formData.actualPickQty || 0) > 0}
+ />
{/* ✅ Show issue description and handler fields when bad items > 0 */}
diff --git a/src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx b/src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
index 2bde71e..9a5ae19 100644
--- a/src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
+++ b/src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
@@ -118,7 +118,7 @@ const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => {
// ✅ 新增:搜索状态
const [searchQuery, setSearchQuery] = useState>({});
const [filteredDoPickOrders, setFilteredDoPickOrders] = useState([]);
-
+
// ✅ 新增:分页状态
const [paginationController, setPaginationController] = useState({
pageNum: 0,
@@ -358,10 +358,10 @@ const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => {
{/* 加载状态 */}
{completedDoPickOrdersLoading ? (
-
-
-
- ) : (
+
+
+
+ ) : (
{/* 结果统计 */}
@@ -370,12 +370,12 @@ const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => {
{/* 列表 */}
{filteredDoPickOrders.length === 0 ? (
-
-
+
+
{t("No completed DO pick orders found")}
-
-
- ) : (
+
+
+ ) : (
{paginatedData.map((doPickOrder) => (
@@ -429,10 +429,10 @@ const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => {
onRowsPerPageChange={handlePageSizeChange}
rowsPerPageOptions={[5, 10, 25, 50]}
/>
- )}
-
- )}
-
+ )}
+
+ )}
+
);
};
diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
index 1af86fc..b3a8912 100644
--- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
+++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
@@ -586,12 +586,20 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
setQrScanError(false);
setQrScanSuccess(false);
setQrScanInput('');
- setIsManualScanning(false);
- stopScan();
- resetScan();
+ //setIsManualScanning(false);
+ //stopScan();
+ //resetScan();
setProcessedQrCodes(new Set());
setLastProcessedQr('');
-
+ setQrModalOpen(false);
+ setPickExecutionFormOpen(false);
+ if(selectedLotForQr?.stockOutLineId){
+ const stockOutLineUpdate = await updateStockOutLineStatus({
+ id: selectedLotForQr.stockOutLineId,
+ status: 'checked',
+ qty: 0
+ });
+ }
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
@@ -709,9 +717,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
setQrScanSuccess(true);
setQrScanError(false);
setQrScanInput(''); // Clear input after successful processing
- setIsManualScanning(false);
- stopScan();
- resetScan();
+ //setIsManualScanning(false);
+ // stopScan();
+ // resetScan();
// ✅ Clear success state after a delay
//setTimeout(() => {
diff --git a/src/components/JoSave/ActionButtons.tsx b/src/components/JoSave/ActionButtons.tsx
index 480576d..12e0892 100644
--- a/src/components/JoSave/ActionButtons.tsx
+++ b/src/components/JoSave/ActionButtons.tsx
@@ -14,6 +14,7 @@ type Props = {
interface ErrorEntry {
qtyErr: boolean;
scanErr: boolean;
+ pickErr: boolean;
}
const ActionButtons: React.FC = ({
@@ -36,24 +37,31 @@ const ActionButtons: React.FC = ({
const errors: ErrorEntry = useMemo(() => {
let qtyErr = false;
let scanErr = false;
+ let pickErr = false
pickLines.forEach((line) => {
if (!qtyErr) {
const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0
- qtyErr = pickedQty > 0 && pickedQty >= line.reqQty
+ qtyErr = pickedQty <= 0 || pickedQty < line.reqQty
}
if (!scanErr) {
scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false
}
+
+ if (!pickErr) {
+ pickErr = line.pickedLotNo === null
+ }
})
return {
qtyErr: qtyErr,
- scanErr: scanErr
+ scanErr: scanErr,
+ pickErr: pickErr
}
- }, [pickLines])
+ }, [pickLines, status])
+ console.log(pickLines)
return (
{status === "planning" && (
@@ -71,12 +79,13 @@ const ActionButtons: React.FC = ({
variant="outlined"
startIcon={}
onClick={handleStart}
- disabled={errors.qtyErr || errors.scanErr}
+ disabled={errors.qtyErr || errors.scanErr || errors.pickErr}
>
{t("Start Job Order")}
- {errors.scanErr && ({t("Please scan the item qr code.")})}
- {errors.qtyErr && ({t("Please make sure the qty is enough.")})}
+ {errors.pickErr && ({t("Please make sure all required items are picked")})}
+ {errors.scanErr && ({t("Please scan the item qr code")})}
+ {errors.qtyErr && ({t("Please make sure the qty is enough")})}
)}
diff --git a/src/components/JoSave/JoSave.tsx b/src/components/JoSave/JoSave.tsx
index 0c3b24e..1c0546b 100644
--- a/src/components/JoSave/JoSave.tsx
+++ b/src/components/JoSave/JoSave.tsx
@@ -8,13 +8,17 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } fro
import { Button, Stack, Typography } from "@mui/material";
import StartIcon from "@mui/icons-material/Start";
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
-import { releaseJo } from "@/app/api/jo/actions";
+import { releaseJo, startJo } from "@/app/api/jo/actions";
import InfoCard from "./InfoCard";
import PickTable from "./PickTable";
import ActionButtons from "./ActionButtons";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
import { fetchStockInLineInfo } from "@/app/api/po/actions";
+<<<<<<< HEAD
import JoRelease from "./JoRelease";
+=======
+import { submitDialog } from "../Swal/CustomAlerts";
+>>>>>>> 5ef2a717b8e76f98fdf437b56fa641e990ef106b
type Props = {
id?: number;
@@ -94,12 +98,18 @@ const JoSave: React.FC = ({
shouldValidate: true,
shouldDirty: true,
});
+
+ // Ask user and confirm to start JO
+ await submitDialog(() => handleStart(), t, {
+ title: t("Do you want to start job order"),
+ confirmButtonText: t("Start Job Order")
+ })
}
}
}
} finally {
- scanner.resetScan()
- setIsUploading(false)
+ scanner.resetScan()
+ setIsUploading(false)
}
}, [])
@@ -127,7 +137,6 @@ const JoSave: React.FC = ({
formProps.setValue("status", response.entity.status)
}
}
-
} catch (e) {
// backend error
setServerError(t("An error has occurred. Please try again later."));
@@ -138,7 +147,25 @@ const JoSave: React.FC = ({
}, [])
const handleStart = useCallback(async () => {
- console.log("first")
+ try {
+ setIsUploading(true)
+ if (id) {
+ const response = await startJo({ id: id })
+ if (response) {
+ formProps.setValue("status", response.entity.status)
+
+ pickLines.map((line) => ({...line, status: "completed"}))
+
+ handleBack()
+ }
+ }
+ } catch (e) {
+ // backend error
+ setServerError(t("An error has occurred. Please try again later."));
+ console.log(e);
+ } finally {
+ setIsUploading(false)
+ }
}, [])
// --------------------------------------------- Form Submit --------------------------------------------- //
diff --git a/src/components/JoSave/PickTable.tsx b/src/components/JoSave/PickTable.tsx
index 50a6a30..e226a52 100644
--- a/src/components/JoSave/PickTable.tsx
+++ b/src/components/JoSave/PickTable.tsx
@@ -141,8 +141,8 @@ const PickTable: React.FC = ({
if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) {
return notPickedStatusColumn
}
- const scanStatus = params.row.pickedLotNo.map((pln) => Boolean(pln.isScanned))
- return isEmpty(scanStatus) ? notPickedStatusColumn : {scanStatus.map((status) => scanStatusColumn(status))}
+ const scanStatus = params.row.pickedLotNo.map((pln) => params.row.status === "completed" ? true : Boolean(pln.isScanned))
+ return isEmpty(scanStatus) ? notPickedStatusColumn : {scanStatus.map((status) => scanStatusColumn(status))}
},
},
{
@@ -213,10 +213,11 @@ const PickTable: React.FC = ({
align: "right",
headerAlign: "right",
renderCell: (params: GridRenderCellParams) => {
+ const status = Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))) || params.row.status === "completed"
return (
<>
{params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))}
- {scanStatusColumn(Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))))}
+ {scanStatusColumn(status)}
>
)
},
diff --git a/src/components/Jodetail/CombinedLotTable.tsx b/src/components/Jodetail/CombinedLotTable.tsx
new file mode 100644
index 0000000..8b99721
--- /dev/null
+++ b/src/components/Jodetail/CombinedLotTable.tsx
@@ -0,0 +1,231 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ CircularProgress,
+ Paper,
+ Stack,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ TextField,
+ Typography,
+ TablePagination,
+} from "@mui/material";
+import { useCallback, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+
+interface CombinedLotTableProps {
+ combinedLotData: any[];
+ combinedDataLoading: boolean;
+ pickQtyData: Record;
+ paginationController: {
+ pageNum: number;
+ pageSize: number;
+ };
+ onPickQtyChange: (lotKey: string, value: number | string) => void;
+ onSubmitPickQty: (lot: any) => void;
+ onRejectLot: (lot: any) => void;
+ onPageChange: (event: unknown, newPage: number) => void;
+ onPageSizeChange: (event: React.ChangeEvent) => void;
+}
+
+// ✅ Simple helper function to check if item is completed
+const isItemCompleted = (lot: any) => {
+ const actualPickQty = Number(lot.actualPickQty) || 0;
+ const requiredQty = Number(lot.requiredQty) || 0;
+
+ return lot.stockOutLineStatus === 'completed' ||
+ (actualPickQty > 0 && requiredQty > 0 && actualPickQty >= requiredQty);
+};
+
+const isItemRejected = (lot: any) => {
+ return lot.stockOutLineStatus === 'rejected';
+};
+
+const CombinedLotTable: React.FC = ({
+ combinedLotData,
+ combinedDataLoading,
+ pickQtyData,
+ paginationController,
+ onPickQtyChange,
+ onSubmitPickQty,
+ onRejectLot,
+ onPageChange,
+ onPageSizeChange,
+}) => {
+ const { t } = useTranslation("pickOrder");
+
+ // ✅ Paginated data
+ const paginatedLotData = useMemo(() => {
+ const startIndex = paginationController.pageNum * paginationController.pageSize;
+ const endIndex = startIndex + paginationController.pageSize;
+ return combinedLotData.slice(startIndex, endIndex);
+ }, [combinedLotData, paginationController]);
+
+ if (combinedDataLoading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+ {t("Pick Order Code")}
+ {t("Item Code")}
+ {t("Item Name")}
+ {t("Lot No")}
+ {/* {t("Expiry Date")} */}
+ {t("Location")}
+
+ {t("Current Stock")}
+ {t("Lot Required Pick Qty")}
+ {t("Qty Already Picked")}
+ {t("Lot Actual Pick Qty")}
+ {t("Stock Unit")}
+ {t("Submit")}
+ {t("Reject")}
+
+
+
+ {paginatedLotData.length === 0 ? (
+
+
+
+ {t("No data available")}
+
+
+
+ ) : (
+ paginatedLotData.map((lot: any) => {
+ const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
+ const currentPickQty = pickQtyData[lotKey] ?? '';
+ const isCompleted = isItemCompleted(lot);
+ const isRejected = isItemRejected(lot);
+
+ // ✅ Green text color for completed items
+ const textColor = isCompleted ? 'success.main' : isRejected ? 'error.main' : 'inherit';
+
+ return (
+
+ {lot.pickOrderCode}
+ {lot.itemCode}
+ {lot.itemName}
+ {lot.lotNo}
+ {/*
+ {lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A'}
+
+ */}
+ {lot.location}
+
+ {lot.availableQty}
+ {lot.requiredQty}
+ {lot.actualPickQty || 0}
+
+ {
+ onPickQtyChange(lotKey, e.target.value);
+ }}
+ onFocus={(e) => {
+ e.target.select();
+ }}
+ inputProps={{
+ min: 0,
+ max: lot.availableQty,
+ step: 0.01
+ }}
+ disabled={
+ isCompleted ||
+ isRejected ||
+ lot.lotAvailability === 'expired' ||
+ lot.lotAvailability === 'status_unavailable'
+ }
+ sx={{
+ width: '80px',
+ '& .MuiInputBase-input': {
+ textAlign: 'right',
+ }
+ }}
+ />
+
+ {lot.stockUnit}
+
+
+
+
+
+
+
+ );
+ })
+ )}
+
+
+
+
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+ >
+ );
+};
+
+export default CombinedLotTable;
\ No newline at end of file
diff --git a/src/components/Jodetail/CreateForm.tsx b/src/components/Jodetail/CreateForm.tsx
new file mode 100644
index 0000000..45e7514
--- /dev/null
+++ b/src/components/Jodetail/CreateForm.tsx
@@ -0,0 +1,321 @@
+"use client";
+
+import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions";
+import {
+ Autocomplete,
+ Box,
+ Card,
+ CardContent,
+ FormControl,
+ Grid,
+ Stack,
+ TextField,
+ Tooltip,
+ Typography,
+} from "@mui/material";
+import { Controller, useFormContext } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import StyledDataGrid from "../StyledDataGrid";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ GridColDef,
+ GridRowIdGetter,
+ GridRowModel,
+ useGridApiContext,
+ GridRenderCellParams,
+ GridRenderEditCellParams,
+ useGridApiRef,
+} from "@mui/x-data-grid";
+import InputDataGrid from "../InputDataGrid";
+import { TableRow } from "../InputDataGrid/InputDataGrid";
+import { GridEditInputCell } from "@mui/x-data-grid";
+import { StockInLine } from "@/app/api/po";
+import { INPUT_DATE_FORMAT, stockInLineStatusMap } from "@/app/utils/formatUtil";
+import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions";
+import { QcItemWithChecks } from "@/app/api/qc";
+import axios from "@/app/(main)/axios/axiosInstance";
+import { NEXT_PUBLIC_API_URL } from "@/config/api";
+import axiosInstance from "@/app/(main)/axios/axiosInstance";
+import { SavePickOrderLineRequest, SavePickOrderRequest } from "@/app/api/pickOrder/actions";
+import TwoLineCell from "../PoDetail/TwoLineCell";
+import ItemSelect from "./ItemSelect";
+import { ItemCombo } from "@/app/api/settings/item/actions";
+import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
+import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
+import dayjs from "dayjs";
+
+interface Props {
+ items: ItemCombo[];
+// disabled: boolean;
+}
+type EntryError =
+ | {
+ [field in keyof SavePickOrderLineRequest]?: string;
+ }
+ | undefined;
+
+type PolRow = TableRow, EntryError>;
+// fetchQcItemCheck
+const CreateForm: React.FC = ({ items }) => {
+ const {
+ t,
+ i18n: { language },
+ } = useTranslation("pickOrder");
+ const apiRef = useGridApiRef();
+ const {
+ formState: { errors, defaultValues, touchedFields },
+ watch,
+ control,
+ setValue,
+ } = useFormContext();
+ console.log(defaultValues);
+ const targetDate = watch("targetDate");
+
+//// validate form
+// const accQty = watch("acceptedQty");
+// const validateForm = useCallback(() => {
+// console.log(accQty);
+// if (accQty > itemDetail.acceptedQty) {
+// setError("acceptedQty", {
+// message: `${t("acceptedQty must not greater than")} ${
+// itemDetail.acceptedQty
+// }`,
+// type: "required",
+// });
+// }
+// if (accQty < 1) {
+// setError("acceptedQty", {
+// message: t("minimal value is 1"),
+// type: "required",
+// });
+// }
+// if (isNaN(accQty)) {
+// setError("acceptedQty", {
+// message: t("value must be a number"),
+// type: "required",
+// });
+// }
+// }, [accQty]);
+
+// useEffect(() => {
+// clearErrors();
+// validateForm();
+// }, [clearErrors, validateForm]);
+
+ const columns = useMemo(
+ () => [
+ {
+ field: "itemId",
+ headerName: t("Item"),
+ // width: 100,
+ flex: 1,
+ editable: true,
+ valueFormatter(params) {
+ const row = params.id ? params.api.getRow(params.id) : null;
+ if (!row) {
+ return null;
+ }
+ const Item = items.find((q) => q.id === row.itemId);
+ return Item ? Item.label : t("Please select item");
+ },
+ renderCell(params: GridRenderCellParams) {
+ console.log(params.value);
+ return {params.formattedValue};
+ },
+ renderEditCell(params: GridRenderEditCellParams) {
+ const errorMessage =
+ params.row._error?.[params.field as keyof SavePickOrderLineRequest];
+ console.log(errorMessage);
+ const content = (
+ // <>>
+ {
+ console.log(uom)
+ await params.api.setEditCellValue({
+ id: params.id,
+ field: "itemId",
+ value: itemId,
+ });
+ await params.api.setEditCellValue({
+ id: params.id,
+ field: "uom",
+ value: uom
+ })
+ await params.api.setEditCellValue({
+ id: params.id,
+ field: "uomId",
+ value: uomId
+ })
+ }}
+ />
+ );
+ return errorMessage ? (
+
+ {content}
+
+ ) : (
+ content
+ );
+ },
+ },
+ {
+ field: "qty",
+ headerName: t("qty"),
+ // width: 100,
+ flex: 1,
+ type: "number",
+ editable: true,
+ renderEditCell(params: GridRenderEditCellParams) {
+ const errorMessage =
+ params.row._error?.[params.field as keyof SavePickOrderLineRequest];
+ const content = ;
+ return errorMessage ? (
+
+ {content}
+
+ ) : (
+ content
+ );
+ },
+ },
+ {
+ field: "uom",
+ headerName: t("uom"),
+ // width: 100,
+ flex: 1,
+ editable: true,
+ // renderEditCell(params: GridRenderEditCellParams) {
+ // console.log(params.row)
+ // const errorMessage =
+ // params.row._error?.[params.field as keyof SavePickOrderLineRequest];
+ // const content = ;
+ // return errorMessage ? (
+ //
+ // {content}
+ //
+ // ) : (
+ // content
+ // );
+ // }
+ }
+ ],
+ [items, t],
+ );
+ /// validate datagrid
+ const validation = useCallback(
+ (newRow: GridRowModel): EntryError => {
+ const error: EntryError = {};
+ const { itemId, qty } = newRow;
+ if (!itemId || itemId <= 0) {
+ error["itemId"] = t("select qc");
+ }
+ if (!qty || qty <= 0) {
+ error["qty"] = t("enter a qty");
+ }
+ return Object.keys(error).length > 0 ? error : undefined;
+ },
+ [],
+ );
+
+ const typeList = [
+ {
+ type: "Consumable"
+ }
+ ]
+
+ const onChange = useCallback(
+ (event: React.SyntheticEvent, newValue: {type: string}) => {
+ console.log(newValue);
+ setValue("type", newValue.type);
+ },
+ [setValue],
+ );
+
+ return (
+
+
+
+ {t("Pick Order Detail")}
+
+
+
+
+
+ option.type}
+ options={typeList}
+ onChange={onChange}
+ renderInput={(params) => }
+ />
+
+
+
+ {
+ return (
+
+ {
+ console.log(date);
+ if (!date) return;
+ console.log(date.format(INPUT_DATE_FORMAT));
+ setValue("targetDate", date.format(INPUT_DATE_FORMAT));
+ // field.onChange(date);
+ }}
+ inputRef={field.ref}
+ slotProps={{
+ textField: {
+ // required: true,
+ error: Boolean(errors.targetDate?.message),
+ helperText: errors.targetDate?.message,
+ },
+ }}
+ />
+
+ );
+ }}
+ />
+
+
+
+
+
+ apiRef={apiRef}
+ checkboxSelection={false}
+ _formKey={"pickOrderLine"}
+ columns={columns}
+ validateRow={validation}
+ needAdd={true}
+ />
+
+
+
+ );
+};
+export default CreateForm;
diff --git a/src/components/Jodetail/CreatedItemsTable.tsx b/src/components/Jodetail/CreatedItemsTable.tsx
new file mode 100644
index 0000000..e60bf2f
--- /dev/null
+++ b/src/components/Jodetail/CreatedItemsTable.tsx
@@ -0,0 +1,209 @@
+import React, { useCallback } from 'react';
+import {
+ Box,
+ Typography,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Checkbox,
+ TextField,
+ TablePagination,
+ FormControl,
+ Select,
+ MenuItem,
+} from '@mui/material';
+import { useTranslation } from 'react-i18next';
+
+interface CreatedItem {
+ itemId: number;
+ itemName: string;
+ itemCode: string;
+ qty: number;
+ uom: string;
+ uomId: number;
+ uomDesc: string;
+ isSelected: boolean;
+ currentStockBalance?: number;
+ targetDate?: string | null;
+ groupId?: number | null;
+}
+
+interface Group {
+ id: number;
+ name: string;
+ targetDate: string;
+}
+
+interface CreatedItemsTableProps {
+ items: CreatedItem[];
+ groups: Group[];
+ onItemSelect: (itemId: number, checked: boolean) => void;
+ onQtyChange: (itemId: number, qty: number) => void;
+ onGroupChange: (itemId: number, groupId: string) => void;
+ pageNum: number;
+ pageSize: number;
+ onPageChange: (event: unknown, newPage: number) => void;
+ onPageSizeChange: (event: React.ChangeEvent) => void;
+}
+
+const CreatedItemsTable: React.FC = ({
+ items,
+ groups,
+ onItemSelect,
+ onQtyChange,
+ onGroupChange,
+ pageNum,
+ pageSize,
+ onPageChange,
+ onPageSizeChange,
+}) => {
+ const { t } = useTranslation("pickOrder");
+
+ // Calculate pagination
+ const startIndex = (pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedItems = items.slice(startIndex, endIndex);
+
+ const handleQtyChange = useCallback((itemId: number, value: string) => {
+ const numValue = Number(value);
+ if (!isNaN(numValue) && numValue >= 1) {
+ onQtyChange(itemId, numValue);
+ }
+ }, [onQtyChange]);
+
+ return (
+ <>
+
+
+
+
+
+ {t("Selected")}
+
+
+ {t("Item")}
+
+
+ {t("Group")}
+
+
+ {t("Current Stock")}
+
+
+ {t("Stock Unit")}
+
+
+ {t("Order Quantity")}
+
+
+ {t("Target Date")}
+
+
+
+
+ {paginatedItems.length === 0 ? (
+
+
+
+ {t("No created items")}
+
+
+
+ ) : (
+ paginatedItems.map((item) => (
+
+
+ onItemSelect(item.itemId, e.target.checked)}
+ />
+
+
+ {item.itemName}
+
+ {item.itemCode}
+
+
+
+
+
+
+
+
+ 0 ? "success.main" : "error.main"}
+ >
+ {item.currentStockBalance?.toLocaleString() || 0}
+
+
+
+ {item.uomDesc}
+
+
+ handleQtyChange(item.itemId, e.target.value)}
+ inputProps={{
+ min: 1,
+ step: 1,
+ style: { textAlign: 'center' }
+ }}
+ sx={{
+ width: '80px',
+ '& .MuiInputBase-input': {
+ textAlign: 'center',
+ cursor: 'text'
+ }
+ }}
+ />
+
+
+
+ {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
+
+
+
+ ))
+ )}
+
+
+
+
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+ >
+ );
+};
+
+export default CreatedItemsTable;
\ No newline at end of file
diff --git a/src/components/Jodetail/EscalationComponent.tsx b/src/components/Jodetail/EscalationComponent.tsx
new file mode 100644
index 0000000..53761a8
--- /dev/null
+++ b/src/components/Jodetail/EscalationComponent.tsx
@@ -0,0 +1,179 @@
+import React, { useState, ChangeEvent, FormEvent, Dispatch } from 'react';
+import {
+ Box,
+ Button,
+ Collapse,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ TextField,
+ Checkbox,
+ FormControlLabel,
+ Paper,
+ Typography,
+ RadioGroup,
+ Radio,
+ Stack,
+ Autocomplete,
+} from '@mui/material';
+import { SelectChangeEvent } from '@mui/material/Select';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import ExpandLessIcon from '@mui/icons-material/ExpandLess';
+import { useTranslation } from 'react-i18next';
+
+interface NameOption {
+ value: string;
+ label: string;
+}
+
+interface FormData {
+ name: string;
+ quantity: string;
+ message: string;
+}
+
+interface Props {
+ forSupervisor: boolean
+ isCollapsed: boolean
+ setIsCollapsed: Dispatch>
+}
+const EscalationComponent: React.FC = ({
+ forSupervisor,
+ isCollapsed,
+ setIsCollapsed
+ }) => {
+ const { t } = useTranslation("purchaseOrder");
+
+ const [formData, setFormData] = useState({
+ name: '',
+ quantity: '',
+ message: '',
+ });
+
+ const nameOptions: NameOption[] = [
+ { value: '', label: '請選擇姓名...' },
+ { value: 'john', label: '張大明' },
+ { value: 'jane', label: '李小美' },
+ { value: 'mike', label: '王志強' },
+ { value: 'sarah', label: '陳淑華' },
+ { value: 'david', label: '林建國' },
+ ];
+
+ const handleInputChange = (
+ event: ChangeEvent | SelectChangeEvent
+ ): void => {
+ const { name, value } = event.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ };
+
+ const handleSubmit = (e: FormEvent): void => {
+ e.preventDefault();
+ console.log('表單已提交:', formData);
+ // 處理表單提交
+ };
+
+ const handleCollapseToggle = (e: ChangeEvent): void => {
+ setIsCollapsed(e.target.checked);
+ };
+
+ return (
+ //
+ <>
+
+ {/* */}
+
+
+ }
+ label={
+
+ 上報結果
+ {isCollapsed ? (
+
+ ) : (
+
+ )}
+
+ }
+ />
+
+
+
+ {forSupervisor ? (
+
+
+ } label="合格" />
+ } label="不合格" />
+
+
+ ): undefined}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default EscalationComponent;
\ No newline at end of file
diff --git a/src/components/Jodetail/FGPickOrderCard.tsx b/src/components/Jodetail/FGPickOrderCard.tsx
new file mode 100644
index 0000000..885942a
--- /dev/null
+++ b/src/components/Jodetail/FGPickOrderCard.tsx
@@ -0,0 +1,120 @@
+"use client";
+
+import { FGPickOrderResponse } from "@/app/api/pickOrder/actions";
+import { Box, Card, CardContent, Grid, Stack, TextField, Button } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import QrCodeIcon from '@mui/icons-material/QrCode';
+
+type Props = {
+ fgOrder: FGPickOrderResponse;
+ onQrCodeClick: (pickOrderId: number) => void;
+};
+
+const FGPickOrderCard: React.FC = ({ fgOrder, onQrCodeClick }) => {
+ const { t } = useTranslation("pickOrder");
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default FGPickOrderCard;
\ No newline at end of file
diff --git a/src/components/Jodetail/FInishedJobOrderRecord.tsx b/src/components/Jodetail/FInishedJobOrderRecord.tsx
new file mode 100644
index 0000000..5845588
--- /dev/null
+++ b/src/components/Jodetail/FInishedJobOrderRecord.tsx
@@ -0,0 +1,556 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Stack,
+ TextField,
+ Typography,
+ Alert,
+ CircularProgress,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ TablePagination,
+ Modal,
+ Card,
+ CardContent,
+ CardActions,
+ Chip,
+ Accordion,
+ AccordionSummary,
+ AccordionDetails,
+ Checkbox, // ✅ Add Checkbox import
+} from "@mui/material";
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import { useCallback, useEffect, useState, useRef, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { useRouter } from "next/navigation";
+import {
+ fetchCompletedJobOrderPickOrdersWithCompletedSecondScan,
+ fetchCompletedJobOrderPickOrderLotDetails
+} from "@/app/api/jo/actions";
+import { fetchNameList, NameList } from "@/app/api/user/actions";
+import {
+ FormProvider,
+ useForm,
+} from "react-hook-form";
+import SearchBox, { Criterion } from "../SearchBox";
+import { useSession } from "next-auth/react";
+import { SessionWithTokens } from "@/config/authConfig";
+
+interface Props {
+ filterArgs: Record;
+}
+
+// ✅ 修改:已完成的 Job Order Pick Order 接口
+interface CompletedJobOrderPickOrder {
+ id: number;
+ pickOrderId: number;
+ pickOrderCode: string;
+ pickOrderConsoCode: string;
+ pickOrderTargetDate: string;
+ pickOrderStatus: string;
+ completedDate: string;
+ jobOrderId: number;
+ jobOrderCode: string;
+ jobOrderName: string;
+ reqQty: number;
+ uom: string;
+ planStart: string;
+ planEnd: string;
+ secondScanCompleted: boolean;
+ totalItems: number;
+ completedItems: number;
+}
+
+// ✅ 新增:Lot 详情接口
+interface LotDetail {
+ lotId: number;
+ lotNo: string;
+ expiryDate: string;
+ location: string;
+ availableQty: number;
+ requiredQty: number;
+ actualPickQty: number;
+ processingStatus: string;
+ lotAvailability: string;
+ pickOrderId: number;
+ pickOrderCode: string;
+ pickOrderConsoCode: string;
+ pickOrderLineId: number;
+ stockOutLineId: number;
+ stockOutLineStatus: string;
+ routerIndex: number;
+ routerArea: string;
+ routerRoute: string;
+ uomShortDesc: string;
+ secondQrScanStatus: string;
+ itemId: number;
+ itemCode: string;
+ itemName: string;
+ uomCode: string;
+ uomDesc: string;
+}
+
+const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => {
+ const { t } = useTranslation("jo");
+ const router = useRouter();
+ const { data: session } = useSession() as { data: SessionWithTokens | null };
+
+ const currentUserId = session?.id ? parseInt(session.id) : undefined;
+
+ // ✅ 修改:已完成 Job Order Pick Orders 状态
+ const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState([]);
+ const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false);
+
+ // ✅ 修改:详情视图状态
+ const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState(null);
+ const [showDetailView, setShowDetailView] = useState(false);
+ const [detailLotData, setDetailLotData] = useState([]);
+ const [detailLotDataLoading, setDetailLotDataLoading] = useState(false);
+
+ // ✅ 修改:搜索状态
+ const [searchQuery, setSearchQuery] = useState>({});
+ const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState([]);
+
+ // ✅ 修改:分页状态
+ const [paginationController, setPaginationController] = useState({
+ pageNum: 0,
+ pageSize: 10,
+ });
+
+ const formProps = useForm();
+ const errors = formProps.formState.errors;
+
+ // ✅ 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders
+ const fetchCompletedJobOrderPickOrdersData = useCallback(async () => {
+ if (!currentUserId) return;
+
+ setCompletedJobOrderPickOrdersLoading(true);
+ try {
+ console.log("🔍 Fetching completed Job Order pick orders...");
+
+ const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersWithCompletedSecondScan(currentUserId);
+
+ setCompletedJobOrderPickOrders(completedJobOrderPickOrders);
+ setFilteredJobOrderPickOrders(completedJobOrderPickOrders);
+ console.log("✅ Fetched completed Job Order pick orders:", completedJobOrderPickOrders);
+ } catch (error) {
+ console.error("❌ Error fetching completed Job Order pick orders:", error);
+ setCompletedJobOrderPickOrders([]);
+ setFilteredJobOrderPickOrders([]);
+ } finally {
+ setCompletedJobOrderPickOrdersLoading(false);
+ }
+ }, [currentUserId]);
+
+ // ✅ 新增:获取 lot 详情数据
+ const fetchLotDetailsData = useCallback(async (pickOrderId: number) => {
+ setDetailLotDataLoading(true);
+ try {
+ console.log("🔍 Fetching lot details for pick order:", pickOrderId);
+
+ const lotDetails = await fetchCompletedJobOrderPickOrderLotDetails(pickOrderId);
+
+ setDetailLotData(lotDetails);
+ console.log("✅ Fetched lot details:", lotDetails);
+ } catch (error) {
+ console.error("❌ Error fetching lot details:", error);
+ setDetailLotData([]);
+ } finally {
+ setDetailLotDataLoading(false);
+ }
+ }, []);
+
+ // ✅ 修改:初始化时获取数据
+ useEffect(() => {
+ if (currentUserId) {
+ fetchCompletedJobOrderPickOrdersData();
+ }
+ }, [currentUserId, fetchCompletedJobOrderPickOrdersData]);
+
+ // ✅ 修改:搜索功能
+ const handleSearch = useCallback((query: Record) => {
+ setSearchQuery({ ...query });
+ console.log("Search query:", query);
+
+ const filtered = completedJobOrderPickOrders.filter((pickOrder) => {
+ const pickOrderCodeMatch = !query.pickOrderCode ||
+ pickOrder.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
+
+ const jobOrderCodeMatch = !query.jobOrderCode ||
+ pickOrder.jobOrderCode?.toLowerCase().includes((query.jobOrderCode || "").toLowerCase());
+
+ const jobOrderNameMatch = !query.jobOrderName ||
+ pickOrder.jobOrderName?.toLowerCase().includes((query.jobOrderName || "").toLowerCase());
+
+ return pickOrderCodeMatch && jobOrderCodeMatch && jobOrderNameMatch;
+ });
+
+ setFilteredJobOrderPickOrders(filtered);
+ console.log("Filtered Job Order pick orders count:", filtered.length);
+ }, [completedJobOrderPickOrders]);
+
+ // ✅ 修改:重置搜索
+ const handleSearchReset = useCallback(() => {
+ setSearchQuery({});
+ setFilteredJobOrderPickOrders(completedJobOrderPickOrders);
+ }, [completedJobOrderPickOrders]);
+
+ // ✅ 修改:分页功能
+ const handlePageChange = useCallback((event: unknown, newPage: number) => {
+ setPaginationController(prev => ({
+ ...prev,
+ pageNum: newPage,
+ }));
+ }, []);
+
+ const handlePageSizeChange = useCallback((event: React.ChangeEvent) => {
+ const newPageSize = parseInt(event.target.value, 10);
+ setPaginationController({
+ pageNum: 0,
+ pageSize: newPageSize,
+ });
+ }, []);
+
+ // ✅ 修改:分页数据
+ const paginatedData = useMemo(() => {
+ const startIndex = paginationController.pageNum * paginationController.pageSize;
+ const endIndex = startIndex + paginationController.pageSize;
+ return filteredJobOrderPickOrders.slice(startIndex, endIndex);
+ }, [filteredJobOrderPickOrders, paginationController]);
+
+ // ✅ 修改:搜索条件
+ const searchCriteria: Criterion[] = [
+ {
+ label: t("Pick Order Code"),
+ paramName: "pickOrderCode",
+ type: "text",
+ },
+ {
+ label: t("Job Order Code"),
+ paramName: "jobOrderCode",
+ type: "text",
+ },
+ {
+ label: t("Job Order Item Name"),
+ paramName: "jobOrderName",
+ type: "text",
+ }
+ ];
+
+ // ✅ 修改:详情点击处理
+ const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => {
+ setSelectedJobOrderPickOrder(jobOrderPickOrder);
+ setShowDetailView(true);
+
+ // ✅ 获取 lot 详情数据
+ await fetchLotDetailsData(jobOrderPickOrder.pickOrderId);
+
+ // ✅ 触发打印按钮状态更新 - 基于详情数据
+ const allCompleted = jobOrderPickOrder.secondScanCompleted;
+
+ // ✅ 发送事件,包含标签页信息
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: allCompleted,
+ tabIndex: 2 // ✅ 明确指定这是来自标签页 2 的事件
+ }
+ }));
+
+ }, [fetchLotDetailsData]);
+
+ // ✅ 修改:返回列表视图
+ const handleBackToList = useCallback(() => {
+ setShowDetailView(false);
+ setSelectedJobOrderPickOrder(null);
+ setDetailLotData([]);
+
+ // ✅ 返回列表时禁用打印按钮
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: false,
+ tabIndex: 2
+ }
+ }));
+ }, []);
+
+ // ✅ 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息
+ if (showDetailView && selectedJobOrderPickOrder) {
+ return (
+
+
+ {/* 返回按钮和标题 */}
+
+
+
+ {t("Job Order Pick Order Details")}: {selectedJobOrderPickOrder.pickOrderCode}
+
+
+
+ {/* Job Order 信息卡片 */}
+
+
+
+
+ {t("Pick Order Code")}: {selectedJobOrderPickOrder.pickOrderCode}
+
+
+ {t("Job Order Code")}: {selectedJobOrderPickOrder.jobOrderCode}
+
+
+ {t("Job Order Item Name")}: {selectedJobOrderPickOrder.jobOrderName}
+
+
+ {t("Target Date")}: {selectedJobOrderPickOrder.pickOrderTargetDate}
+
+
+
+
+
+ {t("Required Qty")}: {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom}
+
+
+
+
+
+
+ {/* ✅ 修改:Lot 详情表格 - 添加复选框列 */}
+
+
+
+ {t("Lot Details")}
+
+
+ {detailLotDataLoading ? (
+
+
+
+ ) : (
+
+
+
+
+ {t("Index")}
+ {t("Route")}
+ {t("Item Code")}
+ {t("Item Name")}
+ {t("Lot No")}
+ {t("Location")}
+ {t("Required Qty")}
+ {t("Actual Pick Qty")}
+ {t("Processing Status")}
+ {t("Second Scan Status")}
+
+
+
+ {detailLotData.length === 0 ? (
+
+ {/* ✅ 恢复原来的 colSpan */}
+
+ {t("No lot details available")}
+
+
+
+ ) : (
+ detailLotData.map((lot, index) => (
+
+
+
+ {index + 1}
+
+
+
+
+ {lot.routerRoute || '-'}
+
+
+ {lot.itemCode}
+ {lot.itemName}
+ {lot.lotNo}
+ {lot.location}
+
+ {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc})
+
+
+ {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc})
+
+ {/* ✅ 修改:Processing Status 使用复选框 */}
+
+
+
+
+
+ {/* ✅ 修改:Second Scan Status 使用复选框 */}
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+ )}
+
+
+
+
+ );
+ }
+
+ // ✅ 修改:默认列表视图
+ return (
+
+
+ {/* 搜索框 */}
+
+
+
+
+ {/* 加载状态 */}
+ {completedJobOrderPickOrdersLoading ? (
+
+
+
+ ) : (
+
+ {/* 结果统计 */}
+
+ {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")}
+
+
+ {/* 列表 */}
+ {filteredJobOrderPickOrders.length === 0 ? (
+
+
+ {t("No completed Job Order pick orders with matching found")}
+
+
+ ) : (
+
+ {paginatedData.map((jobOrderPickOrder) => (
+
+
+
+
+
+ {jobOrderPickOrder.pickOrderCode}
+
+
+ {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.jobOrderCode}
+
+
+ {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()}
+
+
+ {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate}
+
+
+
+
+
+ {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")}
+
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* 分页 */}
+ {filteredJobOrderPickOrders.length > 0 && (
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default FInishedJobOrderRecord;
\ No newline at end of file
diff --git a/src/components/Jodetail/FinishedGoodSearchWrapper.tsx b/src/components/Jodetail/FinishedGoodSearchWrapper.tsx
new file mode 100644
index 0000000..da633c5
--- /dev/null
+++ b/src/components/Jodetail/FinishedGoodSearchWrapper.tsx
@@ -0,0 +1,26 @@
+import { fetchPickOrders } from "@/app/api/pickOrder";
+import GeneralLoading from "../General/GeneralLoading";
+import PickOrderSearch from "./FinishedGoodSearchWrapper";
+
+interface SubComponents {
+ Loading: typeof GeneralLoading;
+}
+
+const FinishedGoodSearchWrapper: React.FC & SubComponents = async () => {
+ const [pickOrders] = await Promise.all([
+ fetchPickOrders({
+ code: undefined,
+ targetDateFrom: undefined,
+ targetDateTo: undefined,
+ type: undefined,
+ status: undefined,
+ itemName: undefined,
+ }),
+ ]);
+
+ return ;
+};
+
+FinishedGoodSearchWrapper.Loading = GeneralLoading;
+
+export default FinishedGoodSearchWrapper;
diff --git a/src/components/Jodetail/ItemSelect.tsx b/src/components/Jodetail/ItemSelect.tsx
new file mode 100644
index 0000000..f611e0e
--- /dev/null
+++ b/src/components/Jodetail/ItemSelect.tsx
@@ -0,0 +1,79 @@
+
+import { ItemCombo } from "@/app/api/settings/item/actions";
+import { Autocomplete, TextField } from "@mui/material";
+import { useCallback, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+
+interface CommonProps {
+ allItems: ItemCombo[];
+ error?: boolean;
+}
+
+interface SingleAutocompleteProps extends CommonProps {
+ value: number | string | undefined;
+ onItemSelect: (itemId: number, uom: string, uomId: number) => void | Promise;
+ // multiple: false;
+}
+
+type Props = SingleAutocompleteProps;
+
+const ItemSelect: React.FC = ({
+ allItems,
+ value,
+ error,
+ onItemSelect
+}) => {
+ const { t } = useTranslation("item");
+ const filteredItems = useMemo(() => {
+ return allItems
+ }, [allItems])
+
+ const options = useMemo(() => {
+ return [
+ {
+ value: -1, // think think sin
+ label: t("None"),
+ uom: "",
+ uomId: -1,
+ group: "default",
+ },
+ ...filteredItems.map((i) => ({
+ value: i.id as number,
+ label: i.label,
+ uom: i.uom,
+ uomId: i.uomId,
+ group: "existing",
+ })),
+ ];
+ }, [t, filteredItems]);
+
+ const currentValue = options.find((o) => o.value === value) || options[0];
+
+ const onChange = useCallback(
+ (
+ event: React.SyntheticEvent,
+ newValue: { value: number; uom: string; uomId: number; group: string } | { uom: string; uomId: number; value: number }[],
+ ) => {
+ const singleNewVal = newValue as {
+ value: number;
+ uom: string;
+ uomId: number;
+ group: string;
+ };
+ onItemSelect(singleNewVal.value, singleNewVal.uom, singleNewVal.uomId)
+ }
+ , [onItemSelect])
+ return (
+ option.label}
+ options={options}
+ renderInput={(params) => }
+ />
+ );
+}
+export default ItemSelect
\ No newline at end of file
diff --git a/src/components/Jodetail/JobPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx
new file mode 100644
index 0000000..03f345e
--- /dev/null
+++ b/src/components/Jodetail/JobPickExecution.tsx
@@ -0,0 +1,1514 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Stack,
+ TextField,
+ Typography,
+ Alert,
+ CircularProgress,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Checkbox,
+ TablePagination,
+ Modal,
+} from "@mui/material";
+import { useCallback, useEffect, useState, useRef, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { useRouter } from "next/navigation";
+import {
+ updateStockOutLineStatus,
+ createStockOutLine,
+ recordPickExecutionIssue,
+ fetchFGPickOrders,
+ FGPickOrderResponse,
+ autoAssignAndReleasePickOrder,
+ AutoAssignReleaseResponse,
+ checkPickOrderCompletion,
+ PickOrderCompletionResponse,
+ checkAndCompletePickOrderByConsoCode
+} from "@/app/api/pickOrder/actions";
+// ✅ 修改:使用 Job Order API
+import {
+ fetchJobOrderLotsHierarchical,
+ fetchUnassignedJobOrderPickOrders,
+ assignJobOrderPickOrder
+} from "@/app/api/jo/actions";
+import { fetchNameList, NameList } from "@/app/api/user/actions";
+import {
+ FormProvider,
+ useForm,
+} from "react-hook-form";
+import SearchBox, { Criterion } from "../SearchBox";
+import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
+import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
+import QrCodeIcon from '@mui/icons-material/QrCode';
+import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
+import { useSession } from "next-auth/react";
+import { SessionWithTokens } from "@/config/authConfig";
+import { fetchStockInLineInfo } from "@/app/api/po/actions";
+import GoodPickExecutionForm from "./JobPickExecutionForm";
+import FGPickOrderCard from "./FGPickOrderCard";
+
+interface Props {
+ filterArgs: Record;
+}
+
+// ✅ QR Code Modal Component (from GoodPickExecution)
+const QrCodeModal: React.FC<{
+ open: boolean;
+ onClose: () => void;
+ lot: any | null;
+ onQrCodeSubmit: (lotNo: string) => void;
+ combinedLotData: any[];
+}> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
+ const { t } = useTranslation("jo");
+ const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
+ const [manualInput, setManualInput] = useState('');
+
+ const [manualInputSubmitted, setManualInputSubmitted] = useState(false);
+ const [manualInputError, setManualInputError] = useState(false);
+ const [isProcessingQr, setIsProcessingQr] = useState(false);
+ const [qrScanFailed, setQrScanFailed] = useState(false);
+ const [qrScanSuccess, setQrScanSuccess] = useState(false);
+
+ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set());
+ const [scannedQrResult, setScannedQrResult] = useState('');
+
+ // Process scanned QR codes
+ useEffect(() => {
+ if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
+ const latestQr = qrValues[qrValues.length - 1];
+
+ if (processedQrCodes.has(latestQr)) {
+ console.log("QR code already processed, skipping...");
+ return;
+ }
+
+ setProcessedQrCodes(prev => new Set(prev).add(latestQr));
+
+ try {
+ const qrData = JSON.parse(latestQr);
+
+ if (qrData.stockInLineId && qrData.itemId) {
+ setIsProcessingQr(true);
+ setQrScanFailed(false);
+
+ fetchStockInLineInfo(qrData.stockInLineId)
+ .then((stockInLineInfo) => {
+ console.log("Stock in line info:", stockInLineInfo);
+ setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
+
+ if (stockInLineInfo.lotNo === lot.lotNo) {
+ console.log(`✅ QR Code verified for lot: ${lot.lotNo}`);
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ resetScan();
+ } else {
+ console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ }
+ })
+ .catch((error) => {
+ console.error("Error fetching stock in line info:", error);
+ setScannedQrResult('Error fetching data');
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ })
+ .finally(() => {
+ setIsProcessingQr(false);
+ });
+ } else {
+ const qrContent = latestQr.replace(/[{}]/g, '');
+ setScannedQrResult(qrContent);
+
+ if (qrContent === lot.lotNo) {
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ resetScan();
+ } else {
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ }
+ }
+ } catch (error) {
+ console.log("QR code is not JSON format, trying direct comparison");
+ const qrContent = latestQr.replace(/[{}]/g, '');
+ setScannedQrResult(qrContent);
+
+ if (qrContent === lot.lotNo) {
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ resetScan();
+ } else {
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ }
+ }
+ }
+ }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
+
+ // Clear states when modal opens
+ useEffect(() => {
+ if (open) {
+ setManualInput('');
+ setManualInputSubmitted(false);
+ setManualInputError(false);
+ setIsProcessingQr(false);
+ setQrScanFailed(false);
+ setQrScanSuccess(false);
+ setScannedQrResult('');
+ setProcessedQrCodes(new Set());
+ }
+ }, [open]);
+
+ useEffect(() => {
+ if (lot) {
+ setManualInput('');
+ setManualInputSubmitted(false);
+ setManualInputError(false);
+ setIsProcessingQr(false);
+ setQrScanFailed(false);
+ setQrScanSuccess(false);
+ setScannedQrResult('');
+ setProcessedQrCodes(new Set());
+ }
+ }, [lot]);
+
+ // Auto-submit manual input when it matches
+ useEffect(() => {
+ if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
+ console.log(' Auto-submitting manual input:', manualInput.trim());
+
+ const timer = setTimeout(() => {
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ setManualInput('');
+ setManualInputError(false);
+ setManualInputSubmitted(false);
+ }, 200);
+
+ return () => clearTimeout(timer);
+ }
+ }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
+
+ const handleManualSubmit = () => {
+ if (manualInput.trim() === lot?.lotNo) {
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ setManualInput('');
+ } else {
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ }
+ };
+
+ useEffect(() => {
+ if (open) {
+ startScan();
+ }
+ }, [open, startScan]);
+
+ return (
+
+
+
+ {t("QR Code Scan for Lot")}: {lot?.lotNo}
+
+
+ {isProcessingQr && (
+
+
+ {t("Processing QR code...")}
+
+
+ )}
+
+
+
+ {t("Manual Input")}:
+
+ {
+ setManualInput(e.target.value);
+ if (qrScanFailed || manualInputError) {
+ setQrScanFailed(false);
+ setManualInputError(false);
+ setManualInputSubmitted(false);
+ }
+ }}
+ sx={{ mb: 1 }}
+ error={manualInputSubmitted && manualInputError}
+ helperText={
+ manualInputSubmitted && manualInputError
+ ? `${t("The input is not the same as the expected lot number.")}`
+ : ''
+ }
+ />
+
+
+
+ {qrValues.length > 0 && (
+
+
+ {t("QR Scan Result:")} {scannedQrResult}
+
+
+ {qrScanSuccess && (
+
+ ✅ {t("Verified successfully!")}
+
+ )}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+const JobPickExecution: React.FC = ({ filterArgs }) => {
+ const { t } = useTranslation("jo");
+ const router = useRouter();
+ const { data: session } = useSession() as { data: SessionWithTokens | null };
+
+ const currentUserId = session?.id ? parseInt(session.id) : undefined;
+
+ // ✅ 修改:使用 Job Order 数据结构
+ const [jobOrderData, setJobOrderData] = useState(null);
+ const [combinedLotData, setCombinedLotData] = useState([]);
+ const [combinedDataLoading, setCombinedDataLoading] = useState(false);
+ const [originalCombinedData, setOriginalCombinedData] = useState([]);
+
+ // ✅ 添加未分配订单状态
+ const [unassignedOrders, setUnassignedOrders] = useState([]);
+ const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
+
+ const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
+
+ const [qrScanInput, setQrScanInput] = useState('');
+ const [qrScanError, setQrScanError] = useState(false);
+ const [qrScanSuccess, setQrScanSuccess] = useState(false);
+
+ const [pickQtyData, setPickQtyData] = useState>({});
+ const [searchQuery, setSearchQuery] = useState>({});
+
+ const [paginationController, setPaginationController] = useState({
+ pageNum: 0,
+ pageSize: 10,
+ });
+
+ const [usernameList, setUsernameList] = useState([]);
+
+ const initializationRef = useRef(false);
+ const autoAssignRef = useRef(false);
+
+ const formProps = useForm();
+ const errors = formProps.formState.errors;
+
+ // ✅ Add QR modal states
+ const [qrModalOpen, setQrModalOpen] = useState(false);
+ const [selectedLotForQr, setSelectedLotForQr] = useState(null);
+
+ // ✅ Add GoodPickExecutionForm states
+ const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
+ const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null);
+ const [fgPickOrders, setFgPickOrders] = useState([]);
+ const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
+
+ // ✅ Add these missing state variables
+ const [isManualScanning, setIsManualScanning] = useState(false);
+ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set());
+ const [lastProcessedQr, setLastProcessedQr] = useState('');
+ const [isRefreshingData, setIsRefreshingData] = useState(false);
+
+ // ✅ 修改:加载未分配的 Job Order 订单
+ const loadUnassignedOrders = useCallback(async () => {
+ setIsLoadingUnassigned(true);
+ try {
+ const orders = await fetchUnassignedJobOrderPickOrders();
+ setUnassignedOrders(orders);
+ } catch (error) {
+ console.error("Error loading unassigned orders:", error);
+ } finally {
+ setIsLoadingUnassigned(false);
+ }
+ }, []);
+
+ // ✅ 修改:分配订单给当前用户
+ const handleAssignOrder = useCallback(async (pickOrderId: number) => {
+ if (!currentUserId) {
+ console.error("Missing user id in session");
+ return;
+ }
+
+ try {
+ const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
+ if (result.message === "Successfully assigned") {
+ console.log("✅ Successfully assigned pick order");
+ // 刷新数据
+ window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
+ // 重新加载未分配订单列表
+ loadUnassignedOrders();
+ } else {
+ console.warn("⚠️ Assignment failed:", result.message);
+ alert(`Assignment failed: ${result.message}`);
+ }
+ } catch (error) {
+ console.error("❌ Error assigning order:", error);
+ alert("Error occurred during assignment");
+ }
+ }, [currentUserId, loadUnassignedOrders]);
+
+ const fetchFgPickOrdersData = useCallback(async () => {
+ if (!currentUserId) return;
+
+ setFgPickOrdersLoading(true);
+ try {
+ // Get all pick order IDs from combinedLotData
+ const pickOrderIds = Array.from(new Set(combinedLotData.map(lot => lot.pickOrderId)));
+
+ if (pickOrderIds.length === 0) {
+ setFgPickOrders([]);
+ return;
+ }
+
+ // Fetch FG pick orders for each pick order ID
+ const fgPickOrdersPromises = pickOrderIds.map(pickOrderId =>
+ fetchFGPickOrders(pickOrderId)
+ );
+
+ const fgPickOrdersResults = await Promise.all(fgPickOrdersPromises);
+
+ // Flatten the results (each fetchFGPickOrders returns an array)
+ const allFgPickOrders = fgPickOrdersResults.flat();
+
+ setFgPickOrders(allFgPickOrders);
+ console.log("✅ Fetched FG pick orders:", allFgPickOrders);
+ } catch (error) {
+ console.error("❌ Error fetching FG pick orders:", error);
+ setFgPickOrders([]);
+ } finally {
+ setFgPickOrdersLoading(false);
+ }
+ }, [currentUserId, combinedLotData]);
+
+ useEffect(() => {
+ if (combinedLotData.length > 0) {
+ fetchFgPickOrdersData();
+ }
+ }, [combinedLotData, fetchFgPickOrdersData]);
+
+ // ✅ Handle QR code button click
+ const handleQrCodeClick = (pickOrderId: number) => {
+ console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
+ // TODO: Implement QR code functionality
+ };
+
+ // ✅ 修改:使用 Job Order API 获取数据
+ const fetchJobOrderData = useCallback(async (userId?: number) => {
+ setCombinedDataLoading(true);
+ try {
+ const userIdToUse = userId || currentUserId;
+
+ console.log(" fetchJobOrderData called with userId:", userIdToUse);
+
+ if (!userIdToUse) {
+ console.warn("⚠️ No userId available, skipping API call");
+ setJobOrderData(null);
+ setCombinedLotData([]);
+ setOriginalCombinedData([]);
+ return;
+ }
+
+ // ✅ 使用 Job Order API
+ const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse);
+ console.log("✅ Job Order data:", jobOrderData);
+
+ setJobOrderData(jobOrderData);
+
+ // ✅ Transform hierarchical data to flat structure for the table
+ const flatLotData: any[] = [];
+
+ if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
+ jobOrderData.pickOrderLines.forEach((line: any) => {
+ if (line.lots && line.lots.length > 0) {
+ line.lots.forEach((lot: any) => {
+ flatLotData.push({
+ // Pick order info
+ pickOrderId: jobOrderData.pickOrder.id,
+ pickOrderCode: jobOrderData.pickOrder.code,
+ pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
+ pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
+ pickOrderType: jobOrderData.pickOrder.type,
+ pickOrderStatus: jobOrderData.pickOrder.status,
+ pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
+
+ // Pick order line info
+ pickOrderLineId: line.id,
+ pickOrderLineRequiredQty: line.requiredQty,
+ pickOrderLineStatus: line.status,
+
+ // Item info
+ itemId: line.itemId,
+ itemCode: line.itemCode,
+ itemName: line.itemName,
+ uomCode: line.uomCode,
+ uomDesc: line.uomDesc,
+
+ // Lot info
+ lotId: lot.lotId,
+ lotNo: lot.lotNo,
+ expiryDate: lot.expiryDate,
+ location: lot.location,
+ availableQty: lot.availableQty,
+ requiredQty: lot.requiredQty,
+ actualPickQty: lot.actualPickQty,
+ lotStatus: lot.lotStatus,
+ lotAvailability: lot.lotAvailability,
+ processingStatus: lot.processingStatus,
+ stockOutLineId: lot.stockOutLineId,
+ stockOutLineStatus: lot.stockOutLineStatus,
+ stockOutLineQty: lot.stockOutLineQty,
+
+ // Router info
+ routerIndex: lot.routerIndex,
+ secondQrScanStatus: lot.secondQrScanStatus,
+ routerArea: lot.routerArea,
+ routerRoute: lot.routerRoute,
+ uomShortDesc: lot.uomShortDesc
+ });
+ });
+ }
+ });
+ }
+
+ console.log("✅ Transformed flat lot data:", flatLotData);
+ setCombinedLotData(flatLotData);
+ setOriginalCombinedData(flatLotData);
+
+ // ✅ 计算完成状态并发送事件
+ const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
+ lot.processingStatus === 'completed'
+ );
+
+ // ✅ 发送完成状态事件,包含标签页信息
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: allCompleted,
+ tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件
+ }
+ }));
+
+ } catch (error) {
+ console.error("❌ Error fetching job order data:", error);
+ setJobOrderData(null);
+ setCombinedLotData([]);
+ setOriginalCombinedData([]);
+
+ // ✅ 如果加载失败,禁用打印按钮
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: false,
+ tabIndex: 0
+ }
+ }));
+ } finally {
+ setCombinedDataLoading(false);
+ }
+ }, [currentUserId]);
+
+ // ✅ 修改:初始化时加载数据
+ useEffect(() => {
+ if (session && currentUserId && !initializationRef.current) {
+ console.log("✅ Session loaded, initializing job order...");
+ initializationRef.current = true;
+
+ // 加载 Job Order 数据
+ fetchJobOrderData();
+ // 加载未分配订单
+ loadUnassignedOrders();
+ }
+ }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);
+
+ // ✅ Add event listener for manual assignment
+ useEffect(() => {
+ const handlePickOrderAssigned = () => {
+ console.log("🔄 Pick order assigned event received, refreshing data...");
+ fetchJobOrderData();
+ };
+
+ window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
+
+ return () => {
+ window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
+ };
+ }, [fetchJobOrderData]);
+
+ // ✅ Handle QR code submission for matched lot (external scanning)
+ const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
+ console.log(`✅ Processing QR Code for lot: ${lotNo}`);
+
+ // ✅ Use current data without refreshing to avoid infinite loop
+ const currentLotData = combinedLotData;
+ console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
+
+ const matchingLots = currentLotData.filter(lot =>
+ lot.lotNo === lotNo ||
+ lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
+ );
+
+ if (matchingLots.length === 0) {
+ console.error(`❌ Lot not found: ${lotNo}`);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ const availableLotNos = currentLotData.map(lot => lot.lotNo).join(', ');
+ console.log(`❌ QR Code "${lotNo}" does not match any expected lots. Available lots: ${availableLotNos}`);
+ return;
+ }
+
+ console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots);
+ setQrScanError(false);
+
+ try {
+ let successCount = 0;
+ let errorCount = 0;
+
+ for (const matchingLot of matchingLots) {
+ console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
+
+ if (matchingLot.stockOutLineId) {
+ const stockOutLineUpdate = await updateStockOutLineStatus({
+ id: matchingLot.stockOutLineId,
+ status: 'checked',
+ qty: 0
+ });
+ console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate);
+
+ // Treat multiple backend shapes as success (type-safe via any)
+ const r: any = stockOutLineUpdate as any;
+ const updateOk =
+ r?.code === 'SUCCESS' ||
+ typeof r?.id === 'number' ||
+ r?.type === 'checked' ||
+ r?.status === 'checked' ||
+ typeof r?.entity?.id === 'number' ||
+ r?.entity?.status === 'checked';
+
+ if (updateOk) {
+ successCount++;
+ } else {
+ errorCount++;
+ }
+ } else {
+ const createStockOutLineData = {
+ consoCode: matchingLot.pickOrderConsoCode,
+ pickOrderLineId: matchingLot.pickOrderLineId,
+ inventoryLotLineId: matchingLot.lotId,
+ qty: 0
+ };
+
+ const createResult = await createStockOutLine(createStockOutLineData);
+ console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, createResult);
+
+ if (createResult && createResult.code === "SUCCESS") {
+ // Immediately set status to checked for new line
+ let newSolId: number | undefined;
+ const anyRes: any = createResult as any;
+ if (typeof anyRes?.id === 'number') {
+ newSolId = anyRes.id;
+ } else if (anyRes?.entity) {
+ newSolId = Array.isArray(anyRes.entity) ? anyRes.entity[0]?.id : anyRes.entity?.id;
+ }
+
+ if (newSolId) {
+ const setChecked = await updateStockOutLineStatus({
+ id: newSolId,
+ status: 'checked',
+ qty: 0
+ });
+ if (setChecked && setChecked.code === "SUCCESS") {
+ successCount++;
+ } else {
+ errorCount++;
+ }
+ } else {
+ console.warn("Created stock out line but no ID returned; cannot set to checked");
+ errorCount++;
+ }
+ } else {
+ errorCount++;
+ }
+ }
+ }
+
+ // ✅ FIXED: Set refresh flag before refreshing data
+ setIsRefreshingData(true);
+ console.log("🔄 Refreshing data after QR code processing...");
+ await fetchJobOrderData();
+
+ if (successCount > 0) {
+ console.log(`✅ QR Code processing completed: ${successCount} updated/created`);
+ setQrScanSuccess(true);
+ setQrScanError(false);
+ setQrScanInput(''); // Clear input after successful processing
+ setIsManualScanning(false);
+ stopScan();
+ resetScan();
+ } else {
+ console.error(`❌ QR Code processing failed: ${errorCount} errors`);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ }
+ } catch (error) {
+ console.error("❌ Error processing QR code:", error);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+
+ // ✅ Still refresh data even on error
+ setIsRefreshingData(true);
+ await fetchJobOrderData();
+ } finally {
+ // ✅ Clear refresh flag after a short delay
+ setTimeout(() => {
+ setIsRefreshingData(false);
+ }, 1000);
+ }
+ }, [combinedLotData, fetchJobOrderData]);
+
+ const handleManualInputSubmit = useCallback(() => {
+ if (qrScanInput.trim() !== '') {
+ handleQrCodeSubmit(qrScanInput.trim());
+ }
+ }, [qrScanInput, handleQrCodeSubmit]);
+
+ // ✅ Handle QR code submission from modal (internal scanning)
+ const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
+ if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
+ console.log(`✅ QR Code verified for lot: ${lotNo}`);
+
+ const requiredQty = selectedLotForQr.requiredQty;
+ const lotId = selectedLotForQr.lotId;
+
+ // Create stock out line
+ const stockOutLineData: CreateStockOutLine = {
+ consoCode: selectedLotForQr.pickOrderConsoCode,
+ pickOrderLineId: selectedLotForQr.pickOrderLineId,
+ inventoryLotLineId: selectedLotForQr.lotId,
+ qty: 0.0
+ };
+
+ try {
+ await createStockOutLine(stockOutLineData);
+ console.log("Stock out line created successfully!");
+
+ // Close modal
+ setQrModalOpen(false);
+ setSelectedLotForQr(null);
+
+ // Set pick quantity
+ const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
+ setTimeout(() => {
+ setPickQtyData(prev => ({
+ ...prev,
+ [lotKey]: requiredQty
+ }));
+ console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
+ }, 500);
+
+ // Refresh data
+ await fetchJobOrderData();
+ } catch (error) {
+ console.error("Error creating stock out line:", error);
+ }
+ }
+ }, [selectedLotForQr, fetchJobOrderData]);
+
+ // ✅ Outside QR scanning - process QR codes from outside the page automatically
+ useEffect(() => {
+ if (qrValues.length > 0 && combinedLotData.length > 0) {
+ const latestQr = qrValues[qrValues.length - 1];
+
+ // Extract lot number from QR code
+ let lotNo = '';
+ try {
+ const qrData = JSON.parse(latestQr);
+ if (qrData.stockInLineId && qrData.itemId) {
+ // For JSON QR codes, we need to fetch the lot number
+ fetchStockInLineInfo(qrData.stockInLineId)
+ .then((stockInLineInfo) => {
+ console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
+ const extractedLotNo = stockInLineInfo.lotNo;
+ if (extractedLotNo) {
+ console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
+ handleQrCodeSubmit(extractedLotNo);
+ }
+ })
+ .catch((error) => {
+ console.error("Outside QR scan - Error fetching stock in line info:", error);
+ });
+ return; // Exit early for JSON QR codes
+ }
+ } catch (error) {
+ // Not JSON format, treat as direct lot number
+ lotNo = latestQr.replace(/[{}]/g, '');
+ }
+
+ // For direct lot number QR codes
+ if (lotNo) {
+ console.log(`Outside QR scan detected (direct): ${lotNo}`);
+ handleQrCodeSubmit(lotNo);
+ }
+ }
+ }, [qrValues, combinedLotData, handleQrCodeSubmit]);
+
+ const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
+ if (value === '' || value === null || value === undefined) {
+ setPickQtyData(prev => ({
+ ...prev,
+ [lotKey]: 0
+ }));
+ return;
+ }
+
+ const numericValue = typeof value === 'string' ? parseFloat(value) : value;
+
+ if (isNaN(numericValue)) {
+ setPickQtyData(prev => ({
+ ...prev,
+ [lotKey]: 0
+ }));
+ return;
+ }
+
+ setPickQtyData(prev => ({
+ ...prev,
+ [lotKey]: numericValue
+ }));
+ }, []);
+
+ const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
+ const [autoAssignMessage, setAutoAssignMessage] = useState('');
+ const [completionStatus, setCompletionStatus] = useState(null);
+
+ const checkAndAutoAssignNext = useCallback(async () => {
+ if (!currentUserId) return;
+
+ try {
+ const completionResponse = await checkPickOrderCompletion(currentUserId);
+
+ if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
+ console.log("Found completed pick orders, auto-assigning next...");
+ // ✅ 移除前端的自动分配逻辑,因为后端已经处理了
+ // await handleAutoAssignAndRelease(); // 删除这个函数
+ }
+ } catch (error) {
+ console.error("Error checking pick order completion:", error);
+ }
+ }, [currentUserId]);
+
+ // ✅ Handle submit pick quantity
+ const handleSubmitPickQty = useCallback(async (lot: any) => {
+ const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
+ const newQty = pickQtyData[lotKey] || 0;
+
+ if (!lot.stockOutLineId) {
+ console.error("No stock out line found for this lot");
+ return;
+ }
+
+ try {
+ const currentActualPickQty = lot.actualPickQty || 0;
+ const cumulativeQty = currentActualPickQty + newQty;
+
+ let newStatus = 'partially_completed';
+
+ if (cumulativeQty >= lot.requiredQty) {
+ newStatus = 'completed';
+ }
+
+ console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
+ console.log(`Lot: ${lot.lotNo}`);
+ console.log(`Required Qty: ${lot.requiredQty}`);
+ console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
+ console.log(`New Submitted Qty: ${newQty}`);
+ console.log(`Cumulative Qty: ${cumulativeQty}`);
+ console.log(`New Status: ${newStatus}`);
+ console.log(`=====================================`);
+
+ await updateStockOutLineStatus({
+ id: lot.stockOutLineId,
+ status: newStatus,
+ qty: cumulativeQty
+ });
+
+ if (newQty > 0) {
+ await updateInventoryLotLineQuantities({
+ inventoryLotLineId: lot.lotId,
+ qty: newQty,
+ status: 'available',
+ operation: 'pick'
+ });
+ }
+
+ // ✅ FIXED: Use the proper API function instead of direct fetch
+ if (newStatus === 'completed' && lot.pickOrderConsoCode) {
+ console.log(`✅ Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
+
+ try {
+ // ✅ Use the imported API function instead of direct fetch
+ 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("Pick quantity submitted successfully!");
+
+ setTimeout(() => {
+ checkAndAutoAssignNext();
+ }, 1000);
+
+ } catch (error) {
+ console.error("Error submitting pick quantity:", error);
+ }
+ }, [pickQtyData, fetchJobOrderData, checkAndAutoAssignNext]);
+ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
+ if (!lot.stockOutLineId) {
+ console.error("No stock out line found for this lot");
+ return;
+ }
+
+ try {
+ // ✅ FIXED: Calculate cumulative quantity correctly
+ const currentActualPickQty = lot.actualPickQty || 0;
+ const cumulativeQty = currentActualPickQty + submitQty;
+
+ // ✅ FIXED: Determine status based on cumulative quantity vs required quantity
+ let newStatus = 'partially_completed';
+
+ if (cumulativeQty >= lot.requiredQty) {
+ newStatus = 'completed';
+ } else if (cumulativeQty > 0) {
+ newStatus = 'partially_completed';
+ } else {
+ newStatus = 'checked'; // QR scanned but no quantity submitted yet
+ }
+
+ console.log(`=== PICK QUANTITY SUBMISSION DEBUG ===`);
+ console.log(`Lot: ${lot.lotNo}`);
+ console.log(`Required Qty: ${lot.requiredQty}`);
+ console.log(`Current Actual Pick Qty: ${currentActualPickQty}`);
+ console.log(`New Submitted Qty: ${submitQty}`);
+ console.log(`Cumulative Qty: ${cumulativeQty}`);
+ console.log(`New Status: ${newStatus}`);
+ console.log(`=====================================`);
+
+ await updateStockOutLineStatus({
+ id: lot.stockOutLineId,
+ status: newStatus,
+ qty: cumulativeQty // ✅ Use cumulative quantity
+ });
+
+ if (submitQty > 0) {
+ await updateInventoryLotLineQuantities({
+ inventoryLotLineId: lot.lotId,
+ qty: submitQty,
+ status: 'available',
+ operation: 'pick'
+ });
+ }
+
+ // ✅ 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...`);
+
+ 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("Pick quantity submitted successfully!");
+
+ setTimeout(() => {
+ checkAndAutoAssignNext();
+ }, 1000);
+
+ } catch (error) {
+ console.error("Error submitting pick quantity:", error);
+ }
+ }, [fetchJobOrderData, checkAndAutoAssignNext]);
+ // ✅ Handle reject lot
+ const handleRejectLot = useCallback(async (lot: any) => {
+ if (!lot.stockOutLineId) {
+ console.error("No stock out line found for this lot");
+ return;
+ }
+
+ try {
+ await updateStockOutLineStatus({
+ id: lot.stockOutLineId,
+ status: 'rejected',
+ qty: 0
+ });
+
+ await fetchJobOrderData();
+ console.log("Lot rejected successfully!");
+
+ setTimeout(() => {
+ checkAndAutoAssignNext();
+ }, 1000);
+
+ } catch (error) {
+ console.error("Error rejecting lot:", error);
+ }
+ }, [fetchJobOrderData, checkAndAutoAssignNext]);
+
+ // ✅ Handle pick execution form
+ const handlePickExecutionForm = useCallback((lot: any) => {
+ console.log("=== Pick Execution Form ===");
+ console.log("Lot data:", lot);
+
+ if (!lot) {
+ console.warn("No lot data provided for pick execution form");
+ return;
+ }
+
+ console.log("Opening pick execution form for lot:", lot.lotNo);
+
+ setSelectedLotForExecutionForm(lot);
+ setPickExecutionFormOpen(true);
+
+ console.log("Pick execution form opened for lot ID:", lot.lotId);
+ }, []);
+
+ const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
+ try {
+ console.log("Pick execution form submitted:", data);
+
+ const result = await recordPickExecutionIssue(data);
+ console.log("Pick execution issue recorded:", result);
+
+ if (result && result.code === "SUCCESS") {
+ console.log("✅ Pick execution issue recorded successfully");
+ } else {
+ console.error("❌ Failed to record pick execution issue:", result);
+ }
+
+ setPickExecutionFormOpen(false);
+ setSelectedLotForExecutionForm(null);
+
+ await fetchJobOrderData();
+ } catch (error) {
+ console.error("Error submitting pick execution form:", error);
+ }
+ }, [fetchJobOrderData]);
+
+ // ✅ Calculate remaining required quantity
+ const calculateRemainingRequiredQty = useCallback((lot: any) => {
+ const requiredQty = lot.requiredQty || 0;
+ const stockOutLineQty = lot.stockOutLineQty || 0;
+ return Math.max(0, requiredQty - stockOutLineQty);
+ }, []);
+
+ // Search criteria
+ const searchCriteria: Criterion[] = [
+ {
+ label: t("Pick Order Code"),
+ paramName: "pickOrderCode",
+ type: "text",
+ },
+ {
+ label: t("Item Code"),
+ paramName: "itemCode",
+ type: "text",
+ },
+ {
+ label: t("Item Name"),
+ paramName: "itemName",
+ type: "text",
+ },
+ {
+ label: t("Lot No"),
+ paramName: "lotNo",
+ type: "text",
+ },
+ ];
+
+ const handleSearch = useCallback((query: Record) => {
+ setSearchQuery({ ...query });
+ console.log("Search query:", query);
+
+ if (!originalCombinedData) return;
+
+ const filtered = originalCombinedData.filter((lot: any) => {
+ const pickOrderCodeMatch = !query.pickOrderCode ||
+ lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
+
+ const itemCodeMatch = !query.itemCode ||
+ lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
+
+ const itemNameMatch = !query.itemName ||
+ lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
+
+ const lotNoMatch = !query.lotNo ||
+ lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
+
+ return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
+ });
+
+ setCombinedLotData(filtered);
+ console.log("Filtered lots count:", filtered.length);
+ }, [originalCombinedData]);
+
+ const handleReset = useCallback(() => {
+ setSearchQuery({});
+ if (originalCombinedData) {
+ setCombinedLotData(originalCombinedData);
+ }
+ }, [originalCombinedData]);
+
+ const handlePageChange = useCallback((event: unknown, newPage: number) => {
+ setPaginationController(prev => ({
+ ...prev,
+ pageNum: newPage,
+ }));
+ }, []);
+
+ const handlePageSizeChange = useCallback((event: React.ChangeEvent) => {
+ const newPageSize = parseInt(event.target.value, 10);
+ setPaginationController({
+ pageNum: 0,
+ pageSize: newPageSize,
+ });
+ }, []);
+
+ // Pagination data with sorting by routerIndex
+ const paginatedData = useMemo(() => {
+ // ✅ Sort by routerIndex first, then by other criteria
+ const sortedData = [...combinedLotData].sort((a, b) => {
+ const aIndex = a.routerIndex || 0;
+ const bIndex = b.routerIndex || 0;
+
+ // Primary sort: by routerIndex
+ if (aIndex !== bIndex) {
+ return aIndex - bIndex;
+ }
+
+ // Secondary sort: by pickOrderCode if routerIndex is the same
+ if (a.pickOrderCode !== b.pickOrderCode) {
+ return a.pickOrderCode.localeCompare(b.pickOrderCode);
+ }
+
+ // Tertiary sort: by lotNo if everything else is the same
+ return (a.lotNo || '').localeCompare(b.lotNo || '');
+ });
+
+ const startIndex = paginationController.pageNum * paginationController.pageSize;
+ const endIndex = startIndex + paginationController.pageSize;
+ return sortedData.slice(startIndex, endIndex);
+ }, [combinedLotData, paginationController]);
+
+ // ✅ Add these functions for manual scanning
+ const handleStartScan = useCallback(() => {
+ console.log(" Starting manual QR scan...");
+ setIsManualScanning(true);
+ setProcessedQrCodes(new Set());
+ setLastProcessedQr('');
+ setQrScanError(false);
+ setQrScanSuccess(false);
+ startScan();
+ }, [startScan]);
+
+ const handleStopScan = useCallback(() => {
+ console.log("⏹️ Stopping manual QR scan...");
+ setIsManualScanning(false);
+ setQrScanError(false);
+ setQrScanSuccess(false);
+ stopScan();
+ resetScan();
+ }, [stopScan, resetScan]);
+
+ const getStatusMessage = useCallback((lot: any) => {
+ switch (lot.stockOutLineStatus?.toLowerCase()) {
+ case 'pending':
+ return t("Please finish QR code scan and pick order.");
+ case 'checked':
+ return t("Please submit the pick order.");
+ case 'partially_completed':
+ return t("Partial quantity submitted. Please submit more or complete the order.");
+ case 'completed':
+ return t("Pick order completed successfully!");
+ case 'rejected':
+ return t("Lot has been rejected and marked as unavailable.");
+ case 'unavailable':
+ return t("This order is insufficient, please pick another lot.");
+ default:
+ return t("Please finish QR code scan and pick order.");
+ }
+ }, [t]);
+
+ return (
+
+
+ {/* Job Order Header */}
+ {jobOrderData && (
+
+
+
+ {t("Job Order")}: {jobOrderData.pickOrder?.jobOrder?.name || '-'}
+
+
+ {t("Pick Order Code")}: {jobOrderData.pickOrder?.code || '-'}
+
+
+ {t("Target Date")}: {jobOrderData.pickOrder?.targetDate || '-'}
+
+
+ {t("Status")}: {jobOrderData.pickOrder?.status || '-'}
+
+
+
+ )}
+
+
+ {/* Combined Lot Table */}
+
+
+
+
+
+ {!isManualScanning ? (
+ }
+ onClick={handleStartScan}
+ color="primary"
+ sx={{ minWidth: '120px' }}
+ >
+ {t("Start QR Scan")}
+
+ ) : (
+ }
+ onClick={handleStopScan}
+ color="secondary"
+ sx={{ minWidth: '120px' }}
+ >
+ {t("Stop QR Scan")}
+
+ )}
+
+ {isManualScanning && (
+
+
+
+ {t("Scanning...")}
+
+
+ )}
+
+
+
+ {qrScanError && !qrScanSuccess && (
+
+ {t("QR code does not match any item in current orders.")}
+
+ )}
+ {qrScanSuccess && (
+
+ {t("QR code verified.")}
+
+ )}
+
+
+
+
+
+ {t("Index")}
+ {t("Route")}
+ {t("Item Code")}
+ {t("Item Name")}
+ {t("Lot No")}
+ {t("Lot Required Pick Qty")}
+ {t("Scan Result")}
+ {t("Submit Required Pick Qty")}
+
+
+
+ {paginatedData.length === 0 ? (
+
+
+
+ {t("No data available")}
+
+
+
+ ) : (
+ paginatedData.map((lot, index) => (
+
+
+
+ {index + 1}
+
+
+
+
+ {lot.routerRoute || '-'}
+
+
+ {lot.itemCode}
+ {lot.itemName+'('+lot.uomDesc+')'}
+
+
+
+ {lot.lotNo}
+
+
+
+
+ {(() => {
+ const requiredQty = lot.requiredQty || 0;
+ return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
+ })()}
+
+
+
+ {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+
+
+
+ {/* ✅ QR Code Modal */}
+ {
+ setQrModalOpen(false);
+ setSelectedLotForQr(null);
+ stopScan();
+ resetScan();
+ }}
+ lot={selectedLotForQr}
+ combinedLotData={combinedLotData}
+ onQrCodeSubmit={handleQrCodeSubmitFromModal}
+ />
+
+ {/* ✅ Pick Execution Form Modal */}
+ {pickExecutionFormOpen && selectedLotForExecutionForm && (
+ {
+ setPickExecutionFormOpen(false);
+ setSelectedLotForExecutionForm(null);
+ }}
+ onSubmit={handlePickExecutionFormSubmit}
+ selectedLot={selectedLotForExecutionForm}
+ selectedPickOrderLine={{
+ id: selectedLotForExecutionForm.pickOrderLineId,
+ itemId: selectedLotForExecutionForm.itemId,
+ itemCode: selectedLotForExecutionForm.itemCode,
+ itemName: selectedLotForExecutionForm.itemName,
+ pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
+ // ✅ 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
+ }}
+ pickOrderId={selectedLotForExecutionForm.pickOrderId}
+ pickOrderCreateDate={new Date()}
+ />
+ )}
+
+ );
+};
+
+export default JobPickExecution
\ No newline at end of file
diff --git a/src/components/Jodetail/JobPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx
new file mode 100644
index 0000000..7af42b7
--- /dev/null
+++ b/src/components/Jodetail/JobPickExecutionForm.tsx
@@ -0,0 +1,383 @@
+// FPSMS-frontend/src/components/PickOrderSearch/PickExecutionForm.tsx
+"use client";
+
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ FormControl,
+ Grid,
+ InputLabel,
+ MenuItem,
+ Select,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { useCallback, useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions";
+import { fetchEscalationCombo } from "@/app/api/user/actions";
+
+interface LotPickData {
+ id: number;
+ lotId: number;
+ lotNo: string;
+ expiryDate: string;
+ location: string;
+ stockUnit: string;
+ inQty: number;
+ outQty: number;
+ holdQty: number;
+ totalPickedByAllPickOrders: number;
+ availableQty: number;
+ requiredQty: number;
+ actualPickQty: number;
+ lotStatus: string;
+ lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'|'rejected';
+ stockOutLineId?: number;
+ stockOutLineStatus?: string;
+ stockOutLineQty?: number;
+}
+
+interface PickExecutionFormProps {
+ open: boolean;
+ onClose: () => void;
+ onSubmit: (data: PickExecutionIssueData) => Promise;
+ selectedLot: LotPickData | null;
+ selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
+ pickOrderId?: number;
+ pickOrderCreateDate: any;
+ // ✅ Remove these props since we're not handling normal cases
+ // onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise;
+ // selectedRowId?: number | null;
+}
+
+// 定义错误类型
+interface FormErrors {
+ actualPickQty?: string;
+ missQty?: string;
+ badItemQty?: string;
+ issueRemark?: string;
+ handledBy?: string;
+}
+
+const PickExecutionForm: React.FC = ({
+ open,
+ onClose,
+ onSubmit,
+ selectedLot,
+ selectedPickOrderLine,
+ pickOrderId,
+ pickOrderCreateDate,
+ // ✅ Remove these props
+ // onNormalPickSubmit,
+ // selectedRowId,
+}) => {
+ const { t } = useTranslation();
+ const [formData, setFormData] = useState>({});
+ const [errors, setErrors] = useState({});
+ const [loading, setLoading] = useState(false);
+ const [handlers, setHandlers] = useState>([]);
+ const [verifiedQty, setVerifiedQty] = useState(0);
+ // 计算剩余可用数量
+ const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
+ const remainingQty = lot.inQty - lot.outQty;
+ return Math.max(0, remainingQty);
+ }, []);
+ const calculateRequiredQty = useCallback((lot: LotPickData) => {
+ // ✅ Use the original required quantity, not subtracting actualPickQty
+ // The actualPickQty in the form should be independent of the database value
+ return lot.requiredQty || 0;
+ }, []);
+
+ // 获取处理人员列表
+ useEffect(() => {
+ const fetchHandlers = async () => {
+ try {
+ const escalationCombo = await fetchEscalationCombo();
+ setHandlers(escalationCombo);
+ } catch (error) {
+ console.error("Error fetching handlers:", error);
+ }
+ };
+
+ fetchHandlers();
+ }, []);
+
+ // 初始化表单数据 - 每次打开时都重新初始化
+ useEffect(() => {
+ if (open && selectedLot && selectedPickOrderLine && pickOrderId) {
+ const getSafeDate = (dateValue: any): string => {
+ if (!dateValue) return new Date().toISOString().split('T')[0];
+ try {
+ const date = new Date(dateValue);
+ if (isNaN(date.getTime())) {
+ return new Date().toISOString().split('T')[0];
+ }
+ return date.toISOString().split('T')[0];
+ } catch {
+ return new Date().toISOString().split('T')[0];
+ }
+ };
+
+ // ✅ Initialize verified quantity to the received quantity (actualPickQty)
+ const initialVerifiedQty = selectedLot.actualPickQty || 0;
+ setVerifiedQty(initialVerifiedQty);
+
+ console.log("=== PickExecutionForm Debug ===");
+ console.log("selectedLot:", selectedLot);
+ console.log("initialVerifiedQty:", initialVerifiedQty);
+ console.log("=== End Debug ===");
+
+ setFormData({
+ pickOrderId: pickOrderId,
+ pickOrderCode: selectedPickOrderLine.pickOrderCode,
+ pickOrderCreateDate: getSafeDate(pickOrderCreateDate),
+ pickExecutionDate: new Date().toISOString().split('T')[0],
+ pickOrderLineId: selectedPickOrderLine.id,
+ itemId: selectedPickOrderLine.itemId,
+ itemCode: selectedPickOrderLine.itemCode,
+ itemDescription: selectedPickOrderLine.itemName,
+ lotId: selectedLot.lotId,
+ lotNo: selectedLot.lotNo,
+ storeLocation: selectedLot.location,
+ requiredQty: selectedLot.requiredQty,
+ actualPickQty: initialVerifiedQty, // ✅ Use the initial value
+ missQty: 0,
+ badItemQty: 0,
+ issueRemark: '',
+ pickerName: '',
+ handledBy: undefined,
+ });
+ }
+ }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate]);
+
+ const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+
+ // ✅ Update verified quantity state when actualPickQty changes
+ if (field === 'actualPickQty') {
+ setVerifiedQty(value);
+ }
+
+ // 清除错误
+ if (errors[field as keyof FormErrors]) {
+ setErrors(prev => ({ ...prev, [field]: undefined }));
+ }
+ }, [errors]);
+
+ // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0
+ const validateForm = (): boolean => {
+ const newErrors: FormErrors = {};
+
+ if (verifiedQty === undefined || verifiedQty < 0) {
+ newErrors.actualPickQty = t('Qty is required');
+ }
+
+ // ✅ Check if verified qty exceeds received qty
+ if (verifiedQty > (selectedLot?.actualPickQty || 0)) {
+ newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity');
+ }
+
+ // ✅ Check if verified qty exceeds required qty
+ if (verifiedQty > (selectedLot?.requiredQty || 0)) {
+ newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
+ }
+
+ // ✅ 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 (!validateForm() || !formData.pickOrderId) {
+ return;
+ }
+
+ setLoading(true);
+ try {
+ // ✅ Use the verified quantity in the submission
+ const submissionData = {
+ ...formData,
+ actualPickQty: verifiedQty
+ } as PickExecutionIssueData;
+
+ await onSubmit(submissionData);
+ onClose();
+ } catch (error) {
+ console.error('Error submitting pick execution issue:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleClose = () => {
+ setFormData({});
+ setErrors({});
+ setVerifiedQty(0);
+ onClose();
+ };
+
+ if (!selectedLot || !selectedPickOrderLine) {
+ return null;
+ }
+
+ const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot);
+ const requiredQty = calculateRequiredQty(selectedLot);
+
+ return (
+
+ );
+};
+
+export default PickExecutionForm;
\ No newline at end of file
diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx
new file mode 100644
index 0000000..eedc840
--- /dev/null
+++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx
@@ -0,0 +1,1295 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Stack,
+ TextField,
+ Typography,
+ Alert,
+ CircularProgress,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Checkbox,
+ TablePagination,
+ Modal,
+} from "@mui/material";
+import { useCallback, useEffect, useState, useRef, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { useRouter } from "next/navigation";
+
+// ✅ 修改:使用 Job Order API
+import {
+ fetchCompletedJobOrderPickOrders,
+ fetchUnassignedJobOrderPickOrders,
+ assignJobOrderPickOrder,
+ updateSecondQrScanStatus,
+ submitSecondScanQuantity,
+ recordSecondScanIssue
+
+} from "@/app/api/jo/actions";
+import { fetchNameList, NameList } from "@/app/api/user/actions";
+import {
+ FormProvider,
+ useForm,
+} from "react-hook-form";
+import SearchBox, { Criterion } from "../SearchBox";
+import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
+import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
+import QrCodeIcon from '@mui/icons-material/QrCode';
+import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
+import { useSession } from "next-auth/react";
+import { SessionWithTokens } from "@/config/authConfig";
+import { fetchStockInLineInfo } from "@/app/api/po/actions";
+import GoodPickExecutionForm from "./JobPickExecutionForm";
+import FGPickOrderCard from "./FGPickOrderCard";
+
+interface Props {
+ filterArgs: Record;
+}
+
+// ✅ QR Code Modal Component (from GoodPickExecution)
+const QrCodeModal: React.FC<{
+ open: boolean;
+ onClose: () => void;
+ lot: any | null;
+ onQrCodeSubmit: (lotNo: string) => void;
+ combinedLotData: any[];
+}> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
+ const { t } = useTranslation("jo");
+ const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
+ const [manualInput, setManualInput] = useState('');
+
+ const [manualInputSubmitted, setManualInputSubmitted] = useState(false);
+ const [manualInputError, setManualInputError] = useState(false);
+ const [isProcessingQr, setIsProcessingQr] = useState(false);
+ const [qrScanFailed, setQrScanFailed] = useState(false);
+ const [qrScanSuccess, setQrScanSuccess] = useState(false);
+
+ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set());
+ const [scannedQrResult, setScannedQrResult] = useState('');
+
+ // Process scanned QR codes
+ useEffect(() => {
+ if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
+ const latestQr = qrValues[qrValues.length - 1];
+
+ if (processedQrCodes.has(latestQr)) {
+ console.log("QR code already processed, skipping...");
+ return;
+ }
+
+ setProcessedQrCodes(prev => new Set(prev).add(latestQr));
+
+ try {
+ const qrData = JSON.parse(latestQr);
+
+ if (qrData.stockInLineId && qrData.itemId) {
+ setIsProcessingQr(true);
+ setQrScanFailed(false);
+
+ fetchStockInLineInfo(qrData.stockInLineId)
+ .then((stockInLineInfo) => {
+ console.log("Stock in line info:", stockInLineInfo);
+ setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
+
+ if (stockInLineInfo.lotNo === lot.lotNo) {
+ console.log(`✅ QR Code verified for lot: ${lot.lotNo}`);
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ resetScan();
+ } else {
+ console.log(`❌ QR Code mismatch. Expected: ${lot.lotNo}, Got: ${stockInLineInfo.lotNo}`);
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ }
+ })
+ .catch((error) => {
+ console.error("Error fetching stock in line info:", error);
+ setScannedQrResult('Error fetching data');
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ })
+ .finally(() => {
+ setIsProcessingQr(false);
+ });
+ } else {
+ const qrContent = latestQr.replace(/[{}]/g, '');
+ setScannedQrResult(qrContent);
+
+ if (qrContent === lot.lotNo) {
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ resetScan();
+ } else {
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ }
+ }
+ } catch (error) {
+ console.log("QR code is not JSON format, trying direct comparison");
+ const qrContent = latestQr.replace(/[{}]/g, '');
+ setScannedQrResult(qrContent);
+
+ if (qrContent === lot.lotNo) {
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ resetScan();
+ } else {
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ }
+ }
+ }
+ }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes]);
+
+ // Clear states when modal opens
+ useEffect(() => {
+ if (open) {
+ setManualInput('');
+ setManualInputSubmitted(false);
+ setManualInputError(false);
+ setIsProcessingQr(false);
+ setQrScanFailed(false);
+ setQrScanSuccess(false);
+ setScannedQrResult('');
+ setProcessedQrCodes(new Set());
+ }
+ }, [open]);
+
+ useEffect(() => {
+ if (lot) {
+ setManualInput('');
+ setManualInputSubmitted(false);
+ setManualInputError(false);
+ setIsProcessingQr(false);
+ setQrScanFailed(false);
+ setQrScanSuccess(false);
+ setScannedQrResult('');
+ setProcessedQrCodes(new Set());
+ }
+ }, [lot]);
+
+ // Auto-submit manual input when it matches
+ useEffect(() => {
+ if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
+ console.log(' Auto-submitting manual input:', manualInput.trim());
+
+ const timer = setTimeout(() => {
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ setManualInput('');
+ setManualInputError(false);
+ setManualInputSubmitted(false);
+ }, 200);
+
+ return () => clearTimeout(timer);
+ }
+ }, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);
+
+ const handleManualSubmit = () => {
+ if (manualInput.trim() === lot?.lotNo) {
+ setQrScanSuccess(true);
+ onQrCodeSubmit(lot.lotNo);
+ onClose();
+ setManualInput('');
+ } else {
+ setQrScanFailed(true);
+ setManualInputError(true);
+ setManualInputSubmitted(true);
+ }
+ };
+
+ useEffect(() => {
+ if (open) {
+ startScan();
+ }
+ }, [open, startScan]);
+
+ return (
+
+
+
+ {t("QR Code Scan for Lot")}: {lot?.lotNo}
+
+
+ {isProcessingQr && (
+
+
+ {t("Processing QR code...")}
+
+
+ )}
+
+
+
+ {t("Manual Input")}:
+
+ {
+ setManualInput(e.target.value);
+ if (qrScanFailed || manualInputError) {
+ setQrScanFailed(false);
+ setManualInputError(false);
+ setManualInputSubmitted(false);
+ }
+ }}
+ sx={{ mb: 1 }}
+ error={manualInputSubmitted && manualInputError}
+ helperText={
+ manualInputSubmitted && manualInputError
+ ? `${t("The input is not the same as the expected lot number.")}`
+ : ''
+ }
+ />
+
+
+
+ {qrValues.length > 0 && (
+
+
+ {t("QR Scan Result:")} {scannedQrResult}
+
+
+ {qrScanSuccess && (
+
+ ✅ {t("Verified successfully!")}
+
+ )}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+const JobPickExecution: React.FC = ({ filterArgs }) => {
+ const { t } = useTranslation("jo");
+ const router = useRouter();
+ const { data: session } = useSession() as { data: SessionWithTokens | null };
+
+ const currentUserId = session?.id ? parseInt(session.id) : undefined;
+
+ // ✅ 修改:使用 Job Order 数据结构
+ const [jobOrderData, setJobOrderData] = useState(null);
+ const [combinedLotData, setCombinedLotData] = useState([]);
+ const [combinedDataLoading, setCombinedDataLoading] = useState(false);
+ const [originalCombinedData, setOriginalCombinedData] = useState([]);
+
+ // ✅ 添加未分配订单状态
+ const [unassignedOrders, setUnassignedOrders] = useState([]);
+ const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
+
+ const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
+
+ const [qrScanInput, setQrScanInput] = useState('');
+ const [qrScanError, setQrScanError] = useState(false);
+ const [qrScanSuccess, setQrScanSuccess] = useState(false);
+
+ const [pickQtyData, setPickQtyData] = useState>({});
+ const [searchQuery, setSearchQuery] = useState>({});
+
+ const [paginationController, setPaginationController] = useState({
+ pageNum: 0,
+ pageSize: 10,
+ });
+
+ const [usernameList, setUsernameList] = useState([]);
+
+ const initializationRef = useRef(false);
+ const autoAssignRef = useRef(false);
+
+ const formProps = useForm();
+ const errors = formProps.formState.errors;
+
+ // ✅ Add QR modal states
+ const [qrModalOpen, setQrModalOpen] = useState(false);
+ const [selectedLotForQr, setSelectedLotForQr] = useState(null);
+
+ // ✅ Add GoodPickExecutionForm states
+ const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
+ const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState(null);
+ // ✅ Add these missing state variables
+ const [isManualScanning, setIsManualScanning] = useState(false);
+ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set());
+ const [lastProcessedQr, setLastProcessedQr] = useState('');
+ const [isRefreshingData, setIsRefreshingData] = useState(false);
+
+ // ✅ 修改:加载未分配的 Job Order 订单
+ const loadUnassignedOrders = useCallback(async () => {
+ setIsLoadingUnassigned(true);
+ try {
+ const orders = await fetchUnassignedJobOrderPickOrders();
+ setUnassignedOrders(orders);
+ } catch (error) {
+ console.error("Error loading unassigned orders:", error);
+ } finally {
+ setIsLoadingUnassigned(false);
+ }
+ }, []);
+
+ // ✅ 修改:分配订单给当前用户
+ const handleAssignOrder = useCallback(async (pickOrderId: number) => {
+ if (!currentUserId) {
+ console.error("Missing user id in session");
+ return;
+ }
+
+ try {
+ const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
+ if (result.message === "Successfully assigned") {
+ console.log("✅ Successfully assigned pick order");
+ // 刷新数据
+ window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
+ // 重新加载未分配订单列表
+ loadUnassignedOrders();
+ } else {
+ console.warn("⚠️ Assignment failed:", result.message);
+ alert(`Assignment failed: ${result.message}`);
+ }
+ } catch (error) {
+ console.error("❌ Error assigning order:", error);
+ alert("Error occurred during assignment");
+ }
+ }, [currentUserId, loadUnassignedOrders]);
+
+
+ // ✅ Handle QR code button click
+ const handleQrCodeClick = (pickOrderId: number) => {
+ console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
+ // TODO: Implement QR code functionality
+ };
+
+ // ✅ 修改:使用 Job Order API 获取数据
+ const fetchJobOrderData = useCallback(async (userId?: number) => {
+ setCombinedDataLoading(true);
+ try {
+ const userIdToUse = userId || currentUserId;
+
+ console.log(" fetchJobOrderData called with userId:", userIdToUse);
+
+ if (!userIdToUse) {
+ console.warn("⚠️ No userId available, skipping API call");
+ setJobOrderData(null);
+ setCombinedLotData([]);
+ setOriginalCombinedData([]);
+ return;
+ }
+
+ // ✅ 使用 Job Order API
+ const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse);
+ console.log("✅ Job Order data:", jobOrderData);
+
+ setJobOrderData(jobOrderData);
+
+ // ✅ Transform hierarchical data to flat structure for the table
+ const flatLotData: any[] = [];
+
+ if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
+ jobOrderData.pickOrderLines.forEach((line: any) => {
+ if (line.lots && line.lots.length > 0) {
+ line.lots.forEach((lot: any) => {
+ flatLotData.push({
+ // Pick order info
+ pickOrderId: jobOrderData.pickOrder.id,
+ pickOrderCode: jobOrderData.pickOrder.code,
+ pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
+ pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
+ pickOrderType: jobOrderData.pickOrder.type,
+ pickOrderStatus: jobOrderData.pickOrder.status,
+ pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
+
+ // Pick order line info
+ pickOrderLineId: line.id,
+ pickOrderLineRequiredQty: line.requiredQty,
+ pickOrderLineStatus: line.status,
+
+ // Item info
+ itemId: line.itemId,
+ itemCode: line.itemCode,
+ itemName: line.itemName,
+ uomCode: line.uomCode,
+ uomDesc: line.uomDesc,
+
+ // Lot info
+ lotId: lot.lotId,
+ lotNo: lot.lotNo,
+ expiryDate: lot.expiryDate,
+ location: lot.location,
+ availableQty: lot.availableQty,
+ requiredQty: lot.requiredQty,
+ actualPickQty: lot.actualPickQty,
+ lotStatus: lot.lotStatus,
+ lotAvailability: lot.lotAvailability,
+ processingStatus: lot.processingStatus,
+ stockOutLineId: lot.stockOutLineId,
+ stockOutLineStatus: lot.stockOutLineStatus,
+ stockOutLineQty: lot.stockOutLineQty,
+
+ // Router info
+ routerIndex: lot.routerIndex,
+ secondQrScanStatus: lot.secondQrScanStatus,
+ routerArea: lot.routerArea,
+ routerRoute: lot.routerRoute,
+ uomShortDesc: lot.uomShortDesc
+ });
+ });
+ }
+ });
+ }
+
+ console.log("✅ Transformed flat lot data:", flatLotData);
+ setCombinedLotData(flatLotData);
+ setOriginalCombinedData(flatLotData);
+
+ // ✅ 计算完成状态并发送事件
+ const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
+ lot.processingStatus === 'completed'
+ );
+
+ // ✅ 发送完成状态事件,包含标签页信息
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: allCompleted,
+ tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件
+ }
+ }));
+
+ } catch (error) {
+ console.error("❌ Error fetching job order data:", error);
+ setJobOrderData(null);
+ setCombinedLotData([]);
+ setOriginalCombinedData([]);
+
+ // ✅ 如果加载失败,禁用打印按钮
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: false,
+ tabIndex: 0
+ }
+ }));
+ } finally {
+ setCombinedDataLoading(false);
+ }
+ }, [currentUserId]);
+
+ // ✅ 修改:初始化时加载数据
+ useEffect(() => {
+ if (session && currentUserId && !initializationRef.current) {
+ console.log("✅ Session loaded, initializing job order...");
+ initializationRef.current = true;
+
+ // 加载 Job Order 数据
+ fetchJobOrderData();
+ // 加载未分配订单
+ loadUnassignedOrders();
+ }
+ }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);
+
+ // ✅ Add event listener for manual assignment
+ useEffect(() => {
+ const handlePickOrderAssigned = () => {
+ console.log("🔄 Pick order assigned event received, refreshing data...");
+ fetchJobOrderData();
+ };
+
+ window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
+
+ return () => {
+ window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
+ };
+ }, [fetchJobOrderData]);
+
+ // ✅ Handle QR code submission for matched lot (external scanning)
+ const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
+ console.log(`✅ Processing Second QR Code for lot: ${lotNo}`);
+
+ // ✅ Check if this lot was already processed recently
+ const lotKey = `${lotNo}_${Date.now()}`;
+ if (processedQrCodes.has(lotNo)) {
+ console.log(`⏭️ Lot ${lotNo} already processed, skipping...`);
+ return;
+ }
+
+ const currentLotData = combinedLotData;
+ const matchingLots = currentLotData.filter(lot =>
+ lot.lotNo === lotNo ||
+ lot.lotNo?.toLowerCase() === lotNo.toLowerCase()
+ );
+
+ if (matchingLots.length === 0) {
+ console.error(`❌ Lot not found: ${lotNo}`);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ return;
+ }
+
+ try {
+ let successCount = 0;
+
+ for (const matchingLot of matchingLots) {
+ // ✅ Check if this specific item was already processed
+ const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`;
+ if (processedQrCodes.has(itemKey)) {
+ console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`);
+ continue;
+ }
+
+ // ✅ Use the new second scan API
+ const result = await updateSecondQrScanStatus(
+ matchingLot.pickOrderId,
+ matchingLot.itemId
+ );
+
+ if (result.code === "SUCCESS") {
+ successCount++;
+ // ✅ Mark this item as processed
+ setProcessedQrCodes(prev => new Set(prev).add(itemKey));
+ console.log(`✅ Second QR scan status updated for item ${matchingLot.itemId}`);
+ } else {
+ console.error(`❌ Failed to update second QR scan status: ${result.message}`);
+ }
+ }
+
+ if (successCount > 0) {
+ setQrScanSuccess(true);
+ setQrScanError(false);
+ await fetchJobOrderData(); // Refresh data
+ } else {
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ }
+ } catch (error) {
+ console.error("❌ Error processing second QR code:", error);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ }
+ }, [combinedLotData, fetchJobOrderData, processedQrCodes]);
+
+ useEffect(() => {
+ if (qrValues.length > 0 && combinedLotData.length > 0) {
+ const latestQr = qrValues[qrValues.length - 1];
+
+ // ✅ Check if this QR was already processed recently
+ if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
+ console.log("⏭️ QR code already processed, skipping...");
+ return;
+ }
+
+ // ✅ Mark as processed
+ setProcessedQrCodes(prev => new Set(prev).add(latestQr));
+ setLastProcessedQr(latestQr);
+
+ // Extract lot number from QR code
+ let lotNo = '';
+ try {
+ const qrData = JSON.parse(latestQr);
+ if (qrData.stockInLineId && qrData.itemId) {
+ // For JSON QR codes, we need to fetch the lot number
+ fetchStockInLineInfo(qrData.stockInLineId)
+ .then((stockInLineInfo) => {
+ console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
+ const extractedLotNo = stockInLineInfo.lotNo;
+ if (extractedLotNo) {
+ console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
+ handleQrCodeSubmit(extractedLotNo);
+ }
+ })
+ .catch((error) => {
+ console.error("Outside QR scan - Error fetching stock in line info:", error);
+ });
+ return; // Exit early for JSON QR codes
+ }
+ } catch (error) {
+ // Not JSON format, treat as direct lot number
+ lotNo = latestQr.replace(/[{}]/g, '');
+ }
+
+ // For direct lot number QR codes
+ if (lotNo) {
+ console.log(`Outside QR scan detected (direct): ${lotNo}`);
+ handleQrCodeSubmit(lotNo);
+ }
+ }
+ }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr]);
+ const handleManualInputSubmit = useCallback(() => {
+ if (qrScanInput.trim() !== '') {
+ handleQrCodeSubmit(qrScanInput.trim());
+ }
+ }, [qrScanInput, handleQrCodeSubmit]);
+
+ // ✅ Handle QR code submission from modal (internal scanning)
+ const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
+ if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
+ console.log(`✅ QR Code verified for lot: ${lotNo}`);
+
+ const requiredQty = selectedLotForQr.requiredQty;
+ const lotId = selectedLotForQr.lotId;
+
+ // Create stock out line
+ const stockOutLineData: CreateStockOutLine = {
+ consoCode: selectedLotForQr.pickOrderConsoCode,
+ pickOrderLineId: selectedLotForQr.pickOrderLineId,
+ inventoryLotLineId: selectedLotForQr.lotId,
+ qty: 0.0
+ };
+
+ try {
+
+
+
+ // Close modal
+ setQrModalOpen(false);
+ setSelectedLotForQr(null);
+
+ // Set pick quantity
+ const lotKey = `${selectedLotForQr.pickOrderLineId}-${lotId}`;
+ setTimeout(() => {
+ setPickQtyData(prev => ({
+ ...prev,
+ [lotKey]: requiredQty
+ }));
+ console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
+ }, 500);
+
+ // Refresh data
+ await fetchJobOrderData();
+ } catch (error) {
+ console.error("Error creating stock out line:", error);
+ }
+ }
+ }, [selectedLotForQr, fetchJobOrderData]);
+
+ // ✅ Outside QR scanning - process QR codes from outside the page automatically
+ useEffect(() => {
+ if (qrValues.length > 0 && combinedLotData.length > 0) {
+ const latestQr = qrValues[qrValues.length - 1];
+
+ // Extract lot number from QR code
+ let lotNo = '';
+ try {
+ const qrData = JSON.parse(latestQr);
+ if (qrData.stockInLineId && qrData.itemId) {
+ // For JSON QR codes, we need to fetch the lot number
+ fetchStockInLineInfo(qrData.stockInLineId)
+ .then((stockInLineInfo) => {
+ console.log("Outside QR scan - Stock in line info:", stockInLineInfo);
+ const extractedLotNo = stockInLineInfo.lotNo;
+ if (extractedLotNo) {
+ console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`);
+ handleQrCodeSubmit(extractedLotNo);
+ }
+ })
+ .catch((error) => {
+ console.error("Outside QR scan - Error fetching stock in line info:", error);
+ });
+ return; // Exit early for JSON QR codes
+ }
+ } catch (error) {
+ // Not JSON format, treat as direct lot number
+ lotNo = latestQr.replace(/[{}]/g, '');
+ }
+
+ // For direct lot number QR codes
+ if (lotNo) {
+ console.log(`Outside QR scan detected (direct): ${lotNo}`);
+ handleQrCodeSubmit(lotNo);
+ }
+ }
+ }, [qrValues, combinedLotData, handleQrCodeSubmit]);
+
+ const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
+ if (value === '' || value === null || value === undefined) {
+ setPickQtyData(prev => ({
+ ...prev,
+ [lotKey]: 0
+ }));
+ return;
+ }
+
+ const numericValue = typeof value === 'string' ? parseFloat(value) : value;
+
+ if (isNaN(numericValue)) {
+ setPickQtyData(prev => ({
+ ...prev,
+ [lotKey]: 0
+ }));
+ return;
+ }
+
+ setPickQtyData(prev => ({
+ ...prev,
+ [lotKey]: numericValue
+ }));
+ }, []);
+
+ const [autoAssignStatus, setAutoAssignStatus] = useState<'idle' | 'checking' | 'assigned' | 'no_orders'>('idle');
+ const [autoAssignMessage, setAutoAssignMessage] = useState('');
+
+
+
+
+ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
+ try {
+ // ✅ Use the new second scan submit API
+ const result = await submitSecondScanQuantity(
+ lot.pickOrderId,
+ lot.itemId,
+ {
+ qty: submitQty,
+ isMissing: false,
+ isBad: false,
+ reason: undefined // ✅ Fix TypeScript error
+ }
+ );
+
+
+ if (result.code === "SUCCESS") {
+ console.log(`✅ Second scan quantity submitted: ${submitQty}`);
+ await fetchJobOrderData(); // Refresh data
+ } else {
+ console.error(`❌ Failed to submit second scan quantity: ${result.message}`);
+ }
+ } catch (error) {
+ console.error("Error submitting second scan quantity:", error);
+ }
+ }, [fetchJobOrderData]);
+ // ✅ Handle reject lot
+
+ // ✅ Handle pick execution form
+ const handlePickExecutionForm = useCallback((lot: any) => {
+ console.log("=== Pick Execution Form ===");
+ console.log("Lot data:", lot);
+
+ if (!lot) {
+ console.warn("No lot data provided for pick execution form");
+ return;
+ }
+
+ console.log("Opening pick execution form for lot:", lot.lotNo);
+
+ setSelectedLotForExecutionForm(lot);
+ setPickExecutionFormOpen(true);
+
+ console.log("Pick execution form opened for lot ID:", lot.lotId);
+ }, []);
+
+ const handlePickExecutionFormSubmit = useCallback(async (data: any) => {
+ try {
+ console.log("Pick execution form submitted:", data);
+ if (!currentUserId) {
+ console.error("❌ No current user ID available");
+ return;
+ }
+ const result = await recordSecondScanIssue(
+ selectedLotForExecutionForm.pickOrderId,
+ selectedLotForExecutionForm.itemId,
+ {
+ qty: data.actualPickQty,
+ isMissing: data.missQty > 0,
+ isBad: data.badItemQty > 0,
+ reason: data.issueRemark || '',
+ createdBy: currentUserId
+ }
+ );
+ console.log("Pick execution issue recorded:", result);
+
+ if (result && result.code === "SUCCESS") {
+ console.log("✅ Pick execution issue recorded successfully");
+ } else {
+ console.error("❌ Failed to record pick execution issue:", result);
+ }
+
+ setPickExecutionFormOpen(false);
+ setSelectedLotForExecutionForm(null);
+
+ await fetchJobOrderData();
+ } catch (error) {
+ console.error("Error submitting pick execution form:", error);
+ }
+ }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]);
+
+ // ✅ Calculate remaining required quantity
+ const calculateRemainingRequiredQty = useCallback((lot: any) => {
+ const requiredQty = lot.requiredQty || 0;
+ const stockOutLineQty = lot.stockOutLineQty || 0;
+ return Math.max(0, requiredQty - stockOutLineQty);
+ }, []);
+
+ // Search criteria
+ const searchCriteria: Criterion[] = [
+ {
+ label: t("Pick Order Code"),
+ paramName: "pickOrderCode",
+ type: "text",
+ },
+ {
+ label: t("Item Code"),
+ paramName: "itemCode",
+ type: "text",
+ },
+ {
+ label: t("Item Name"),
+ paramName: "itemName",
+ type: "text",
+ },
+ {
+ label: t("Lot No"),
+ paramName: "lotNo",
+ type: "text",
+ },
+ ];
+
+ const handleSearch = useCallback((query: Record) => {
+ setSearchQuery({ ...query });
+ console.log("Search query:", query);
+
+ if (!originalCombinedData) return;
+
+ const filtered = originalCombinedData.filter((lot: any) => {
+ const pickOrderCodeMatch = !query.pickOrderCode ||
+ lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
+
+ const itemCodeMatch = !query.itemCode ||
+ lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
+
+ const itemNameMatch = !query.itemName ||
+ lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
+
+ const lotNoMatch = !query.lotNo ||
+ lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
+
+ return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
+ });
+
+ setCombinedLotData(filtered);
+ console.log("Filtered lots count:", filtered.length);
+ }, [originalCombinedData]);
+
+ const handleReset = useCallback(() => {
+ setSearchQuery({});
+ if (originalCombinedData) {
+ setCombinedLotData(originalCombinedData);
+ }
+ }, [originalCombinedData]);
+
+ const handlePageChange = useCallback((event: unknown, newPage: number) => {
+ setPaginationController(prev => ({
+ ...prev,
+ pageNum: newPage,
+ }));
+ }, []);
+
+ const handlePageSizeChange = useCallback((event: React.ChangeEvent) => {
+ const newPageSize = parseInt(event.target.value, 10);
+ setPaginationController({
+ pageNum: 0,
+ pageSize: newPageSize,
+ });
+ }, []);
+
+ // Pagination data with sorting by routerIndex
+const paginatedData = useMemo(() => {
+ // ✅ Sort by routerIndex first, then by other criteria
+ const sortedData = [...combinedLotData].sort((a, b) => {
+ const aIndex = a.routerIndex || 0;
+ const bIndex = b.routerIndex || 0;
+
+ // Primary sort: by routerIndex
+ if (aIndex !== bIndex) {
+ return aIndex - bIndex;
+ }
+
+ // Secondary sort: by pickOrderCode if routerIndex is the same
+ if (a.pickOrderCode !== b.pickOrderCode) {
+ return a.pickOrderCode.localeCompare(b.pickOrderCode);
+ }
+
+ // Tertiary sort: by lotNo if everything else is the same
+ return (a.lotNo || '').localeCompare(b.lotNo || '');
+ });
+
+ const startIndex = paginationController.pageNum * paginationController.pageSize;
+ const endIndex = startIndex + paginationController.pageSize;
+ return sortedData.slice(startIndex, endIndex);
+ }, [combinedLotData, paginationController]);
+
+ // ✅ Add these functions for manual scanning
+ const handleStartScan = useCallback(() => {
+ console.log(" Starting manual QR scan...");
+ setIsManualScanning(true);
+ setProcessedQrCodes(new Set());
+ setLastProcessedQr('');
+ setQrScanError(false);
+ setQrScanSuccess(false);
+ startScan();
+ }, [startScan]);
+
+ const handleStopScan = useCallback(() => {
+ console.log("⏹️ Stopping manual QR scan...");
+ setIsManualScanning(false);
+ setQrScanError(false);
+ setQrScanSuccess(false);
+ stopScan();
+ resetScan();
+ }, [stopScan, resetScan]);
+
+ const getStatusMessage = useCallback((lot: any) => {
+ switch (lot.stockOutLineStatus?.toLowerCase()) {
+ case 'pending':
+ return t("Please finish QR code scan and pick order.");
+ case 'checked':
+ return t("Please submit the pick order.");
+ case 'partially_completed':
+ return t("Partial quantity submitted. Please submit more or complete the order.");
+ case 'completed':
+ return t("Pick order completed successfully!");
+ case 'rejected':
+ return t("Lot has been rejected and marked as unavailable.");
+ case 'unavailable':
+ return t("This order is insufficient, please pick another lot.");
+ default:
+ return t("Please finish QR code scan and pick order.");
+ }
+ }, [t]);
+
+ return (
+
+
+ {/* Job Order Header */}
+ {jobOrderData && (
+
+
+
+ {t("Job Order")}: {jobOrderData.pickOrder?.jobOrder?.name || '-'}
+
+
+ {t("Pick Order Code")}: {jobOrderData.pickOrder?.code || '-'}
+
+
+ {t("Target Date")}: {jobOrderData.pickOrder?.targetDate || '-'}
+
+
+ {t("Status")}: {jobOrderData.pickOrder?.status || '-'}
+
+
+
+ )}
+
+
+
+ {/* Combined Lot Table */}
+
+
+
+
+
+ {!isManualScanning ? (
+ }
+ onClick={handleStartScan}
+ color="primary"
+ sx={{ minWidth: '120px' }}
+ >
+ {t("Start QR Scan")}
+
+ ) : (
+ }
+ onClick={handleStopScan}
+ color="secondary"
+ sx={{ minWidth: '120px' }}
+ >
+ {t("Stop QR Scan")}
+
+ )}
+
+ {isManualScanning && (
+
+
+
+ {t("Scanning...")}
+
+
+ )}
+
+
+
+ {qrScanError && !qrScanSuccess && (
+
+ {t("QR code does not match any item in current orders.")}
+
+ )}
+ {qrScanSuccess && (
+
+ {t("QR code verified.")}
+
+ )}
+
+
+
+
+
+ {t("Index")}
+ {t("Route")}
+ {t("Item Code")}
+ {t("Item Name")}
+ {t("Lot No")}
+ {t("Lot Required Pick Qty")}
+ {t("Scan Result")}
+ {t("Submit Required Pick Qty")}
+
+
+
+ {paginatedData.length === 0 ? (
+
+
+
+ {t("No data available")}
+
+
+
+ ) : (
+ paginatedData.map((lot, index) => (
+
+
+
+ {index + 1}
+
+
+
+
+ {lot.routerRoute || '-'}
+
+
+ {lot.itemCode}
+ {lot.itemName+'('+lot.uomDesc+')'}
+
+
+
+ {lot.lotNo}
+
+
+
+
+ {(() => {
+ const requiredQty = lot.requiredQty || 0;
+ return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')';
+ })()}
+
+
+
+ {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+
+
+
+ {/* ✅ QR Code Modal */}
+ {
+ setQrModalOpen(false);
+ setSelectedLotForQr(null);
+ stopScan();
+ resetScan();
+ }}
+ lot={selectedLotForQr}
+ combinedLotData={combinedLotData}
+ onQrCodeSubmit={handleQrCodeSubmitFromModal}
+ />
+
+ {/* ✅ Pick Execution Form Modal */}
+ {pickExecutionFormOpen && selectedLotForExecutionForm && (
+ {
+ setPickExecutionFormOpen(false);
+ setSelectedLotForExecutionForm(null);
+ }}
+ onSubmit={handlePickExecutionFormSubmit}
+ selectedLot={selectedLotForExecutionForm}
+ selectedPickOrderLine={{
+ id: selectedLotForExecutionForm.pickOrderLineId,
+ itemId: selectedLotForExecutionForm.itemId,
+ itemCode: selectedLotForExecutionForm.itemCode,
+ itemName: selectedLotForExecutionForm.itemName,
+ pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
+ // ✅ 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
+ }}
+ pickOrderId={selectedLotForExecutionForm.pickOrderId}
+ pickOrderCreateDate={new Date()}
+ />
+ )}
+
+ );
+};
+
+export default JobPickExecution
\ No newline at end of file
diff --git a/src/components/Jodetail/Jobcreatitem.tsx b/src/components/Jodetail/Jobcreatitem.tsx
new file mode 100644
index 0000000..9231102
--- /dev/null
+++ b/src/components/Jodetail/Jobcreatitem.tsx
@@ -0,0 +1,1824 @@
+"use client";
+
+import { createPickOrder, SavePickOrderRequest, SavePickOrderLineRequest, getLatestGroupNameAndCreate, createOrUpdateGroups } from "@/app/api/pickOrder/actions";
+import {
+ Autocomplete,
+ Box,
+ Button,
+ FormControl,
+ Grid,
+ Stack,
+ TextField,
+ Typography,
+ Checkbox,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Select,
+ MenuItem,
+ Modal,
+ Card,
+ CardContent,
+ TablePagination,
+} from "@mui/material";
+import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
+import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
+import dayjs from "dayjs";
+import { Check, Search, RestartAlt } from "@mui/icons-material";
+import { ItemCombo, fetchAllItemsInClient } from "@/app/api/settings/item/actions";
+import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
+import SearchResults, { Column } from "../SearchResults/SearchResults";
+import { fetchJobOrderDetailByCode } from "@/app/api/jo/actions";
+import SearchBox, { Criterion } from "../SearchBox";
+
+type Props = {
+ filterArgs?: Record;
+ searchQuery?: Record;
+ onPickOrderCreated?: () => void; // 添加回调函数
+};
+
+// 扩展表单类型以包含搜索字段
+interface SearchFormData extends SavePickOrderRequest {
+ searchCode?: string;
+ searchName?: string;
+}
+
+// Update the CreatedItem interface to allow null values for groupId
+interface CreatedItem {
+ itemId: number;
+ itemName: string;
+ itemCode: string;
+ qty: number;
+ uom: string;
+ uomId: number;
+ uomDesc: string;
+ isSelected: boolean;
+ currentStockBalance?: number;
+ targetDate?: string | null; // Make it optional to match the source
+ groupId?: number | null; // Allow null values
+}
+
+// Add interface for search items with quantity
+interface SearchItemWithQty extends ItemCombo {
+ qty: number | null; // Changed from number to number | null
+ jobOrderCode?: string;
+ jobOrderId?: number;
+ currentStockBalance?: number;
+ targetDate?: string | null; // Allow null values
+ groupId?: number | null; // Allow null values
+}
+interface JobOrderDetailPickLine {
+ id: number;
+ code: string;
+ name: string;
+ lotNo: string | null;
+ reqQty: number;
+ uom: string;
+ status: string;
+}
+
+// 添加组相关的接口
+interface Group {
+ id: number;
+ name: string;
+ targetDate: string;
+}
+
+const JobCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCreated }) => {
+ const { t } = useTranslation("pickOrder");
+ const [items, setItems] = useState([]);
+ const [filteredItems, setFilteredItems] = useState([]);
+ const [createdItems, setCreatedItems] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [hasSearched, setHasSearched] = useState(false);
+
+ // 添加组相关的状态 - 只声明一次
+ const [groups, setGroups] = useState([]);
+ const [selectedGroup, setSelectedGroup] = useState(null);
+ const [nextGroupNumber, setNextGroupNumber] = useState(1);
+
+ // Add state for selected item IDs in search results
+ const [selectedSearchItemIds, setSelectedSearchItemIds] = useState<(string | number)[]>([]);
+
+ // Add state for second search
+ const [secondSearchQuery, setSecondSearchQuery] = useState>({});
+ const [secondSearchResults, setSecondSearchResults] = useState([]);
+ const [isLoadingSecondSearch, setIsLoadingSecondSearch] = useState(false);
+ const [hasSearchedSecond, setHasSearchedSecond] = useState(false);
+
+ // Add selection state for second search
+ const [selectedSecondSearchItemIds, setSelectedSecondSearchItemIds] = useState<(string | number)[]>([]);
+
+ const formProps = useForm();
+ const errors = formProps.formState.errors;
+ const targetDate = formProps.watch("targetDate");
+ const type = formProps.watch("type");
+ const searchCode = formProps.watch("searchCode");
+ const searchName = formProps.watch("searchName");
+ const [jobOrderItems, setJobOrderItems] = useState([]);
+ const [isLoadingJobOrder, setIsLoadingJobOrder] = useState(false);
+
+ useEffect(() => {
+ const loadItems = async () => {
+ try {
+ const itemsData = await fetchAllItemsInClient();
+ console.log("Loaded items:", itemsData);
+ setItems(itemsData);
+ setFilteredItems([]);
+ } catch (error) {
+ console.error("Error loading items:", error);
+ }
+ };
+
+ loadItems();
+ }, []);
+ const searchJobOrderItems = useCallback(async (jobOrderCode: string) => {
+ if (!jobOrderCode.trim()) return;
+
+ setIsLoadingJobOrder(true);
+ try {
+ const jobOrderDetail = await fetchJobOrderDetailByCode(jobOrderCode);
+ setJobOrderItems(jobOrderDetail.pickLines || []);
+
+ // Fix the Job Order conversion - add missing uomDesc
+ const convertedItems = (jobOrderDetail.pickLines || []).map(item => ({
+ id: item.id,
+ label: item.name,
+ qty: item.reqQty,
+ uom: item.uom,
+ uomId: 0,
+ uomDesc: item.uomDesc, // Add missing uomDesc
+ jobOrderCode: jobOrderDetail.code,
+ jobOrderId: jobOrderDetail.id,
+ }));
+
+ setFilteredItems(convertedItems);
+ setHasSearched(true);
+ } catch (error) {
+ console.error("Error fetching Job Order items:", error);
+ alert(t("Job Order not found or has no items"));
+ } finally {
+ setIsLoadingJobOrder(false);
+ }
+ }, [t]);
+
+ // Update useEffect to handle Job Order search
+ useEffect(() => {
+ if (searchQuery && searchQuery.jobOrderCode) {
+ searchJobOrderItems(searchQuery.jobOrderCode);
+ } else if (searchQuery && items.length > 0) {
+ // Existing item search logic
+ // ... your existing search logic
+ }
+ }, [searchQuery, items, searchJobOrderItems]);
+ useEffect(() => {
+ if (searchQuery) {
+ if (searchQuery.type) {
+ formProps.setValue("type", searchQuery.type);
+ }
+
+ if (searchQuery.targetDate) {
+ formProps.setValue("targetDate", searchQuery.targetDate);
+ }
+
+ if (searchQuery.code) {
+ formProps.setValue("searchCode", searchQuery.code);
+ }
+
+ if (searchQuery.items) {
+ formProps.setValue("searchName", searchQuery.items);
+ }
+ }
+ }, [searchQuery, formProps]);
+
+ useEffect(() => {
+ setFilteredItems([]);
+ setHasSearched(false);
+ }, []);
+
+ const typeList = [
+ { type: "Consumable" },
+ { type: "Material" },
+ { type: "Product" }
+ ];
+
+ const handleTypeChange = useCallback(
+ (event: React.SyntheticEvent, newValue: {type: string} | null) => {
+ formProps.setValue("type", newValue?.type || "");
+ },
+ [formProps],
+ );
+
+ const handleSearch = useCallback(() => {
+ if (!type) {
+ alert(t("Please select type"));
+ return;
+ }
+
+ if (!searchCode && !searchName) {
+ alert(t("Please enter at least code or name"));
+ return;
+ }
+
+ setIsLoading(true);
+ setHasSearched(true);
+
+ console.log("Searching with:", { type, searchCode, searchName, targetDate, itemsCount: items.length });
+
+ setTimeout(() => {
+ let filtered = items;
+
+ if (searchCode && searchCode.trim()) {
+ filtered = filtered.filter(item =>
+ item.label.toLowerCase().includes(searchCode.toLowerCase())
+ );
+ console.log("After code filter:", filtered.length);
+ }
+
+ if (searchName && searchName.trim()) {
+ filtered = filtered.filter(item =>
+ item.label.toLowerCase().includes(searchName.toLowerCase())
+ );
+ console.log("After name filter:", filtered.length);
+ }
+
+ // Convert to SearchItemWithQty with default qty = null and include targetDate
+ const filteredWithQty = filtered.slice(0, 100).map(item => ({
+ ...item,
+ qty: null,
+ targetDate: targetDate, // Add target date to each item
+ }));
+ console.log("Final filtered results:", filteredWithQty.length);
+ setFilteredItems(filteredWithQty);
+ setIsLoading(false);
+ }, 500);
+ }, [type, searchCode, searchName, targetDate, items, t]); // Add targetDate back to dependencies
+
+ // Handle quantity change in search results
+ const handleSearchQtyChange = useCallback((itemId: number, newQty: number | null) => {
+ setFilteredItems(prev =>
+ prev.map(item =>
+ item.id === itemId ? { ...item, qty: newQty } : item
+ )
+ );
+
+ // Auto-update created items if this item exists there
+ setCreatedItems(prev =>
+ prev.map(item =>
+ item.itemId === itemId ? { ...item, qty: newQty || 1 } : item
+ )
+ );
+ }, []);
+
+ // Modified handler for search item selection
+ const handleSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => {
+ if (isSelected) {
+ const item = filteredItems.find(i => i.id === itemId);
+ if (!item) return;
+
+ const existingItem = createdItems.find(created => created.itemId === item.id);
+ if (existingItem) {
+ alert(t("Item already exists in created items"));
+ return;
+ }
+
+ // Fix the newCreatedItem creation - add missing uomDesc
+ const newCreatedItem: CreatedItem = {
+ itemId: item.id,
+ itemName: item.label,
+ itemCode: item.label,
+ qty: item.qty || 1,
+ uom: item.uom || "",
+ uomId: item.uomId || 0,
+ uomDesc: item.uomDesc || "", // Add missing uomDesc
+ isSelected: true,
+ currentStockBalance: item.currentStockBalance,
+ targetDate: item.targetDate || targetDate, // Use item's targetDate or fallback to form's targetDate
+ groupId: item.groupId || undefined, // Handle null values
+ };
+ setCreatedItems(prev => [...prev, newCreatedItem]);
+ }
+ }, [filteredItems, createdItems, t, targetDate]);
+
+ // Handler for created item selection
+ const handleCreatedItemSelect = useCallback((itemId: number, isSelected: boolean) => {
+ setCreatedItems(prev =>
+ prev.map(item =>
+ item.itemId === itemId ? { ...item, isSelected } : item
+ )
+ );
+ }, []);
+
+ const handleQtyChange = useCallback((itemId: number, newQty: number) => {
+ setCreatedItems(prev =>
+ prev.map(item =>
+ item.itemId === itemId ? { ...item, qty: newQty } : item
+ )
+ );
+ }, []);
+
+ // Check if item is already in created items
+ const isItemInCreated = useCallback((itemId: number) => {
+ return createdItems.some(item => item.itemId === itemId);
+ }, [createdItems]);
+
+ // 1) Created Items 行内改组:只改这一行的 groupId,并把该行 targetDate 同步为该组日期
+ const handleCreatedItemGroupChange = useCallback((itemId: number, newGroupId: string) => {
+ const gid = newGroupId ? Number(newGroupId) : undefined;
+ const group = groups.find(g => g.id === gid);
+ setCreatedItems(prev =>
+ prev.map(it =>
+ it.itemId === itemId
+ ? {
+ ...it,
+ groupId: gid,
+ targetDate: group?.targetDate || it.targetDate,
+ }
+ : it,
+ ),
+ );
+ }, [groups]);
+
+ // Update the handleGroupChange function to update target dates for items in the selected group
+ const handleGroupChange = useCallback((groupId: string | number) => {
+ const gid = typeof groupId === "string" ? Number(groupId) : groupId;
+ const group = groups.find(g => g.id === gid);
+ if (!group) return;
+
+ setSelectedGroup(group);
+
+ // Update target dates for items that belong to this group
+ setSecondSearchResults(prev => prev.map(item =>
+ item.groupId === gid
+ ? {
+ ...item,
+ targetDate: group.targetDate
+ }
+ : item
+ ));
+ }, [groups]);
+
+ // Update the handleGroupTargetDateChange function to update selected items that belong to that group
+ const handleGroupTargetDateChange = useCallback((groupId: number, newTargetDate: string) => {
+ setGroups(prev => prev.map(g => (g.id === groupId ? { ...g, targetDate: newTargetDate } : g)));
+
+ // Update selected items that belong to this group
+ setSecondSearchResults(prev => prev.map(item =>
+ item.groupId === groupId
+ ? {
+ ...item,
+ targetDate: newTargetDate
+ }
+ : item
+ ));
+ }, []);
+
+ // Fix the handleCreateGroup function to use the API properly
+ const handleCreateGroup = useCallback(async () => {
+ try {
+ // Use the API to get latest group name and create it automatically
+ const response = await getLatestGroupNameAndCreate();
+
+ if (response.id && response.name) {
+ const newGroup: Group = {
+ id: response.id,
+ name: response.name,
+ targetDate: dayjs().format(INPUT_DATE_FORMAT)
+ };
+
+ setGroups(prev => [...prev, newGroup]);
+ setSelectedGroup(newGroup);
+
+ console.log(`Created new group: ${response.name}`);
+ } else {
+ alert(t('Failed to create group'));
+ }
+ } catch (error) {
+ console.error('Error creating group:', error);
+ alert(t('Failed to create group'));
+ }
+ }, [t]);
+
+ // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group)
+ const handleSecondSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => {
+ if (!isSelected) return;
+ const item = secondSearchResults.find(i => i.id === itemId);
+ if (!item) return;
+ const exists = createdItems.find(c => c.itemId === item.id);
+ if (exists) { alert(t("Item already exists in created items")); return; }
+
+ // 找到项目所属的组,使用该组的 targetDate
+ const itemGroup = groups.find(g => g.id === item.groupId);
+ const itemTargetDate = itemGroup?.targetDate || item.targetDate || targetDate;
+
+ const newCreatedItem: CreatedItem = {
+ itemId: item.id,
+ itemName: item.label,
+ itemCode: item.label,
+ qty: item.qty || 1,
+ uom: item.uom || "",
+ uomId: item.uomId || 0,
+ uomDesc: item.uomDesc || "",
+ isSelected: true,
+ currentStockBalance: item.currentStockBalance,
+ targetDate: itemTargetDate, // 使用项目所属组的 targetDate
+ groupId: item.groupId || undefined, // 使用项目自身的 groupId
+ };
+ setCreatedItems(prev => [...prev, newCreatedItem]);
+ }, [secondSearchResults, createdItems, groups, targetDate, t]);
+
+ // 修改提交函数,按组分别创建提料单
+ const onSubmit = useCallback>(
+ async (data, event) => {
+
+ const selectedCreatedItems = createdItems.filter(item => item.isSelected);
+
+ if (selectedCreatedItems.length === 0) {
+ alert(t("Please select at least one item to submit"));
+ return;
+ }
+
+ if (!data.type) {
+ alert(t("Please select product type"));
+ return;
+ }
+
+ // Remove the data.targetDate check since we'll use group target dates
+ // if (!data.targetDate) {
+ // alert(t("Please select target date"));
+ // return;
+ // }
+
+ // 按组分组选中的项目
+ const itemsByGroup = selectedCreatedItems.reduce((acc, item) => {
+ const groupId = item.groupId || 'no-group';
+ if (!acc[groupId]) {
+ acc[groupId] = [];
+ }
+ acc[groupId].push(item);
+ return acc;
+ }, {} as Record);
+
+ console.log("Items grouped by group:", itemsByGroup);
+
+ let successCount = 0;
+ const totalGroups = Object.keys(itemsByGroup).length;
+ const groupUpdates: Array<{groupId: number, pickOrderId: number}> = [];
+
+ // 为每个组创建提料单
+ for (const [groupId, items] of Object.entries(itemsByGroup)) {
+ try {
+ // 获取组的名称和目标日期
+ const group = groups.find(g => g.id === Number(groupId));
+ const groupName = group?.name || 'No Group';
+
+ // Use the group's target date, fallback to item's target date, then form's target date
+ let groupTargetDate = group?.targetDate;
+ if (!groupTargetDate && items.length > 0) {
+ groupTargetDate = items[0].targetDate || undefined; // Add || undefined to handle null
+ }
+ if (!groupTargetDate) {
+ groupTargetDate = data.targetDate;
+ }
+
+ // If still no target date, use today
+ if (!groupTargetDate) {
+ groupTargetDate = dayjs().format(INPUT_DATE_FORMAT);
+ }
+
+ console.log(`Creating pick order for group: ${groupName} with ${items.length} items, target date: ${groupTargetDate}`);
+
+ let formattedTargetDate = groupTargetDate;
+ if (groupTargetDate && typeof groupTargetDate === 'string') {
+ try {
+ const date = dayjs(groupTargetDate);
+ formattedTargetDate = date.format('YYYY-MM-DD');
+ } catch (error) {
+ console.error("Invalid date format:", groupTargetDate);
+ alert(t("Invalid date format"));
+ return;
+ }
+ }
+
+ const pickOrderData: SavePickOrderRequest = {
+ type: data.type || "Consumable",
+ targetDate: formattedTargetDate,
+ pickOrderLine: items.map(item => ({
+ itemId: item.itemId,
+ qty: item.qty,
+ uomId: item.uomId
+ } as SavePickOrderLineRequest))
+ };
+
+ console.log(`Submitting pick order for group ${groupName}:`, pickOrderData);
+
+ const res = await createPickOrder(pickOrderData);
+ if (res.id) {
+ console.log(`Pick order created successfully for group ${groupName}:`, res);
+ successCount++;
+
+ // Store group ID and pick order ID for updating
+ if (groupId !== 'no-group' && group?.id) {
+ groupUpdates.push({
+ groupId: group.id,
+ pickOrderId: res.id
+ });
+ }
+ } else {
+ console.error(`Failed to create pick order for group ${groupName}:`, res);
+ alert(t(`Failed to create pick order for group ${groupName}`));
+ return;
+ }
+ } catch (error) {
+ console.error(`Error creating pick order for group ${groupId}:`, error);
+ alert(t(`Error creating pick order for group ${groupId}`));
+ return;
+ }
+ }
+
+ // Update groups with pick order information
+ if (groupUpdates.length > 0) {
+ try {
+ // Update each group with its corresponding pick order ID
+ for (const update of groupUpdates) {
+ const updateResponse = await createOrUpdateGroups({
+ groupIds: [update.groupId],
+ targetDate: data.targetDate,
+ pickOrderId: update.pickOrderId
+ });
+
+ console.log(`Group ${update.groupId} updated with pick order ${update.pickOrderId}:`, updateResponse);
+ }
+ } catch (error) {
+ console.error('Error updating groups:', error);
+ // Don't fail the whole operation if group update fails
+ }
+ }
+
+ // 所有组都创建成功后,清理选中的项目并切换到 Assign & Release
+ if (successCount === totalGroups) {
+ setCreatedItems(prev => prev.filter(item => !item.isSelected));
+ formProps.reset();
+ setHasSearched(false);
+ setFilteredItems([]);
+ alert(t("All pick orders created successfully"));
+
+ // 通知父组件切换到 Assign & Release 标签页
+ if (onPickOrderCreated) {
+ onPickOrderCreated();
+ }
+ }
+ },
+ [createdItems, t, formProps, groups, onPickOrderCreated]
+ );
+
+ // Fix the handleReset function to properly clear all states including search results
+ const handleReset = useCallback(() => {
+ formProps.reset();
+ setCreatedItems([]);
+ setHasSearched(false);
+ setFilteredItems([]);
+
+ // Clear second search states completely
+ setSecondSearchResults([]);
+ setHasSearchedSecond(false);
+ setSelectedSecondSearchItemIds([]);
+ setSecondSearchQuery({});
+
+ // Clear groups
+ setGroups([]);
+ setSelectedGroup(null);
+ setNextGroupNumber(1);
+
+ // Clear pagination states
+ setSearchResultsPagingController({
+ pageNum: 1,
+ pageSize: 10,
+ });
+ setCreatedItemsPagingController({
+ pageNum: 1,
+ pageSize: 10,
+ });
+
+ // Clear first search states
+ setSelectedSearchItemIds([]);
+ }, [formProps]);
+
+ // Pagination state
+ const [page, setPage] = useState(0);
+ const [rowsPerPage, setRowsPerPage] = useState(10);
+
+ // Handle page change
+ const handleChangePage = (
+ _event: React.MouseEvent | React.KeyboardEvent,
+ newPage: number,
+ ) => {
+ console.log(_event);
+ setPage(newPage);
+ // The original code had setPagingController and defaultPagingController,
+ // but these are not defined in the provided context.
+ // Assuming they are meant to be part of a larger context or will be added.
+ // For now, commenting out the setPagingController part as it's not defined.
+ // if (setPagingController) {
+ // setPagingController({
+ // ...(pagingController ?? defaultPagingController),
+ // pageNum: newPage + 1,
+ // });
+ // }
+ };
+
+ // Handle rows per page change
+ const handleChangeRowsPerPage = (
+ event: React.ChangeEvent,
+ ) => {
+ console.log(event);
+ setRowsPerPage(+event.target.value);
+ setPage(0);
+ // The original code had setPagingController and defaultPagingController,
+ // but these are not defined in the provided context.
+ // Assuming they are meant to be part of a larger context or will be added.
+ // For now, commenting out the setPagingController part as it's not defined.
+ // if (setPagingController) {
+ // setPagingController({
+ // ...(pagingController ?? defaultPagingController),
+ // pageNum: 1,
+ // });
+ // }
+ };
+
+ // Add missing handleSearchCheckboxChange function
+ const handleSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => {
+ if (typeof ids === 'function') {
+ const newIds = ids(selectedSearchItemIds);
+ setSelectedSearchItemIds(newIds);
+
+ if (newIds.length === filteredItems.length) {
+ // Select all
+ filteredItems.forEach(item => {
+ if (!isItemInCreated(item.id)) {
+ handleSearchItemSelect(item.id, true);
+ }
+ });
+ } else {
+ // Handle individual selections
+ filteredItems.forEach(item => {
+ const isSelected = newIds.includes(item.id);
+ const isCurrentlyInCreated = isItemInCreated(item.id);
+
+ if (isSelected && !isCurrentlyInCreated) {
+ handleSearchItemSelect(item.id, true);
+ } else if (!isSelected && isCurrentlyInCreated) {
+ setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id));
+ }
+ });
+ }
+ } else {
+ const previousIds = selectedSearchItemIds;
+ setSelectedSearchItemIds(ids);
+
+ const newlySelected = ids.filter(id => !previousIds.includes(id));
+ const newlyDeselected = previousIds.filter(id => !ids.includes(id));
+
+ newlySelected.forEach(id => {
+ if (!isItemInCreated(id as number)) {
+ handleSearchItemSelect(id as number, true);
+ }
+ });
+
+ newlyDeselected.forEach(id => {
+ setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id));
+ });
+ }
+ }, [selectedSearchItemIds, filteredItems, isItemInCreated, handleSearchItemSelect]);
+
+ // Add pagination state for created items
+ const [createdItemsPagingController, setCreatedItemsPagingController] = useState({
+ pageNum: 1,
+ pageSize: 10,
+ });
+
+ // Add pagination handlers for created items
+ const handleCreatedItemsPageChange = useCallback((event: unknown, newPage: number) => {
+ const newPagingController = {
+ ...createdItemsPagingController,
+ pageNum: newPage + 1,
+ };
+ setCreatedItemsPagingController(newPagingController);
+ }, [createdItemsPagingController]);
+
+ const handleCreatedItemsPageSizeChange = useCallback((event: React.ChangeEvent) => {
+ const newPageSize = parseInt(event.target.value, 10);
+ const newPagingController = {
+ pageNum: 1,
+ pageSize: newPageSize,
+ };
+ setCreatedItemsPagingController(newPagingController);
+ }, []);
+
+ // Create a custom table for created items with pagination
+ const CustomCreatedItemsTable = () => {
+ const startIndex = (createdItemsPagingController.pageNum - 1) * createdItemsPagingController.pageSize;
+ const endIndex = startIndex + createdItemsPagingController.pageSize;
+ const paginatedCreatedItems = createdItems.slice(startIndex, endIndex);
+
+ return (
+ <>
+
+
+
+
+
+ {t("Selected")}
+
+
+ {t("Item")}
+
+
+ {t("Group")}
+
+
+ {t("Current Stock")}
+
+
+ {t("Stock Unit")}
+
+
+ {t("Order Quantity")}
+
+
+ {t("Target Date")}
+
+
+
+
+ {paginatedCreatedItems.length === 0 ? (
+
+
+
+ {t("No created items")}
+
+
+
+ ) : (
+ paginatedCreatedItems.map((item) => (
+
+
+ handleCreatedItemSelect(item.itemId, e.target.checked)}
+ />
+
+
+ {item.itemName}
+
+ {item.itemCode}
+
+
+
+
+
+
+
+
+ 0 ? "success.main" : "error.main"}
+ >
+ {item.currentStockBalance?.toLocaleString() || 0}
+
+
+
+ {item.uomDesc}
+
+
+ {
+ const newQty = Number(e.target.value);
+ handleQtyChange(item.itemId, newQty);
+ }}
+ inputProps={{
+ min: 1,
+ step: 1,
+ style: { textAlign: 'center' }
+ }}
+ sx={{
+ width: '80px',
+ '& .MuiInputBase-input': {
+ textAlign: 'center',
+ cursor: 'text'
+ }
+ }}
+ />
+
+
+
+ {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
+
+
+
+ ))
+ )}
+
+
+
+
+ {/* Pagination for created items */}
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+ >
+ );
+ };
+
+ // Define columns for SearchResults
+ const searchItemColumns: Column[] = useMemo(() => [
+ {
+ name: "id",
+ label: "",
+ type: "checkbox",
+ disabled: (item) => isItemInCreated(item.id), // Disable if already in created items
+ },
+
+ {
+ name: "label",
+ label: t("Item"),
+ renderCell: (item) => {
+
+ const parts = item.label.split(' - ');
+ const code = parts[0] || '';
+ const name = parts[1] || '';
+
+ return (
+
+
+ {name} {/* 显示项目名称 */}
+
+
+ {code} {/* 显示项目代码 */}
+
+
+ );
+ },
+ },
+ {
+ name: "qty",
+ label: t("Order Quantity"),
+ renderCell: (item) => (
+ {
+ const value = e.target.value;
+ const numValue = value === "" ? null : Number(value);
+ handleSearchQtyChange(item.id, numValue);
+ }}
+ inputProps={{
+ min: 1,
+ step: 1,
+ style: { textAlign: 'center' } // Center the text
+ }}
+ sx={{
+ width: '80px',
+ '& .MuiInputBase-input': {
+ textAlign: 'center',
+ cursor: 'text'
+ }
+ }}
+ />
+ ),
+ },
+ {
+ name: "currentStockBalance",
+ label: t("Current Stock"),
+ renderCell: (item) => {
+ const stockBalance = item.currentStockBalance || 0;
+ return (
+ 0 ? "success.main" : "error.main"}
+ sx={{ fontWeight: stockBalance > 0 ? 'bold' : 'normal' }}
+ >
+ {stockBalance}
+
+ );
+ },
+ },
+ {
+ name: "targetDate",
+ label: t("Target Date"),
+ renderCell: (item) => (
+
+ {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
+
+ ),
+ },
+ {
+ name: "uom",
+ label: t("Stock Unit"),
+ renderCell: (item) => item.uom || "-",
+ },
+ ], [t, isItemInCreated, handleSearchQtyChange]);
+ // 修改搜索条件为3行,每行一个 - 确保SearchBox组件能正确处理
+ const pickOrderSearchCriteria: Criterion[] = useMemo(
+ () => [
+
+ {
+ label: t("Job Order Code"),
+ paramName: "jobOrderCode",
+ type: "text"
+ },
+ {
+ label: t("Item Code"),
+ paramName: "code",
+ type: "text"
+ },
+ {
+ label: t("Item Name"),
+ paramName: "name",
+ type: "text"
+ },
+ {
+ label: t("Product Type"),
+ paramName: "type",
+ type: "autocomplete",
+ options: [
+ { value: "Consumable", label: t("Consumable") },
+ { value: "MATERIAL", label: t("Material") },
+ { value: "End_product", label: t("End Product") }
+ ],
+ },
+ ],
+ [t],
+ );
+
+ // 添加重置函数
+ const handleSecondReset = useCallback(() => {
+ console.log("Second search reset");
+ setSecondSearchQuery({});
+ setSecondSearchResults([]);
+ setHasSearchedSecond(false);
+ // 清空表单中的类型,但保留今天的日期
+ formProps.setValue("type", "");
+ const today = dayjs().format(INPUT_DATE_FORMAT);
+ formProps.setValue("targetDate", today);
+ }, [formProps]);
+
+ // 添加数量变更处理函数
+ const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => {
+ setSecondSearchResults(prev =>
+ prev.map(item =>
+ item.id === itemId ? { ...item, qty: newQty } : item
+ )
+ );
+
+ // Auto-update created items if this item exists there
+ setCreatedItems(prev =>
+ prev.map(item =>
+ item.itemId === itemId ? { ...item, qty: newQty || 1 } : item
+ )
+ );
+ }, []);
+
+ // Add checkbox change handler for second search
+ const handleSecondSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => {
+ if (typeof ids === 'function') {
+ const newIds = ids(selectedSecondSearchItemIds);
+ setSelectedSecondSearchItemIds(newIds);
+
+ // 处理全选逻辑 - 选择所有搜索结果,不仅仅是当前页面
+ if (newIds.length === secondSearchResults.length) {
+ // 全选:将所有搜索结果添加到创建项目
+ secondSearchResults.forEach(item => {
+ if (!isItemInCreated(item.id)) {
+ handleSecondSearchItemSelect(item.id, true);
+ }
+ });
+ } else {
+ // 部分选择:只处理当前页面的选择
+ secondSearchResults.forEach(item => {
+ const isSelected = newIds.includes(item.id);
+ const isCurrentlyInCreated = isItemInCreated(item.id);
+
+ if (isSelected && !isCurrentlyInCreated) {
+ handleSecondSearchItemSelect(item.id, true);
+ } else if (!isSelected && isCurrentlyInCreated) {
+ setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id));
+ }
+ });
+ }
+ } else {
+ const previousIds = selectedSecondSearchItemIds;
+ setSelectedSecondSearchItemIds(ids);
+
+ const newlySelected = ids.filter(id => !previousIds.includes(id));
+ const newlyDeselected = previousIds.filter(id => !ids.includes(id));
+
+ newlySelected.forEach(id => {
+ if (!isItemInCreated(id as number)) {
+ handleSecondSearchItemSelect(id as number, true);
+ }
+ });
+
+ newlyDeselected.forEach(id => {
+ setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id));
+ });
+ }
+ }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]);
+
+ // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity
+ const secondSearchItemColumns: Column[] = useMemo(() => [
+ {
+ name: "id",
+ label: "",
+ type: "checkbox",
+ disabled: (item) => isItemInCreated(item.id),
+ },
+ {
+ name: "label",
+ label: t("Item"),
+ renderCell: (item) => {
+ const parts = item.label.split(' - ');
+ const code = parts[0] || '';
+ const name = parts[1] || '';
+
+ return (
+
+
+ {name}
+
+
+ {code}
+
+
+ );
+ },
+ },
+ {
+ name: "currentStockBalance",
+ label: t("Current Stock"),
+ align: "right", // Add right alignment for the label
+ renderCell: (item) => {
+ const stockBalance = item.currentStockBalance || 0;
+ return (
+
+ 0 ? "success.main" : "error.main"}
+ sx={{
+ fontWeight: stockBalance > 0 ? 'bold' : 'normal',
+ textAlign: 'right' // Add right alignment for the value
+ }}
+ >
+ {stockBalance}
+
+
+ );
+ },
+ },
+ {
+ name: "uom",
+ label: t("Stock Unit"),
+ align: "right", // Add right alignment for the label
+ renderCell: (item) => (
+
+ {/* Add right alignment for the value */}
+ {item.uom || "-"}
+
+
+ ),
+ },
+ {
+ name: "qty",
+ label: t("Order Quantity"),
+ align: "right",
+ renderCell: (item) => (
+
+ {
+ const value = e.target.value;
+ // Only allow numbers
+ if (value === "" || /^\d+$/.test(value)) {
+ const numValue = value === "" ? null : Number(value);
+ handleSecondSearchQtyChange(item.id, numValue);
+ }
+ }}
+ inputProps={{
+ style: { textAlign: 'center' }
+ }}
+ sx={{
+ width: '80px',
+ '& .MuiInputBase-input': {
+ textAlign: 'center',
+ cursor: 'text'
+ }
+ }}
+ onBlur={(e) => {
+ const value = e.target.value;
+ const numValue = value === "" ? null : Number(value);
+ if (numValue !== null && numValue < 1) {
+ handleSecondSearchQtyChange(item.id, 1); // Enforce min value
+ }
+ }}
+ />
+
+ ),
+}
+ ], [t, isItemInCreated, handleSecondSearchQtyChange, groups]);
+
+ // 添加缺失的 handleSecondSearch 函数
+ const handleSecondSearch = useCallback((query: Record) => {
+ console.log("Second search triggered with query:", query);
+ setSecondSearchQuery({ ...query });
+ setIsLoadingSecondSearch(true);
+
+ // Sync second search box info to form - ensure type value is correct
+ if (query.type) {
+ // Ensure type value matches backend enum format
+ let correctType = query.type;
+ if (query.type === "consumable") {
+ correctType = "Consumable";
+ } else if (query.type === "material") {
+ correctType = "MATERIAL";
+ } else if (query.type === "jo") {
+ correctType = "JOB_ORDER";
+ }
+ formProps.setValue("type", correctType);
+ }
+
+ setTimeout(() => {
+ let filtered = items;
+
+ // Same filtering logic as first search
+ if (query.code && query.code.trim()) {
+ filtered = filtered.filter(item =>
+ item.label.toLowerCase().includes(query.code.toLowerCase())
+ );
+ }
+
+ if (query.name && query.name.trim()) {
+ filtered = filtered.filter(item =>
+ item.label.toLowerCase().includes(query.name.toLowerCase())
+ );
+ }
+
+ if (query.type && query.type !== "All") {
+ // Filter by type if needed
+ }
+
+ // Convert to SearchItemWithQty with NO group/targetDate initially
+ const filteredWithQty = filtered.slice(0, 100).map(item => ({
+ ...item,
+ qty: null,
+ targetDate: undefined, // No target date initially
+ groupId: undefined, // No group initially
+ }));
+
+ setSecondSearchResults(filteredWithQty);
+ setHasSearchedSecond(true);
+ setIsLoadingSecondSearch(false);
+ }, 500);
+ }, [items, formProps]);
+
+ // Create a custom search box component that displays fields vertically
+ const VerticalSearchBox = ({ criteria, onSearch, onReset }: {
+ criteria: Criterion[];
+ onSearch: (inputs: Record) => void;
+ onReset?: () => void;
+ }) => {
+ const { t } = useTranslation("common");
+ const [inputs, setInputs] = useState>({});
+
+ const handleInputChange = (paramName: string, value: any) => {
+ setInputs(prev => ({ ...prev, [paramName]: value }));
+ };
+
+ const handleSearch = () => {
+ onSearch(inputs);
+ };
+
+ const handleReset = () => {
+ setInputs({});
+ onReset?.();
+ };
+
+ return (
+
+
+ {t("Search Criteria")}
+
+ {criteria.map((c) => {
+ return (
+
+ {c.type === "text" && (
+ handleInputChange(c.paramName, e.target.value)}
+ value={inputs[c.paramName] || ""}
+ />
+ )}
+ {c.type === "autocomplete" && (
+ option.label}
+ onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")}
+ renderInput={(params) => (
+
+ )}
+ />
+ )}
+
+ );
+ })}
+
+
+ }
+ onClick={handleReset}
+ >
+ {t("Reset")}
+
+ }
+ onClick={handleSearch}
+ >
+ {t("Search")}
+
+
+
+
+ );
+ };
+
+ // Add pagination state for search results
+ const [searchResultsPagingController, setSearchResultsPagingController] = useState({
+ pageNum: 1,
+ pageSize: 10,
+ });
+
+ // Add pagination handlers for search results
+ const handleSearchResultsPageChange = useCallback((event: unknown, newPage: number) => {
+ const newPagingController = {
+ ...searchResultsPagingController,
+ pageNum: newPage + 1, // API uses 1-based pagination
+ };
+ setSearchResultsPagingController(newPagingController);
+ }, [searchResultsPagingController]);
+
+ const handleSearchResultsPageSizeChange = useCallback((event: React.ChangeEvent) => {
+ const newPageSize = parseInt(event.target.value, 10);
+ const newPagingController = {
+ pageNum: 1, // Reset to first page
+ pageSize: newPageSize,
+ };
+ setSearchResultsPagingController(newPagingController);
+ }, []);
+const getValidationMessage = useCallback(() => {
+ const selectedItems = secondSearchResults.filter(item =>
+ selectedSecondSearchItemIds.includes(item.id)
+ );
+
+ const itemsWithoutGroup = selectedItems.filter(item =>
+ item.groupId === undefined || item.groupId === null
+ );
+
+ const itemsWithoutQty = selectedItems.filter(item =>
+ item.qty === null || item.qty === undefined || item.qty <= 0
+ );
+
+ if (itemsWithoutGroup.length > 0 && itemsWithoutQty.length > 0) {
+ return t("Please select group and enter quantity for all selected items");
+ } else if (itemsWithoutGroup.length > 0) {
+ return t("Please select group for all selected items");
+ } else if (itemsWithoutQty.length > 0) {
+ return t("Please enter quantity for all selected items");
+ }
+
+ return "";
+}, [secondSearchResults, selectedSecondSearchItemIds, t]);
+ // Fix the handleAddSelectedToCreatedItems function to properly clear selections
+ const handleAddSelectedToCreatedItems = useCallback(() => {
+ const selectedItems = secondSearchResults.filter(item =>
+ selectedSecondSearchItemIds.includes(item.id)
+ );
+
+ // Add selected items to created items with their own group info
+ selectedItems.forEach(item => {
+ if (!isItemInCreated(item.id)) {
+ const newCreatedItem: CreatedItem = {
+ itemId: item.id,
+ itemName: item.label,
+ itemCode: item.label,
+ qty: item.qty || 1,
+ uom: item.uom || "",
+ uomId: item.uomId || 0,
+ uomDesc: item.uomDesc || "",
+ isSelected: true,
+ currentStockBalance: item.currentStockBalance,
+ targetDate: item.targetDate || targetDate,
+ groupId: item.groupId || undefined,
+ };
+ setCreatedItems(prev => [...prev, newCreatedItem]);
+ }
+ });
+
+ // Clear the selection
+ setSelectedSecondSearchItemIds([]);
+
+ // Remove the selected/added items from search results entirely
+ setSecondSearchResults(prev => prev.filter(item =>
+ !selectedSecondSearchItemIds.includes(item.id)
+ ));
+}, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]);
+
+ // Add a validation function to check if selected items are valid
+ const areSelectedItemsValid = useCallback(() => {
+ const selectedItems = secondSearchResults.filter(item =>
+ selectedSecondSearchItemIds.includes(item.id)
+ );
+
+ return selectedItems.every(item =>
+ item.groupId !== undefined &&
+ item.groupId !== null &&
+ item.qty !== null &&
+ item.qty !== undefined &&
+ item.qty > 0
+ );
+ }, [secondSearchResults, selectedSecondSearchItemIds]);
+
+ // Move these handlers to the component level (outside of CustomSearchResultsTable)
+
+// Handle individual checkbox change - ONLY select, don't add to created items
+const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => {
+ if (checked) {
+ // Just add to selected IDs, don't auto-add to created items
+ setSelectedSecondSearchItemIds(prev => [...prev, itemId]);
+
+ // Set the item's group and targetDate to current group when selected
+ setSecondSearchResults(prev => prev.map(item =>
+ item.id === itemId
+ ? {
+ ...item,
+ groupId: selectedGroup?.id || undefined,
+ targetDate: selectedGroup?.targetDate || undefined
+ }
+ : item
+ ));
+ } else {
+ // Just remove from selected IDs, don't remove from created items
+ setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId));
+
+ // Clear the item's group and targetDate when deselected
+ setSecondSearchResults(prev => prev.map(item =>
+ item.id === itemId
+ ? {
+ ...item,
+ groupId: undefined,
+ targetDate: undefined
+ }
+ : item
+ ));
+ }
+}, [selectedGroup]);
+
+// Handle select all checkbox for current page
+const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: SearchItemWithQty[]) => {
+ if (checked) {
+ // Select all items on current page that are not already in created items
+ const newSelectedIds = paginatedResults
+ .filter(item => !isItemInCreated(item.id))
+ .map(item => item.id);
+
+ setSelectedSecondSearchItemIds(prev => {
+ const existingIds = prev.filter(id => !paginatedResults.some(item => item.id === id));
+ return [...existingIds, ...newSelectedIds];
+ });
+
+ // Set group and targetDate for all selected items on current page
+ setSecondSearchResults(prev => prev.map(item =>
+ newSelectedIds.includes(item.id)
+ ? {
+ ...item,
+ groupId: selectedGroup?.id || undefined,
+ targetDate: selectedGroup?.targetDate || undefined
+ }
+ : item
+ ));
+ } else {
+ // Deselect all items on current page
+ const pageItemIds = paginatedResults.map(item => item.id);
+ setSelectedSecondSearchItemIds(prev => prev.filter(id => !pageItemIds.includes(id as number)));
+
+ // Clear group and targetDate for all deselected items on current page
+ setSecondSearchResults(prev => prev.map(item =>
+ pageItemIds.includes(item.id)
+ ? {
+ ...item,
+ groupId: undefined,
+ targetDate: undefined
+ }
+ : item
+ ));
+ }
+}, [selectedGroup, isItemInCreated]);
+
+// Update the CustomSearchResultsTable to use the handlers from component level
+const CustomSearchResultsTable = () => {
+ // Calculate pagination
+ const startIndex = (searchResultsPagingController.pageNum - 1) * searchResultsPagingController.pageSize;
+ const endIndex = startIndex + searchResultsPagingController.pageSize;
+ const paginatedResults = secondSearchResults.slice(startIndex, endIndex);
+
+ // Check if all items on current page are selected
+ const allSelectedOnPage = paginatedResults.length > 0 &&
+ paginatedResults.every(item => selectedSecondSearchItemIds.includes(item.id));
+
+ // Check if some items on current page are selected
+ const someSelectedOnPage = paginatedResults.some(item => selectedSecondSearchItemIds.includes(item.id));
+
+ return (
+ <>
+
+
+
+
+
+ {t("Selected")}
+
+
+ {t("Item")}
+
+
+ {t("Group")}
+
+
+ {t("Current Stock")}
+
+
+ {t("Stock Unit")}
+
+
+ {t("Order Quantity")}
+
+
+ {t("Target Date")}
+
+
+
+
+ {paginatedResults.length === 0 ? (
+
+
+
+ {t("No data available")}
+
+
+
+ ) : (
+ paginatedResults.map((item) => (
+
+
+ handleIndividualCheckboxChange(item.id, e.target.checked)}
+ disabled={isItemInCreated(item.id)}
+ />
+
+
+ {/* Item */}
+
+
+
+ {item.label.split(' - ')[1] || item.label}
+
+
+ {item.label.split(' - ')[0] || ''}
+
+
+
+
+ {/* Group - Show the item's own group (or "-" if not selected) */}
+
+
+ {(() => {
+ if (item.groupId) {
+ const group = groups.find(g => g.id === item.groupId);
+ return group?.name || "-";
+ }
+ return "-"; // Show "-" for unselected items
+ })()}
+
+
+
+ {/* Current Stock */}
+
+ 0 ? "success.main" : "error.main"}
+ sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }}
+ >
+ {item.currentStockBalance || 0}
+
+
+
+ {/* Stock Unit */}
+
+
+ {item.uomDesc || "-"}
+
+
+
+ {/* Order Quantity */}
+
+ {
+ const value = e.target.value;
+ // Only allow numbers
+ if (value === "" || /^\d+$/.test(value)) {
+ const numValue = value === "" ? null : Number(value);
+ handleSecondSearchQtyChange(item.id, numValue);
+ }
+ }}
+ inputProps={{
+ style: { textAlign: 'center' }
+ }}
+ sx={{
+ width: '80px',
+ '& .MuiInputBase-input': {
+ textAlign: 'center',
+ cursor: 'text'
+ }
+ }}
+ />
+
+
+ {/* Target Date - Show the item's own target date (or "-" if not selected) */}
+
+
+ {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
+
+
+
+ ))
+ )}
+
+
+
+
+ {/* Add pagination for search results */}
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+ >
+ );
+};
+
+ // Add helper function to get group range text
+ const getGroupRangeText = useCallback(() => {
+ if (groups.length === 0) return "";
+
+ const firstGroup = groups[0];
+ const lastGroup = groups[groups.length - 1];
+
+ if (firstGroup.id === lastGroup.id) {
+ return `${t("First created group")}: ${firstGroup.name}`;
+ } else {
+ return `${t("First created group")}: ${firstGroup.name} - ${t("Latest created group")}: ${lastGroup.name}`;
+ }
+ }, [groups, t]);
+
+ return (
+
+
+ {/* First Search Box - Item Search with vertical layout */}
+
+
+ {t("Search Items")}
+
+
+
+
+
+ {/* Create Group Section - 简化版本,不需要表单 */}
+
+
+
+
+
+
+ {groups.length > 0 && (
+ <>
+
+ {t("Group")}:
+
+
+
+
+
+
+
+ {selectedGroup && (
+
+
+ {
+ if (date) {
+ const formattedDate = date.format(INPUT_DATE_FORMAT);
+ handleGroupTargetDateChange(selectedGroup.id, formattedDate);
+ }
+ }}
+ slotProps={{
+ textField: {
+ size: "small",
+ label: t("Target Date"),
+ sx: { width: 180 }
+ },
+ }}
+ />
+
+
+ )}
+ >
+ )}
+
+
+ {/* Add group range text */}
+ {groups.length > 0 && (
+
+
+ {getGroupRangeText()}
+
+
+ )}
+
+
+ {/* Second Search Results - Use custom table like AssignAndRelease */}
+ {hasSearchedSecond && (
+
+
+ {t("Search Results")} ({secondSearchResults.length})
+
+
+ {/* Add selected items info text */}
+ {selectedSecondSearchItemIds.length > 0 && (
+
+
+ {t("Selected items will join above created group")}
+
+
+ )}
+
+ {isLoadingSecondSearch ? (
+ {t("Loading...")}
+ ) : secondSearchResults.length === 0 ? (
+ {t("No results found")}
+ ) : (
+
+ )}
+
+ )}
+
+ {/* Add Submit Button between tables */}
+
+ {/* Search Results with SearchResults component */}
+ {hasSearchedSecond && secondSearchResults.length > 0 && selectedSecondSearchItemIds.length > 0 && (
+
+
+
+
+ {selectedSecondSearchItemIds.length > 0 && !areSelectedItemsValid() && (
+
+ {getValidationMessage()}
+
+ )}
+
+
+ )}
+
+
+ {/* 创建项目区域 - 修改Group列为可选择的 */}
+ {createdItems.length > 0 && (
+
+
+ {t("Created Items")} ({createdItems.length})
+
+
+
+
+ )}
+
+ {/* 操作按钮 */}
+
+ }
+ type="submit"
+ disabled={createdItems.filter(item => item.isSelected).length === 0}
+ >
+ {t("Create Pick Order")}
+
+
+
+
+
+ );
+};
+
+export default JobCreateItem;
\ No newline at end of file
diff --git a/src/components/Jodetail/Jodetail.tsx b/src/components/Jodetail/Jodetail.tsx
new file mode 100644
index 0000000..20704d3
--- /dev/null
+++ b/src/components/Jodetail/Jodetail.tsx
@@ -0,0 +1,167 @@
+import { Button, CircularProgress, Grid } from "@mui/material";
+import SearchResults, { Column } from "../SearchResults/SearchResults";
+import { PickOrderResult } from "@/app/api/pickOrder";
+import { useTranslation } from "react-i18next";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { isEmpty, upperCase, upperFirst } from "lodash";
+import { arrayToDateString, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
+import {
+ consolidatePickOrder,
+ fetchPickOrderClient,
+} from "@/app/api/pickOrder/actions";
+import useUploadContext from "../UploadProvider/useUploadContext";
+import dayjs from "dayjs";
+import arraySupport from "dayjs/plugin/arraySupport";
+dayjs.extend(arraySupport);
+interface Props {
+ filteredPickOrders: PickOrderResult[];
+ filterArgs: Record;
+}
+
+const Jodetail: React.FC = ({ filteredPickOrders, filterArgs }) => {
+ const { t } = useTranslation("pickOrder");
+ const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]);
+ const [filteredPickOrder, setFilteredPickOrder] = useState(
+ [] as PickOrderResult[],
+ );
+ const { setIsUploading } = useUploadContext();
+ const [isLoading, setIsLoading] = useState(false);
+ const [pagingController, setPagingController] = useState({
+ pageNum: 0,
+ pageSize: 10,
+ });
+ const [totalCount, setTotalCount] = useState();
+
+ const fetchNewPagePickOrder = useCallback(
+ async (
+ pagingController: Record,
+ filterArgs: Record,
+ ) => {
+ setIsLoading(true);
+ const params = {
+ ...pagingController,
+ ...filterArgs,
+ };
+ const res = await fetchPickOrderClient(params);
+ if (res) {
+ console.log(res);
+ setFilteredPickOrder(res.records);
+ setTotalCount(res.total);
+ }
+ setIsLoading(false);
+ },
+ [],
+ );
+
+ const handleConsolidatedRows = useCallback(async () => {
+ console.log(selectedRows);
+ setIsUploading(true);
+ try {
+ const res = await consolidatePickOrder(selectedRows as number[]);
+ if (res) {
+ console.log(res);
+ }
+ } catch {
+ setIsUploading(false);
+ }
+ fetchNewPagePickOrder(pagingController, filterArgs);
+ setIsUploading(false);
+ }, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]);
+
+
+ useEffect(() => {
+ fetchNewPagePickOrder(pagingController, filterArgs);
+ }, [fetchNewPagePickOrder, pagingController, filterArgs]);
+
+ const columns = useMemo[]>(
+ () => [
+ {
+ name: "id",
+ label: "",
+ type: "checkbox",
+ disabled: (params) => {
+ return !isEmpty(params.consoCode);
+ },
+ },
+ {
+ name: "code",
+ label: t("Code"),
+ },
+ {
+ name: "consoCode",
+ label: t("Consolidated Code"),
+ renderCell: (params) => {
+ return params.consoCode ?? "";
+ },
+ },
+ {
+ name: "type",
+ label: t("type"),
+ renderCell: (params) => {
+ return upperCase(params.type);
+ },
+ },
+ {
+ name: "items",
+ label: t("Items"),
+ renderCell: (params) => {
+ return params.items?.map((i) => i.name).join(", ");
+ },
+ },
+ {
+ name: "targetDate",
+ label: t("Target Date"),
+ renderCell: (params) => {
+ return (
+ dayjs(params.targetDate)
+ .add(-1, "month")
+ .format(OUTPUT_DATE_FORMAT)
+ );
+ },
+ },
+ {
+ name: "releasedBy",
+ label: t("Released By"),
+ },
+ {
+ name: "status",
+ label: t("Status"),
+ renderCell: (params) => {
+ return upperFirst(params.status);
+ },
+ },
+ ],
+ [t],
+ );
+
+ return (
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+ items={filteredPickOrder}
+ columns={columns}
+ pagingController={pagingController}
+ setPagingController={setPagingController}
+ totalCount={totalCount}
+ checkboxIds={selectedRows!}
+ setCheckboxIds={setSelectedRows}
+ />
+ )}
+
+
+ );
+};
+
+export default Jodetail;
diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx
new file mode 100644
index 0000000..e5d819c
--- /dev/null
+++ b/src/components/Jodetail/JodetailSearch.tsx
@@ -0,0 +1,443 @@
+"use client";
+import { PickOrderResult } from "@/app/api/pickOrder";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useTranslation } from "react-i18next";
+import SearchBox, { Criterion } from "../SearchBox";
+import {
+ flatten,
+ intersectionWith,
+ isEmpty,
+ sortBy,
+ uniqBy,
+ upperCase,
+ upperFirst,
+} from "lodash";
+import {
+ arrayToDayjs,
+} from "@/app/utils/formatUtil";
+import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography, Box } from "@mui/material";
+import Jodetail from "./Jodetail"
+import PickExecution from "./JobPickExecution";
+import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions";
+import { fetchPickOrderClient, autoAssignAndReleasePickOrder, autoAssignAndReleasePickOrderByStore } from "@/app/api/pickOrder/actions";
+import { useSession } from "next-auth/react";
+import { SessionWithTokens } from "@/config/authConfig";
+import JobPickExecutionsecondscan from "./JobPickExecutionsecondscan";
+import FInishedJobOrderRecord from "./FInishedJobOrderRecord";
+import JobPickExecution from "./JobPickExecution";
+import {
+ fetchUnassignedJobOrderPickOrders,
+ assignJobOrderPickOrder,
+ fetchJobOrderLotsHierarchical,
+ fetchCompletedJobOrderPickOrders,
+ fetchCompletedJobOrderPickOrderRecords
+} from "@/app/api/jo/actions";
+interface Props {
+ pickOrders: PickOrderResult[];
+}
+
+type SearchQuery = Partial<
+ Omit
+>;
+
+type SearchParamNames = keyof SearchQuery;
+
+const JodetailSearch: React.FC = ({ pickOrders }) => {
+ const { t } = useTranslation("jo");
+ const { data: session } = useSession() as { data: SessionWithTokens | null };
+ const currentUserId = session?.id ? parseInt(session.id) : undefined;
+
+ const [isOpenCreateModal, setIsOpenCreateModal] = useState(false)
+ const [items, setItems] = useState([])
+ const [printButtonsEnabled, setPrintButtonsEnabled] = useState(false);
+ const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders);
+ const [filterArgs, setFilterArgs] = useState>({});
+ const [searchQuery, setSearchQuery] = useState>({});
+ const [tabIndex, setTabIndex] = useState(0);
+ const [totalCount, setTotalCount] = useState();
+ const [isAssigning, setIsAssigning] = useState(false);
+ const [unassignedOrders, setUnassignedOrders] = useState([]);
+const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
+ const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState(
+ typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true'
+ );
+ useEffect(() => {
+ const onAssigned = () => {
+ localStorage.removeItem('hideCompletedUntilNext');
+ setHideCompletedUntilNext(false);
+ };
+ window.addEventListener('pickOrderAssigned', onAssigned);
+ return () => window.removeEventListener('pickOrderAssigned', onAssigned);
+ }, []);
+ // ... existing code ...
+
+ useEffect(() => {
+ const handleCompletionStatusChange = (event: CustomEvent) => {
+ const { allLotsCompleted, tabIndex: eventTabIndex } = event.detail;
+
+ // ✅ 修复:根据标签页和事件来源决定是否更新打印按钮状态
+ if (eventTabIndex === undefined || eventTabIndex === tabIndex) {
+ setPrintButtonsEnabled(allLotsCompleted);
+ console.log(`Print buttons enabled for tab ${tabIndex}:`, allLotsCompleted);
+ }
+ };
+
+ window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
+
+ return () => {
+ window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
+ };
+ }, [tabIndex]); // ✅ 添加 tabIndex 依赖
+
+ // ✅ 新增:处理标签页切换时的打印按钮状态重置
+ useEffect(() => {
+ // 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态
+ if (tabIndex === 2) {
+ setPrintButtonsEnabled(false);
+ console.log("Reset print buttons for Pick Execution Record tab");
+ }
+ }, [tabIndex]);
+
+// ... existing code ...
+ const handleAssignByStore = async (storeId: "2/F" | "4/F") => {
+ if (!currentUserId) {
+ console.error("Missing user id in session");
+ return;
+ }
+
+ setIsAssigning(true);
+ try {
+ const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId);
+ console.log("Assign by store result:", res);
+
+ // ✅ Handle different response codes
+ if (res.code === "SUCCESS") {
+ console.log("✅ Successfully assigned pick order to store", storeId);
+ // ✅ Trigger refresh to show newly assigned data
+ window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
+ } else if (res.code === "USER_BUSY") {
+ console.warn("⚠️ User already has pick orders in progress:", res.message);
+ // ✅ Show warning but still refresh to show existing orders
+ alert(`Warning: ${res.message}`);
+ window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
+ } else if (res.code === "NO_ORDERS") {
+ console.log("ℹ️ No available pick orders for store", storeId);
+ alert(`Info: ${res.message}`);
+ } else {
+ console.log("ℹ️ Assignment result:", res.message);
+ alert(`Info: ${res.message}`);
+ }
+ } catch (error) {
+ console.error("❌ Error assigning by store:", error);
+ alert("Error occurred during assignment");
+ } finally {
+ setIsAssigning(false);
+ }
+ };
+ // ✅ Manual assignment handler - uses the action function
+ const loadUnassignedOrders = useCallback(async () => {
+ setIsLoadingUnassigned(true);
+ try {
+ const orders = await fetchUnassignedJobOrderPickOrders();
+ setUnassignedOrders(orders);
+ } catch (error) {
+ console.error("Error loading unassigned orders:", error);
+ } finally {
+ setIsLoadingUnassigned(false);
+ }
+ }, []);
+
+ // 分配订单给当前用户
+ const handleAssignOrder = useCallback(async (pickOrderId: number) => {
+ if (!currentUserId) {
+ console.error("Missing user id in session");
+ return;
+ }
+
+ try {
+ const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
+ if (result.message === "Successfully assigned") {
+ console.log("✅ Successfully assigned pick order");
+ // 刷新数据
+ window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
+ // 重新加载未分配订单列表
+ loadUnassignedOrders();
+ } else {
+ console.warn("⚠️ Assignment failed:", result.message);
+ alert(`Assignment failed: ${result.message}`);
+ }
+ } catch (error) {
+ console.error("❌ Error assigning order:", error);
+ alert("Error occurred during assignment");
+ }
+ }, [currentUserId, loadUnassignedOrders]);
+
+ // 在组件加载时获取未分配订单
+ useEffect(() => {
+ loadUnassignedOrders();
+ }, [loadUnassignedOrders]);
+
+ const handleTabChange = useCallback>(
+ (_e, newValue) => {
+ setTabIndex(newValue);
+ },
+ [],
+ );
+
+ const openCreateModal = useCallback(async () => {
+ console.log("testing")
+ const res = await fetchAllItemsInClient()
+ console.log(res)
+ setItems(res)
+ setIsOpenCreateModal(true)
+ }, [])
+
+ const closeCreateModal = useCallback(() => {
+ setIsOpenCreateModal(false)
+ }, [])
+
+
+ useEffect(() => {
+
+ if (tabIndex === 3) {
+ const loadItems = async () => {
+ try {
+ const itemsData = await fetchAllItemsInClient();
+ console.log("PickOrderSearch loaded items:", itemsData.length);
+ setItems(itemsData);
+ } catch (error) {
+ console.error("Error loading items in PickOrderSearch:", error);
+ }
+ };
+
+ // 如果还没有数据,则加载
+ if (items.length === 0) {
+ loadItems();
+ }
+ }
+ }, [tabIndex, items.length]);
+ useEffect(() => {
+ const handleCompletionStatusChange = (event: CustomEvent) => {
+ const { allLotsCompleted } = event.detail;
+ setPrintButtonsEnabled(allLotsCompleted);
+ console.log("Print buttons enabled:", allLotsCompleted);
+ };
+
+ window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
+
+ return () => {
+ window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
+ };
+ }, []);
+
+ const searchCriteria: Criterion[] = useMemo(
+ () => {
+ const baseCriteria: Criterion[] = [
+ {
+ label: tabIndex === 3 ? t("Item Code") : t("Code"),
+ paramName: "code",
+ type: "text"
+ },
+ {
+ label: t("Type"),
+ paramName: "type",
+ type: "autocomplete",
+ options: tabIndex === 3
+ ?
+ [
+ { value: "Consumable", label: t("Consumable") },
+ { value: "Material", label: t("Material") },
+ { value: "Product", label: t("Product") }
+ ]
+ :
+ sortBy(
+ uniqBy(
+ pickOrders.map((po) => ({
+ value: po.type,
+ label: t(upperCase(po.type)),
+ })),
+ "value",
+ ),
+ "label",
+ ),
+ },
+ ];
+
+ // Add Job Order search for Create Item tab (tabIndex === 3)
+ if (tabIndex === 3) {
+ baseCriteria.splice(1, 0, {
+ label: t("Job Order"),
+ paramName: "jobOrderCode" as any, // Type assertion for now
+ type: "text",
+ });
+
+ baseCriteria.splice(2, 0, {
+ label: t("Target Date"),
+ paramName: "targetDate",
+ type: "date",
+ });
+ } else {
+ baseCriteria.splice(1, 0, {
+ label: t("Target Date From"),
+ label2: t("Target Date To"),
+ paramName: "targetDate",
+ type: "dateRange",
+ });
+ }
+
+ // Add Items/Item Name criteria
+ baseCriteria.push({
+ label: tabIndex === 3 ? t("Item Name") : t("Items"),
+ paramName: "items",
+ type: tabIndex === 3 ? "text" : "autocomplete",
+ options: tabIndex === 3
+ ? []
+ :
+ uniqBy(
+ flatten(
+ sortBy(
+ pickOrders.map((po) =>
+ po.items
+ ? po.items.map((item) => ({
+ value: item.name,
+ label: item.name,
+ }))
+ : [],
+ ),
+ "label",
+ ),
+ ),
+ "value",
+ ),
+ });
+
+ // Add Status criteria for non-Create Item tabs
+ if (tabIndex !== 3) {
+ baseCriteria.push({
+ label: t("Status"),
+ paramName: "status",
+ type: "autocomplete",
+ options: sortBy(
+ uniqBy(
+ pickOrders.map((po) => ({
+ value: po.status,
+ label: t(upperFirst(po.status)),
+ })),
+ "value",
+ ),
+ "label",
+ ),
+ });
+ }
+
+ return baseCriteria;
+ },
+ [pickOrders, t, tabIndex, items],
+ );
+
+ const fetchNewPagePickOrder = useCallback(
+ async (
+ pagingController: Record,
+ filterArgs: Record,
+ ) => {
+ const params = {
+ ...pagingController,
+ ...filterArgs,
+ };
+ const res = await fetchPickOrderClient(params);
+ if (res) {
+ console.log(res);
+ setFilteredPickOrders(res.records);
+ setTotalCount(res.total);
+ }
+ },
+ [],
+ );
+
+ const onReset = useCallback(() => {
+ setFilteredPickOrders(pickOrders);
+ }, [pickOrders]);
+
+ useEffect(() => {
+ if (!isOpenCreateModal) {
+ setTabIndex(1)
+ setTimeout(async () => {
+ setTabIndex(0)
+ }, 200)
+ }
+ }, [isOpenCreateModal])
+
+ // 添加处理提料单创建成功的函数
+ const handlePickOrderCreated = useCallback(() => {
+ // 切换到 Assign & Release 标签页 (tabIndex = 1)
+ setTabIndex(2);
+ }, []);
+
+ return (
+
+ {/* Header section */}
+
+
+
+
+
+
+
+ {/* Last 2 buttons aligned right */}
+
+ {/* Unassigned Job Orders */}
+{unassignedOrders.length > 0 && (
+
+
+ {t("Unassigned Job Orders")} ({unassignedOrders.length})
+
+
+ {unassignedOrders.map((order) => (
+
+ ))}
+
+
+)}
+
+
+
+
+
+
+
+ {/* Tabs section - ✅ Move the click handler here */}
+
+
+
+
+
+
+
+
+
+ {/* Content section - NO overflow: 'auto' here */}
+
+ {tabIndex === 0 && }
+ {tabIndex === 1 && }
+ {tabIndex === 2 && }
+
+
+ );
+};
+
+export default JodetailSearch;
\ No newline at end of file
diff --git a/src/components/Jodetail/LotConfirmationModal.tsx b/src/components/Jodetail/LotConfirmationModal.tsx
new file mode 100644
index 0000000..de48da7
--- /dev/null
+++ b/src/components/Jodetail/LotConfirmationModal.tsx
@@ -0,0 +1,124 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Typography,
+ Alert,
+ Stack,
+ Divider,
+} from "@mui/material";
+import { useTranslation } from "react-i18next";
+
+interface LotConfirmationModalProps {
+ open: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ expectedLot: {
+ lotNo: string;
+ itemCode: string;
+ itemName: string;
+ };
+ scannedLot: {
+ lotNo: string;
+ itemCode: string;
+ itemName: string;
+ };
+ isLoading?: boolean;
+}
+
+const LotConfirmationModal: React.FC = ({
+ open,
+ onClose,
+ onConfirm,
+ expectedLot,
+ scannedLot,
+ isLoading = false,
+}) => {
+ const { t } = useTranslation("pickOrder");
+
+ return (
+
+ );
+};
+
+export default LotConfirmationModal;
\ No newline at end of file
diff --git a/src/components/Jodetail/PutawayForm.tsx b/src/components/Jodetail/PutawayForm.tsx
new file mode 100644
index 0000000..aea7779
--- /dev/null
+++ b/src/components/Jodetail/PutawayForm.tsx
@@ -0,0 +1,527 @@
+"use client";
+
+import { PurchaseQcResult, PutAwayInput, PutAwayLine } from "@/app/api/po/actions";
+import {
+ Autocomplete,
+ Box,
+ Button,
+ Card,
+ CardContent,
+ FormControl,
+ Grid,
+ Modal,
+ ModalProps,
+ Stack,
+ TextField,
+ Tooltip,
+ Typography,
+} from "@mui/material";
+import { Controller, useFormContext } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import StyledDataGrid from "../StyledDataGrid";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import {
+ GridColDef,
+ GridRowIdGetter,
+ GridRowModel,
+ useGridApiContext,
+ GridRenderCellParams,
+ GridRenderEditCellParams,
+ useGridApiRef,
+} from "@mui/x-data-grid";
+import InputDataGrid from "../InputDataGrid";
+import { TableRow } from "../InputDataGrid/InputDataGrid";
+import TwoLineCell from "./TwoLineCell";
+import QcSelect from "./QcSelect";
+import { QcItemWithChecks } from "@/app/api/qc";
+import { GridEditInputCell } from "@mui/x-data-grid";
+import { StockInLine } from "@/app/api/po";
+import { WarehouseResult } from "@/app/api/warehouse";
+import {
+ OUTPUT_DATE_FORMAT,
+ stockInLineStatusMap,
+} from "@/app/utils/formatUtil";
+import { QRCodeSVG } from "qrcode.react";
+import { QrCode } from "../QrCode";
+import ReactQrCodeScanner, {
+ ScannerConfig,
+} from "../ReactQrCodeScanner/ReactQrCodeScanner";
+import { QrCodeInfo } from "@/app/api/qrcode";
+import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
+import dayjs from "dayjs";
+import arraySupport from "dayjs/plugin/arraySupport";
+import { dummyPutawayLine } from "./dummyQcTemplate";
+dayjs.extend(arraySupport);
+
+interface Props {
+ itemDetail: StockInLine;
+ warehouse: WarehouseResult[];
+ disabled: boolean;
+ // qc: QcItemWithChecks[];
+}
+type EntryError =
+ | {
+ [field in keyof PutAwayLine]?: string;
+ }
+ | undefined;
+
+type PutawayRow = TableRow, EntryError>;
+
+const style = {
+ position: "absolute",
+ top: "50%",
+ left: "50%",
+ transform: "translate(-50%, -50%)",
+ bgcolor: "background.paper",
+ pt: 5,
+ px: 5,
+ pb: 10,
+ width: "auto",
+};
+
+const PutawayForm: React.FC = ({ itemDetail, warehouse, disabled }) => {
+ const { t } = useTranslation("purchaseOrder");
+ const apiRef = useGridApiRef();
+ const {
+ register,
+ formState: { errors, defaultValues, touchedFields },
+ watch,
+ control,
+ setValue,
+ getValues,
+ reset,
+ resetField,
+ setError,
+ clearErrors,
+ } = useFormContext();
+ console.log(itemDetail);
+ // const [recordQty, setRecordQty] = useState(0);
+ const [warehouseId, setWarehouseId] = useState(itemDetail.defaultWarehouseId);
+ const filteredWarehouse = useMemo(() => {
+ // do filtering here if any
+ return warehouse;
+ }, []);
+
+ const defaultOption = {
+ value: 0, // think think sin
+ label: t("Select warehouse"),
+ group: "default",
+ };
+ const options = useMemo(() => {
+ return [
+ // {
+ // value: 0, // think think sin
+ // label: t("Select warehouse"),
+ // group: "default",
+ // },
+ ...filteredWarehouse.map((w) => ({
+ value: w.id,
+ label: `${w.code} - ${w.name}`,
+ group: "existing",
+ })),
+ ];
+ }, [filteredWarehouse]);
+ const currentValue =
+ warehouseId > 0
+ ? options.find((o) => o.value === warehouseId)
+ : options.find((o) => o.value === getValues("warehouseId")) ||
+ defaultOption;
+
+ const onChange = useCallback(
+ (
+ event: React.SyntheticEvent,
+ newValue: { value: number; group: string } | { value: number }[],
+ ) => {
+ const singleNewVal = newValue as {
+ value: number;
+ group: string;
+ };
+ console.log(singleNewVal);
+ console.log("onChange");
+ // setValue("warehouseId", singleNewVal.value);
+ setWarehouseId(singleNewVal.value);
+ },
+ [],
+ );
+ console.log(watch("putAwayLines"))
+ // const accQty = watch("acceptedQty");
+ // const validateForm = useCallback(() => {
+ // console.log(accQty);
+ // if (accQty > itemDetail.acceptedQty) {
+ // setError("acceptedQty", {
+ // message: `acceptedQty must not greater than ${itemDetail.acceptedQty}`,
+ // type: "required",
+ // });
+ // }
+ // if (accQty < 1) {
+ // setError("acceptedQty", {
+ // message: `minimal value is 1`,
+ // type: "required",
+ // });
+ // }
+ // if (isNaN(accQty)) {
+ // setError("acceptedQty", {
+ // message: `value must be a number`,
+ // type: "required",
+ // });
+ // }
+ // }, [accQty]);
+
+ // useEffect(() => {
+ // clearErrors();
+ // validateForm();
+ // }, [validateForm]);
+
+ const qrContent = useMemo(
+ () => ({
+ stockInLineId: itemDetail.id,
+ itemId: itemDetail.itemId,
+ lotNo: itemDetail.lotNo,
+ // warehouseId: 2 // for testing
+ // expiryDate: itemDetail.expiryDate,
+ // productionDate: itemDetail.productionDate,
+ // supplier: itemDetail.supplier,
+ // poCode: itemDetail.poCode,
+ }),
+ [itemDetail],
+ );
+ const [isOpenScanner, setOpenScanner] = useState(false);
+
+ const closeHandler = useCallback>(
+ (...args) => {
+ setOpenScanner(false);
+ },
+ [],
+ );
+
+ const onOpenScanner = useCallback(() => {
+ setOpenScanner(true);
+ }, []);
+
+ const onCloseScanner = useCallback(() => {
+ setOpenScanner(false);
+ }, []);
+ const scannerConfig = useMemo(
+ () => ({
+ onUpdate: (err, result) => {
+ console.log(result);
+ console.log(Boolean(result));
+ if (result) {
+ const data: QrCodeInfo = JSON.parse(result.getText());
+ console.log(data);
+ if (data.warehouseId) {
+ console.log(data.warehouseId);
+ setWarehouseId(data.warehouseId);
+ onCloseScanner();
+ }
+ } else return;
+ },
+ }),
+ [onCloseScanner],
+ );
+
+ // QR Code Scanner
+ const scanner = useQrCodeScannerContext();
+ useEffect(() => {
+ if (isOpenScanner) {
+ scanner.startScan();
+ } else if (!isOpenScanner) {
+ scanner.stopScan();
+ }
+ }, [isOpenScanner]);
+
+ useEffect(() => {
+ if (scanner.values.length > 0) {
+ console.log(scanner.values[0]);
+ const data: QrCodeInfo = JSON.parse(scanner.values[0]);
+ console.log(data);
+ if (data.warehouseId) {
+ console.log(data.warehouseId);
+ setWarehouseId(data.warehouseId);
+ onCloseScanner();
+ }
+ scanner.resetScan();
+ }
+ }, [scanner.values]);
+
+ useEffect(() => {
+ setValue("status", "completed");
+ setValue("warehouseId", options[0].value);
+ }, []);
+
+ useEffect(() => {
+ if (warehouseId > 0) {
+ setValue("warehouseId", warehouseId);
+ clearErrors("warehouseId");
+ }
+ }, [warehouseId]);
+
+ const getWarningTextHardcode = useCallback((): string | undefined => {
+ console.log(options)
+ if (options.length === 0) return undefined
+ const defaultWarehouseId = options[0].value;
+ const currWarehouseId = watch("warehouseId");
+ if (defaultWarehouseId !== currWarehouseId) {
+ return t("not default warehosue");
+ }
+ return undefined;
+ }, [options]);
+
+ const columns = useMemo(
+ () => [
+ {
+ field: "qty",
+ headerName: t("qty"),
+ flex: 1,
+ // renderCell(params) {
+ // return <>100>
+ // },
+ },
+ {
+ field: "warehouse",
+ headerName: t("warehouse"),
+ flex: 1,
+ // renderCell(params) {
+ // return <>{filteredWarehouse[0].name}>
+ // },
+ },
+ {
+ field: "printQty",
+ headerName: t("printQty"),
+ flex: 1,
+ // renderCell(params) {
+ // return <>100>
+ // },
+ },
+ ], [])
+
+ const validation = useCallback(
+ (newRow: GridRowModel): EntryError => {
+ const error: EntryError = {};
+ const { qty, warehouseId, printQty } = newRow;
+
+ return Object.keys(error).length > 0 ? error : undefined;
+ },
+ [],
+ );
+
+ return (
+
+
+
+ {t("Putaway Detail")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ option.label}
+ options={options}
+ renderInput={(params) => (
+
+ )}
+ />
+
+
+ {/*
+
+
+
+
+ */}
+ {/*
+ {
+ console.log(field);
+ return (
+ o.value == field.value)}
+ onChange={onChange}
+ getOptionLabel={(option) => option.label}
+ options={options}
+ renderInput={(params) => (
+
+ )}
+ />
+ );
+ }}
+ />
+
+ 0
+ // ? options.find((o) => o.value === warehouseId)
+ // : undefined}
+ defaultValue={options[0]}
+ // defaultValue={options.find((o) => o.value === 1)}
+ value={currentValue}
+ onChange={onChange}
+ getOptionLabel={(option) => option.label}
+ options={options}
+ renderInput={(params) => (
+
+ )}
+ />
+
+ */}
+
+ {/* */}
+
+ apiRef={apiRef}
+ checkboxSelection={false}
+ _formKey={"putAwayLines"}
+ columns={columns}
+ validateRow={validation}
+ needAdd={true}
+ showRemoveBtn={false}
+ />
+
+
+ {/*
+
+ */}
+
+
+
+
+ {t("Please scan warehouse qr code.")}
+
+ {/* */}
+
+
+
+ );
+};
+export default PutawayForm;
diff --git a/src/components/Jodetail/QCDatagrid.tsx b/src/components/Jodetail/QCDatagrid.tsx
new file mode 100644
index 0000000..b9947db
--- /dev/null
+++ b/src/components/Jodetail/QCDatagrid.tsx
@@ -0,0 +1,395 @@
+"use client";
+import {
+ Dispatch,
+ MutableRefObject,
+ SetStateAction,
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import StyledDataGrid from "../StyledDataGrid";
+import {
+ FooterPropsOverrides,
+ GridActionsCellItem,
+ GridCellParams,
+ GridColDef,
+ GridEventListener,
+ GridRowEditStopReasons,
+ GridRowId,
+ GridRowIdGetter,
+ GridRowModel,
+ GridRowModes,
+ GridRowModesModel,
+ GridRowSelectionModel,
+ GridToolbarContainer,
+ GridValidRowModel,
+ useGridApiRef,
+} from "@mui/x-data-grid";
+import { set, useFormContext } from "react-hook-form";
+import SaveIcon from "@mui/icons-material/Save";
+import DeleteIcon from "@mui/icons-material/Delete";
+import CancelIcon from "@mui/icons-material/Cancel";
+import { Add } from "@mui/icons-material";
+import { Box, Button, Typography } from "@mui/material";
+import { useTranslation } from "react-i18next";
+import {
+ GridApiCommunity,
+ GridSlotsComponentsProps,
+} from "@mui/x-data-grid/internals";
+import { dummyQCData } from "./dummyQcTemplate";
+// T == CreatexxxInputs map of the form's fields
+// V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc
+// E == error
+interface ResultWithId {
+ id: string | number;
+}
+// export type InputGridProps = {
+// [key: string]: any
+// }
+interface DefaultResult {
+ _isNew: boolean;
+ _error: E;
+}
+
+interface SelectionResult {
+ active: boolean;
+ _isNew: boolean;
+ _error: E;
+}
+type Result = DefaultResult | SelectionResult;
+
+export type TableRow = Partial<
+ V & {
+ isActive: boolean | undefined;
+ _isNew: boolean;
+ _error: E;
+ } & ResultWithId
+>;
+
+export interface InputDataGridProps {
+ apiRef: MutableRefObject;
+// checkboxSelection: false | undefined;
+ _formKey: keyof T;
+ columns: GridColDef[];
+ validateRow: (newRow: GridRowModel>) => E;
+ needAdd?: boolean;
+}
+
+export interface SelectionInputDataGridProps {
+ // thinking how do
+ apiRef: MutableRefObject;
+// checkboxSelection: true;
+ _formKey: keyof T;
+ columns: GridColDef[];
+ validateRow: (newRow: GridRowModel>) => E;
+}
+
+export type Props =
+ | InputDataGridProps
+ | SelectionInputDataGridProps;
+export class ProcessRowUpdateError extends Error {
+ public readonly row: T;
+ public readonly errors: E | undefined;
+ constructor(row: T, message?: string, errors?: E) {
+ super(message);
+ this.row = row;
+ this.errors = errors;
+
+ Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
+ }
+}
+// T == CreatexxxInputs map of the form's fields
+// V == target field input inside CreatexxxInputs, e.g. qcChecks: ItemQc[], V = ItemQc
+// E == error
+function InputDataGrid({
+ apiRef,
+// checkboxSelection = false,
+ _formKey,
+ columns,
+ validateRow,
+}: Props) {
+ const {
+ t,
+ // i18n: { language },
+ } = useTranslation("purchaseOrder");
+ const formKey = _formKey.toString();
+ const { setValue, getValues } = useFormContext();
+ const [rowModesModel, setRowModesModel] = useState({});
+ // const apiRef = useGridApiRef();
+ const getRowId = useCallback>>(
+ (row) => row.id! as number,
+ [],
+ );
+ const formValue = getValues(formKey)
+ const list: TableRow[] = !formValue || formValue.length == 0 ? dummyQCData : getValues(formKey);
+ console.log(list)
+ const [rows, setRows] = useState[]>(() => {
+ // const list: TableRow[] = getValues(formKey);
+ console.log(list)
+ return list && list.length > 0 ? list : [];
+ });
+ console.log(rows)
+ // const originalRows = list && list.length > 0 ? list : [];
+ const originalRows = useMemo(() => (
+ list && list.length > 0 ? list : []
+ ), [list])
+
+ // const originalRowModel = originalRows.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel
+ const [rowSelectionModel, setRowSelectionModel] =
+ useState(() => {
+ // const rowModel = list.filter((li) => li.isActive).map(i => i.id) as GridRowSelectionModel
+ const rowModel: GridRowSelectionModel = getValues(
+ `${formKey}_active`,
+ ) as GridRowSelectionModel;
+ console.log(rowModel);
+ return rowModel;
+ });
+
+ useEffect(() => {
+ for (let i = 0; i < rows.length; i++) {
+ const currRow = rows[i]
+ setRowModesModel((prevRowModesModel) => ({
+ ...prevRowModesModel,
+ [currRow.id as number]: { mode: GridRowModes.View },
+ }));
+ }
+ }, [rows])
+
+ const handleSave = useCallback(
+ (id: GridRowId) => () => {
+ setRowModesModel((prevRowModesModel) => ({
+ ...prevRowModesModel,
+ [id]: { mode: GridRowModes.View },
+ }));
+ },
+ [],
+ );
+ const onProcessRowUpdateError = useCallback(
+ (updateError: ProcessRowUpdateError) => {
+ const errors = updateError.errors;
+ const row = updateError.row;
+ console.log(errors);
+ apiRef.current.updateRows([{ ...row, _error: errors }]);
+ },
+ [apiRef],
+ );
+
+ const processRowUpdate = useCallback(
+ (
+ newRow: GridRowModel>,
+ originalRow: GridRowModel>,
+ ) => {
+ /////////////////
+ // validation here
+ const errors = validateRow(newRow);
+ console.log(newRow);
+ if (errors) {
+ throw new ProcessRowUpdateError(
+ originalRow,
+ "validation error",
+ errors,
+ );
+ }
+ /////////////////
+ const { _isNew, _error, ...updatedRow } = newRow;
+ const rowToSave = {
+ ...updatedRow,
+ } as TableRow; /// test
+ console.log(rowToSave);
+ setRows((rw) =>
+ rw.map((r) => (getRowId(r) === getRowId(originalRow) ? rowToSave : r)),
+ );
+ return rowToSave;
+ },
+ [validateRow, getRowId],
+ );
+
+ const addRow = useCallback(() => {
+ const newEntry = { id: Date.now(), _isNew: true } as TableRow;
+ setRows((prev) => [...prev, newEntry]);
+ setRowModesModel((model) => ({
+ ...model,
+ [getRowId(newEntry)]: {
+ mode: GridRowModes.Edit,
+ // fieldToFocus: "team", /// test
+ },
+ }));
+ }, [getRowId]);
+
+ const reset = useCallback(() => {
+ setRowModesModel({});
+ setRows(originalRows);
+ }, [originalRows]);
+
+ const handleCancel = useCallback(
+ (id: GridRowId) => () => {
+ setRowModesModel((model) => ({
+ ...model,
+ [id]: { mode: GridRowModes.View, ignoreModifications: true },
+ }));
+ const editedRow = rows.find((row) => getRowId(row) === id);
+ if (editedRow?._isNew) {
+ setRows((rw) => rw.filter((r) => getRowId(r) !== id));
+ } else {
+ setRows((rw) =>
+ rw.map((r) => (getRowId(r) === id ? { ...r, _error: undefined } : r)),
+ );
+ }
+ },
+ [rows, getRowId],
+ );
+
+ const handleDelete = useCallback(
+ (id: GridRowId) => () => {
+ setRows((prevRows) => prevRows.filter((row) => getRowId(row) !== id));
+ },
+ [getRowId],
+ );
+
+ const _columns = useMemo(
+ () => [
+ ...columns,
+ {
+ field: "actions",
+ type: "actions",
+ headerName: "",
+ flex: 0.5,
+ cellClassName: "actions",
+ getActions: ({ id }: { id: GridRowId }) => {
+ const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
+ if (isInEditMode) {
+ return [
+ }
+ label="Save"
+ key="edit"
+ sx={{
+ color: "primary.main",
+ }}
+ onClick={handleSave(id)}
+ />,
+ }
+ label="Cancel"
+ key="edit"
+ onClick={handleCancel(id)}
+ />,
+ ];
+ }
+ return [
+ }
+ label="Delete"
+ sx={{
+ color: "error.main",
+ }}
+ onClick={handleDelete(id)}
+ color="inherit"
+ key="edit"
+ />,
+ ];
+ },
+ },
+ ],
+ [columns, rowModesModel, handleSave, handleCancel, handleDelete],
+ );
+ // sync useForm
+ useEffect(() => {
+ // console.log(formKey)
+ // console.log(rows)
+ setValue(formKey, rows);
+ }, [formKey, rows, setValue]);
+
+ const footer = (
+
+ }
+ onClick={addRow}
+ size="small"
+ >
+ 新增
+ {/* {t("Add Record")} */}
+
+ }
+ onClick={reset}
+ size="small"
+ >
+ {/* {t("Clean Record")} */}
+ 清除
+
+
+ );
+ // const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => {
+ // if (params.reason === GridRowEditStopReasons.rowFocusOut) {
+ // event.defaultMuiPrevented = true;
+ // }
+ // };
+
+ return (
+ }
+ rowSelectionModel={rowSelectionModel}
+ apiRef={apiRef}
+ rows={rows}
+ columns={columns}
+ editMode="row"
+ autoHeight
+ sx={{
+ "--DataGrid-overlayHeight": "100px",
+ ".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
+ border: "1px solid",
+ borderColor: "error.main",
+ },
+ ".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
+ border: "1px solid",
+ borderColor: "warning.main",
+ },
+ }}
+ disableColumnMenu
+ processRowUpdate={processRowUpdate as any}
+ // onRowEditStop={handleRowEditStop}
+ rowModesModel={rowModesModel}
+ onRowModesModelChange={setRowModesModel}
+ onProcessRowUpdateError={onProcessRowUpdateError}
+ getCellClassName={(params: GridCellParams>) => {
+ let classname = "";
+ if (params.row._error) {
+ classname = "hasError";
+ }
+ return classname;
+ }}
+ slots={{
+ // footer: FooterToolbar,
+ noRowsOverlay: NoRowsOverlay,
+ }}
+ // slotProps={{
+ // footer: { child: footer },
+ // }
+ // }
+ />
+ );
+}
+const FooterToolbar: React.FC = ({ child }) => {
+ return {child};
+};
+const NoRowsOverlay: React.FC = () => {
+ const { t } = useTranslation("home");
+ return (
+
+ {t("Add some entries!")}
+
+ );
+};
+export default InputDataGrid;
diff --git a/src/components/Jodetail/QcFormVer2.tsx b/src/components/Jodetail/QcFormVer2.tsx
new file mode 100644
index 0000000..ebea29d
--- /dev/null
+++ b/src/components/Jodetail/QcFormVer2.tsx
@@ -0,0 +1,460 @@
+"use client";
+
+import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions";
+import {
+ Box,
+ Card,
+ CardContent,
+ Checkbox,
+ FormControl,
+ FormControlLabel,
+ Grid,
+ Radio,
+ RadioGroup,
+ Stack,
+ Tab,
+ Tabs,
+ TabsProps,
+ TextField,
+ Tooltip,
+ Typography,
+} from "@mui/material";
+import { useFormContext, Controller } from "react-hook-form";
+import { useTranslation } from "react-i18next";
+import StyledDataGrid from "../StyledDataGrid";
+import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react";
+import {
+ GridColDef,
+ GridRowIdGetter,
+ GridRowModel,
+ useGridApiContext,
+ GridRenderCellParams,
+ GridRenderEditCellParams,
+ useGridApiRef,
+ GridRowSelectionModel,
+} from "@mui/x-data-grid";
+import InputDataGrid from "../InputDataGrid";
+import { TableRow } from "../InputDataGrid/InputDataGrid";
+import TwoLineCell from "./TwoLineCell";
+import QcSelect from "./QcSelect";
+import { GridEditInputCell } from "@mui/x-data-grid";
+import { StockInLine } from "@/app/api/po";
+import { stockInLineStatusMap } from "@/app/utils/formatUtil";
+import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions";
+import { QcItemWithChecks } from "@/app/api/qc";
+import axios from "@/app/(main)/axios/axiosInstance";
+import { NEXT_PUBLIC_API_URL } from "@/config/api";
+import axiosInstance from "@/app/(main)/axios/axiosInstance";
+import EscalationComponent from "./EscalationComponent";
+import QcDataGrid from "./QCDatagrid";
+import StockInFormVer2 from "./StockInFormVer2";
+import { dummyEscalationHistory, dummyQCData, QcData } from "./dummyQcTemplate";
+import { ModalFormInput } from "@/app/api/po/actions";
+import { escape } from "lodash";
+
+interface Props {
+ itemDetail: StockInLine;
+ qc: QcItemWithChecks[];
+ disabled: boolean;
+ qcItems: QcData[]
+ setQcItems: Dispatch>
+}
+
+type EntryError =
+ | {
+ [field in keyof QcData]?: string;
+ }
+ | undefined;
+
+type QcRow = TableRow, EntryError>;
+// fetchQcItemCheck
+const QcFormVer2: React.FC = ({ qc, itemDetail, disabled, qcItems, setQcItems }) => {
+ const { t } = useTranslation("purchaseOrder");
+ const apiRef = useGridApiRef();
+ const {
+ register,
+ formState: { errors, defaultValues, touchedFields },
+ watch,
+ control,
+ setValue,
+ getValues,
+ reset,
+ resetField,
+ setError,
+ clearErrors,
+ } = useFormContext();
+
+ const [tabIndex, setTabIndex] = useState(0);
+ const [rowSelectionModel, setRowSelectionModel] = useState();
+ const [escalationHistory, setEscalationHistory] = useState(dummyEscalationHistory);
+ const [qcResult, setQcResult] = useState();
+ const qcAccept = watch("qcAccept");
+ // const [qcAccept, setQcAccept] = useState(true);
+ // const [qcItems, setQcItems] = useState(dummyQCData)
+
+ const column = useMemo(
+ () => [
+ {
+ field: "escalation",
+ headerName: t("escalation"),
+ flex: 1,
+ },
+ {
+ field: "supervisor",
+ headerName: t("supervisor"),
+ flex: 1,
+ },
+ ], []
+ )
+ const handleTabChange = useCallback>(
+ (_e, newValue) => {
+ setTabIndex(newValue);
+ },
+ [],
+ );
+
+ //// validate form
+ const accQty = watch("acceptQty");
+ const validateForm = useCallback(() => {
+ console.log(accQty);
+ if (accQty > itemDetail.acceptedQty) {
+ setError("acceptQty", {
+ message: `${t("acceptQty must not greater than")} ${
+ itemDetail.acceptedQty
+ }`,
+ type: "required",
+ });
+ }
+ if (accQty < 1) {
+ setError("acceptQty", {
+ message: t("minimal value is 1"),
+ type: "required",
+ });
+ }
+ if (isNaN(accQty)) {
+ setError("acceptQty", {
+ message: t("value must be a number"),
+ type: "required",
+ });
+ }
+ }, [accQty]);
+
+ useEffect(() => {
+ clearErrors();
+ validateForm();
+ }, [clearErrors, validateForm]);
+
+ const columns = useMemo(
+ () => [
+ {
+ field: "escalation",
+ headerName: t("escalation"),
+ flex: 1,
+ },
+ {
+ field: "supervisor",
+ headerName: t("supervisor"),
+ flex: 1,
+ },
+ ],
+ [],
+ );
+ /// validate datagrid
+ const validation = useCallback(
+ (newRow: GridRowModel): EntryError => {
+ const error: EntryError = {};
+ // const { qcItemId, failQty } = newRow;
+ return Object.keys(error).length > 0 ? error : undefined;
+ },
+ [],
+ );
+
+ function BooleanEditCell(params: GridRenderEditCellParams) {
+ const apiRef = useGridApiContext();
+ const { id, field, value } = params;
+
+ const handleChange = (e: React.ChangeEvent) => {
+ apiRef.current.setEditCellValue({ id, field, value: e.target.checked });
+ apiRef.current.stopCellEditMode({ id, field }); // commit immediately
+ };
+
+ return ;
+}
+
+ const qcColumns: GridColDef[] = [
+ {
+ field: "qcItem",
+ headerName: t("qcItem"),
+ flex: 2,
+ renderCell: (params) => (
+
+ {params.value}
+ {params.row.qcDescription}
+
+ ),
+ },
+ {
+ field: 'isPassed',
+ headerName: t("qcResult"),
+ flex: 1.5,
+ renderCell: (params) => {
+ const currentValue = params.value;
+ return (
+
+ {
+ const value = e.target.value;
+ setQcItems((prev) =>
+ prev.map((r): QcData => (r.id === params.id ? { ...r, isPassed: value === "true" } : r))
+ );
+ }}
+ name={`isPassed-${params.id}`}
+ >
+ }
+ label="合格"
+ sx={{
+ color: currentValue === true ? "green" : "inherit",
+ "& .Mui-checked": {color: "green"}
+ }}
+ />
+ }
+ label="不合格"
+ sx={{
+ color: currentValue === false ? "red" : "inherit",
+ "& .Mui-checked": {color: "red"}
+ }}
+ />
+
+
+ );
+ },
+ },
+ {
+ field: "failedQty",
+ headerName: t("failedQty"),
+ flex: 1,
+ // editable: true,
+ renderCell: (params) => (
+ {
+ const v = e.target.value;
+ const next = v === '' ? undefined : Number(v);
+ if (Number.isNaN(next)) return;
+ setQcItems((prev) =>
+ prev.map((r) => (r.id === params.id ? { ...r, failedQty: next } : r))
+ );
+ }}
+ onClick={(e) => e.stopPropagation()}
+ onMouseDown={(e) => e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ inputProps={{ min: 0 }}
+ sx={{ width: '100%' }}
+ />
+ ),
+ },
+ {
+ field: "remarks",
+ headerName: t("remarks"),
+ flex: 2,
+ renderCell: (params) => (
+ {
+ const remarks = e.target.value;
+ // const next = v === '' ? undefined : Number(v);
+ // if (Number.isNaN(next)) return;
+ setQcItems((prev) =>
+ prev.map((r) => (r.id === params.id ? { ...r, remarks: remarks } : r))
+ );
+ }}
+ onClick={(e) => e.stopPropagation()}
+ onMouseDown={(e) => e.stopPropagation()}
+ onKeyDown={(e) => e.stopPropagation()}
+ inputProps={{ min: 0 }}
+ sx={{ width: '100%' }}
+ />
+ ),
+ },
+ ]
+
+ useEffect(() => {
+ console.log(itemDetail);
+
+ }, [itemDetail]);
+
+ // Set initial value for acceptQty
+ useEffect(() => {
+ if (itemDetail?.acceptedQty !== undefined) {
+ setValue("acceptQty", itemDetail.acceptedQty);
+ }
+ }, [itemDetail?.acceptedQty, setValue]);
+
+ // const [openCollapse, setOpenCollapse] = useState(false)
+ const [isCollapsed, setIsCollapsed] = useState(false);
+
+ const onFailedOpenCollapse = useCallback((qcItems: QcData[]) => {
+ const isFailed = qcItems.some((qc) => !qc.isPassed)
+ console.log(isFailed)
+ if (isFailed) {
+ setIsCollapsed(true)
+ } else {
+ setIsCollapsed(false)
+ }
+ }, [])
+
+ // const handleRadioChange = useCallback((event: React.ChangeEvent) => {
+ // const value = event.target.value === 'true';
+ // setValue("qcAccept", value);
+ // }, [setValue]);
+
+
+ useEffect(() => {
+ console.log(itemDetail);
+
+ }, [itemDetail]);
+
+ useEffect(() => {
+ // onFailedOpenCollapse(qcItems) // This function is no longer needed
+ }, [qcItems]); // Removed onFailedOpenCollapse from dependency array
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {tabIndex == 0 && (
+ <>
+
+ {/*
+ apiRef={apiRef}
+ columns={qcColumns}
+ _formKey="qcResult"
+ validateRow={validation}
+ /> */}
+
+
+
+
+ {/*
+
+ */}
+ >
+ )}
+ {tabIndex == 1 && (
+ <>
+ {/*
+
+ */}
+
+
+ {t("Escalation Info")}
+
+
+
+ {
+ setRowSelectionModel(newRowSelectionModel);
+ }}
+ />
+
+ >
+ )}
+
+
+ (
+