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..19e2640
--- /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 detail"
+}
+
+const jo: React.FC = async () => {
+ const { t } = await getServerI18n("jo");
+
+ preloadBomCombo()
+
+ return (
+ <>
+
+
+ {t("Job Order detail")}
+
+
+
+ }>
+
+
+
+ >
+ )
+}
+
+export default jo;
\ No newline at end of file
diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
index 3631e80..ff2150e 100644
--- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
+++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
@@ -425,8 +425,8 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => {
}}>
-
-
+
+
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/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/FinishedGoodSearchWrapper.tsx b/src/components/Jodetail/FinishedGoodSearchWrapper.tsx
new file mode 100644
index 0000000..1df245d
--- /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 "./FinishedGoodSearch";
+
+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/GoodPickExecution.tsx b/src/components/Jodetail/GoodPickExecution.tsx
new file mode 100644
index 0000000..1bd52b8
--- /dev/null
+++ b/src/components/Jodetail/GoodPickExecution.tsx
@@ -0,0 +1,1250 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Stack,
+ TextField,
+ Typography,
+ Alert,
+ CircularProgress,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ TablePagination,
+ Modal,
+} from "@mui/material";
+import { useCallback, useEffect, useState, useRef, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { useRouter } from "next/navigation";
+import {
+ fetchALLPickOrderLineLotDetails,
+ updateStockOutLineStatus,
+ createStockOutLine,
+ recordPickExecutionIssue,
+ fetchFGPickOrders, // ✅ Add this import
+ FGPickOrderResponse,
+ autoAssignAndReleasePickOrder,
+ AutoAssignReleaseResponse,
+ checkPickOrderCompletion,
+ PickOrderCompletionResponse,
+ checkAndCompletePickOrderByConsoCode
+} from "@/app/api/pickOrder/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 "./GoodPickExecutionForm";
+import FGPickOrderCard from "./FGPickOrderCard";
+interface Props {
+ filterArgs: Record;
+}
+
+// ✅ QR Code Modal Component (from LotTable)
+const QrCodeModal: React.FC<{
+ open: boolean;
+ onClose: () => void;
+ lot: any | null;
+ onQrCodeSubmit: (lotNo: string) => void;
+ combinedLotData: any[]; // ✅ Add this prop
+}> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
+ const { t } = useTranslation("pickOrder");
+ 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('');
+ const [fgPickOrder, setFgPickOrder] = useState(null);
+ // 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 PickExecution: React.FC = ({ filterArgs }) => {
+ const { t } = useTranslation("pickOrder");
+ const router = useRouter();
+ const { data: session } = useSession() as { data: SessionWithTokens | null };
+
+ const currentUserId = session?.id ? parseInt(session.id) : undefined;
+
+ const [combinedLotData, setCombinedLotData] = useState([]);
+ const [combinedDataLoading, setCombinedDataLoading] = useState(false);
+ const [originalCombinedData, setOriginalCombinedData] = useState([]);
+
+ 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);
+ 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
+ };
+
+ useEffect(() => {
+ startScan();
+ return () => {
+ stopScan();
+ resetScan();
+ };
+ }, [startScan, stopScan, resetScan]);
+
+ const fetchAllCombinedLotData = useCallback(async (userId?: number) => {
+ setCombinedDataLoading(true);
+ try {
+ const userIdToUse = userId || currentUserId;
+
+ console.log(" fetchAllCombinedLotData called with userId:", userIdToUse);
+
+ if (!userIdToUse) {
+ console.warn("⚠️ No userId available, skipping API call");
+ setCombinedLotData([]);
+ setOriginalCombinedData([]);
+ return;
+ }
+
+ // ✅ Use the non-auto-assign endpoint - this only fetches existing data
+ const allLotDetails = await fetchALLPickOrderLineLotDetails(userIdToUse);
+ console.log("✅ All combined lot details:", allLotDetails);
+ setCombinedLotData(allLotDetails);
+ setOriginalCombinedData(allLotDetails);
+
+ // ✅ 计算完成状态并发送事件
+ const allCompleted = allLotDetails.length > 0 && allLotDetails.every(lot =>
+ lot.processingStatus === 'completed'
+ );
+
+ // ✅ 发送完成状态事件,包含标签页信息
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: allCompleted,
+ tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件
+ }
+ }));
+
+ } catch (error) {
+ console.error("❌ Error fetching combined lot data:", error);
+ setCombinedLotData([]);
+ setOriginalCombinedData([]);
+
+ // ✅ 如果加载失败,禁用打印按钮
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: false,
+ tabIndex: 0
+ }
+ }));
+ } finally {
+ setCombinedDataLoading(false);
+ }
+ }, [currentUserId, combinedLotData]);
+
+ // ✅ Only fetch existing data when session is ready, no auto-assignment
+ useEffect(() => {
+ if (session && currentUserId && !initializationRef.current) {
+ console.log("✅ Session loaded, initializing pick order...");
+ initializationRef.current = true;
+
+ // ✅ Only fetch existing data, no auto-assignment
+ fetchAllCombinedLotData();
+ }
+ }, [session, currentUserId, fetchAllCombinedLotData]);
+
+ // ✅ Add event listener for manual assignment
+ useEffect(() => {
+ const handlePickOrderAssigned = () => {
+ console.log("🔄 Pick order assigned event received, refreshing data...");
+ fetchAllCombinedLotData();
+ };
+
+ window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
+
+ return () => {
+ window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
+ };
+ }, [fetchAllCombinedLotData]);
+
+ // ✅ Handle QR code submission for matched lot (external scanning)
+ // ✅ 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);
+ return;
+ }
+
+ console.log(`✅ Found ${matchingLots.length} matching lots:`, matchingLots);
+ setQrScanError(false);
+
+ try {
+ let successCount = 0;
+ let existsCount = 0;
+ let errorCount = 0;
+
+ for (const matchingLot of matchingLots) {
+ console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
+
+ if (matchingLot.stockOutLineId) {
+ console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
+ existsCount++;
+ } else {
+ const stockOutLineData: CreateStockOutLine = {
+ consoCode: matchingLot.pickOrderConsoCode,
+ pickOrderLineId: matchingLot.pickOrderLineId,
+ inventoryLotLineId: matchingLot.lotId,
+ qty: 0.0
+ };
+
+ console.log(`Creating stock out line for pick order line ${matchingLot.pickOrderLineId}:`, stockOutLineData);
+ const result = await createStockOutLine(stockOutLineData);
+ console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result);
+
+ if (result && result.code === "EXISTS") {
+ console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
+ existsCount++;
+ } else if (result && result.code === "SUCCESS") {
+ console.log(`✅ Stock out line created successfully for line ${matchingLot.pickOrderLineId}`);
+ successCount++;
+ } else {
+ console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result);
+ errorCount++;
+ }
+ }
+ }
+
+ // ✅ Always refresh data after processing (success or failure)
+ console.log("🔄 Refreshing data after QR code processing...");
+ await fetchAllCombinedLotData();
+
+ if (successCount > 0 || existsCount > 0) {
+ console.log(`✅ QR Code processing completed: ${successCount} created, ${existsCount} already existed`);
+ setQrScanSuccess(true);
+ setQrScanInput(''); // Clear input after successful processing
+
+ // ✅ Clear success state after a delay
+ setTimeout(() => {
+ setQrScanSuccess(false);
+ }, 2000);
+ } else {
+ console.error(`❌ QR Code processing failed: ${errorCount} errors`);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+
+ // ✅ Clear error state after a delay
+ setTimeout(() => {
+ setQrScanError(false);
+ }, 3000);
+ }
+ } catch (error) {
+ console.error("❌ Error processing QR code:", error);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+
+ // ✅ Still refresh data even on error
+ await fetchAllCombinedLotData();
+
+ // ✅ Clear error state after a delay
+ setTimeout(() => {
+ setQrScanError(false);
+ }, 3000);
+ }
+ }, [combinedLotData, fetchAllCombinedLotData]);
+
+ 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, // ✅ Use pickOrderConsoCode instead of pickOrderCode
+ 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 fetchAllCombinedLotData();
+ } catch (error) {
+ console.error("Error creating stock out line:", error);
+ }
+ }
+ }, [selectedLotForQr, fetchAllCombinedLotData]);
+
+ // ✅ 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 fetchAllCombinedLotData();
+ console.log("Pick quantity submitted successfully!");
+
+ setTimeout(() => {
+ checkAndAutoAssignNext();
+ }, 1000);
+
+ } catch (error) {
+ console.error("Error submitting pick quantity:", error);
+ }
+ }, [pickQtyData, fetchAllCombinedLotData, 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 fetchAllCombinedLotData();
+ console.log("Lot rejected successfully!");
+
+ setTimeout(() => {
+ checkAndAutoAssignNext();
+ }, 1000);
+
+ } catch (error) {
+ console.error("Error rejecting lot:", error);
+ }
+ }, [fetchAllCombinedLotData, 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 fetchAllCombinedLotData();
+ } catch (error) {
+ console.error("Error submitting pick execution form:", error);
+ }
+ }, [fetchAllCombinedLotData]);
+
+ // ✅ 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]);
+
+ return (
+
+
+ {/* Search Box */}
+
+
+ {fgPickOrdersLoading ? (
+
+
+
+ ) : (
+
+ {fgPickOrders.length === 0 ? (
+
+
+ {t("No FG pick orders found")}
+
+
+ ) : (
+ fgPickOrders.map((fgOrder) => (
+
+ ))
+ )}
+
+ )}
+
+
+
+
+ {/*
+
+
+
+
+ {t("All Pick Order Lots")}
+
+
+
+
+
+
+
+
+
+ {t("Index")}
+ {t("Route")}
+ {t("Item Name")}
+ {t("Lot#")}
+
+ {t("Lot Required Pick Qty")}
+
+ {t("Lot Actual Pick Qty")}
+
+ {t("Action")}
+
+
+
+ {paginatedData.length === 0 ? (
+
+
+
+ {t("No data available")}
+
+
+
+ ) : (
+ paginatedData.map((lot, index) => (
+
+
+
+ {lot.routerIndex || index + 1}
+
+
+
+
+ {lot.routerRoute || '-'}
+
+
+ {lot.itemName}
+
+
+
+ {lot.lotNo}
+
+
+
+
+
+ {(() => {
+ const inQty = lot.inQty || 0;
+ const outQty = lot.outQty || 0;
+ const result = inQty - outQty;
+ return result.toLocaleString();
+ })()}
+
+
+
+ {!lot.stockOutLineId ? (
+
+ ) : (
+ // ✅ When stockOutLineId exists, show TextField + Issue button
+
+ {
+ const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
+ handlePickQtyChange(lotKey, parseFloat(e.target.value) || 0);
+ }}
+ disabled={
+ (lot.lotAvailability === 'expired' ||
+ lot.lotAvailability === 'status_unavailable' ||
+ lot.lotAvailability === 'rejected') ||
+ lot.stockOutLineStatus === 'completed'
+ }
+ inputProps={{
+ min: 0,
+ max: calculateRemainingRequiredQty(lot),
+ step: 0.01
+ }}
+ sx={{
+ width: '60px',
+ height: '28px',
+ '& .MuiInputBase-input': {
+ fontSize: '0.7rem',
+ textAlign: 'center',
+ padding: '6px 8px'
+ }
+ }}
+ placeholder="0"
+ />
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+
+*/}
+ {/*
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+
+
+
+
+ {
+ setQrModalOpen(false);
+ setSelectedLotForQr(null);
+ stopScan();
+ resetScan();
+ }}
+ lot={selectedLotForQr}
+ combinedLotData={combinedLotData} // ✅ Add this prop
+ onQrCodeSubmit={handleQrCodeSubmitFromModal}
+ />
+
+
+ {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 PickExecution;
\ No newline at end of file
diff --git a/src/components/Jodetail/GoodPickExecutionForm.tsx b/src/components/Jodetail/GoodPickExecutionForm.tsx
new file mode 100644
index 0000000..b7fe86d
--- /dev/null
+++ b/src/components/Jodetail/GoodPickExecutionForm.tsx
@@ -0,0 +1,368 @@
+// 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 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];
+ }
+ };
+
+ // 计算剩余可用数量
+ 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]);
+
+ const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
+ setFormData(prev => ({ ...prev, [field]: value }));
+ // 清除错误
+ if (errors[field as keyof FormErrors]) {
+ setErrors(prev => ({ ...prev, [field]: undefined }));
+ }
+ }, [errors]);
+
+ // ✅ 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');
+ }
+
+ // ✅ 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');
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm() || !formData.pickOrderId) {
+ return;
+ }
+
+ setLoading(true);
+ try {
+ await onSubmit(formData as PickExecutionIssueData);
+ onClose();
+ } catch (error) {
+ console.error('Error submitting pick execution issue:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleClose = () => {
+ setFormData({});
+ setErrors({});
+ 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/GoodPickExecutionRecord.tsx b/src/components/Jodetail/GoodPickExecutionRecord.tsx
new file mode 100644
index 0000000..2bde71e
--- /dev/null
+++ b/src/components/Jodetail/GoodPickExecutionRecord.tsx
@@ -0,0 +1,440 @@
+"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,
+} 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 {
+ fetchALLPickOrderLineLotDetails,
+ updateStockOutLineStatus,
+ createStockOutLine,
+ recordPickExecutionIssue,
+ fetchFGPickOrders,
+ FGPickOrderResponse,
+ autoAssignAndReleasePickOrder,
+ AutoAssignReleaseResponse,
+ checkPickOrderCompletion,
+ PickOrderCompletionResponse,
+ checkAndCompletePickOrderByConsoCode,
+ fetchCompletedDoPickOrders, // ✅ 新增:使用新的 API
+ CompletedDoPickOrderResponse,
+ CompletedDoPickOrderSearchParams,
+ fetchLotDetailsByPickOrderId // ✅ 修复:导入类型
+} from "@/app/api/pickOrder/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 "./GoodPickExecutionForm";
+import FGPickOrderCard from "./FGPickOrderCard";
+
+interface Props {
+ filterArgs: Record;
+}
+
+// ✅ 新增:已完成的 DO Pick Order 接口
+interface CompletedDoPickOrder {
+ id: number;
+ pickOrderId: number;
+ pickOrderCode: string;
+ pickOrderConsoCode: string;
+ pickOrderStatus: string;
+ deliveryOrderId: number;
+ deliveryNo: string;
+ deliveryDate: string;
+ shopId: number;
+ shopCode: string;
+ shopName: string;
+ shopAddress: string;
+ ticketNo: string;
+ shopPoNo: string;
+ numberOfCartons: number;
+ truckNo: string;
+ storeId: string;
+ completedDate: string;
+ fgPickOrders: FGPickOrderResponse[];
+}
+
+// ✅ 新增:Pick Order 数据接口
+interface PickOrderData {
+ pickOrderId: number;
+ pickOrderCode: string;
+ pickOrderConsoCode: string;
+ pickOrderStatus: string;
+ completedDate: string;
+ lots: any[];
+}
+
+const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => {
+ const { t } = useTranslation("pickOrder");
+ const router = useRouter();
+ const { data: session } = useSession() as { data: SessionWithTokens | null };
+
+ const currentUserId = session?.id ? parseInt(session.id) : undefined;
+
+ // ✅ 新增:已完成 DO Pick Orders 状态
+ const [completedDoPickOrders, setCompletedDoPickOrders] = useState([]);
+ const [completedDoPickOrdersLoading, setCompletedDoPickOrdersLoading] = useState(false);
+
+ // ✅ 新增:详情视图状态
+ const [selectedDoPickOrder, setSelectedDoPickOrder] = useState(null);
+ const [showDetailView, setShowDetailView] = useState(false);
+ const [detailLotData, setDetailLotData] = useState([]);
+
+ // ✅ 新增:搜索状态
+ const [searchQuery, setSearchQuery] = useState>({});
+ const [filteredDoPickOrders, setFilteredDoPickOrders] = useState([]);
+
+ // ✅ 新增:分页状态
+ const [paginationController, setPaginationController] = useState({
+ pageNum: 0,
+ pageSize: 10,
+ });
+
+ const formProps = useForm();
+ const errors = formProps.formState.errors;
+
+ // ✅ 修改:使用新的 API 获取已完成的 DO Pick Orders
+ const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => {
+ if (!currentUserId) return;
+
+ setCompletedDoPickOrdersLoading(true);
+ try {
+ console.log("🔍 Fetching completed DO pick orders with params:", searchParams);
+
+ const completedDoPickOrders = await fetchCompletedDoPickOrders(currentUserId, searchParams);
+
+ setCompletedDoPickOrders(completedDoPickOrders);
+ setFilteredDoPickOrders(completedDoPickOrders);
+ console.log("✅ Fetched completed DO pick orders:", completedDoPickOrders);
+ } catch (error) {
+ console.error("❌ Error fetching completed DO pick orders:", error);
+ setCompletedDoPickOrders([]);
+ setFilteredDoPickOrders([]);
+ } finally {
+ setCompletedDoPickOrdersLoading(false);
+ }
+ }, [currentUserId]);
+
+ // ✅ 初始化时获取数据
+ useEffect(() => {
+ if (currentUserId) {
+ fetchCompletedDoPickOrdersData();
+ }
+ }, [currentUserId, fetchCompletedDoPickOrdersData]);
+
+ // ✅ 修改:搜索功能使用新的 API
+ const handleSearch = useCallback((query: Record) => {
+ setSearchQuery({ ...query });
+ console.log("Search query:", query);
+
+ const searchParams: CompletedDoPickOrderSearchParams = {
+ pickOrderCode: query.pickOrderCode || undefined,
+ shopName: query.shopName || undefined,
+ deliveryNo: query.deliveryNo || undefined,
+ //ticketNo: query.ticketNo || undefined,
+ };
+
+ // 使用新的 API 进行搜索
+ fetchCompletedDoPickOrdersData(searchParams);
+ }, [fetchCompletedDoPickOrdersData]);
+
+ // ✅ 修复:重命名函数避免重复声明
+ const handleSearchReset = useCallback(() => {
+ setSearchQuery({});
+ fetchCompletedDoPickOrdersData(); // 重新获取所有数据
+ }, [fetchCompletedDoPickOrdersData]);
+
+ // ✅ 分页功能
+ 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 filteredDoPickOrders.slice(startIndex, endIndex);
+ }, [filteredDoPickOrders, paginationController]);
+
+ // ✅ 搜索条件
+ const searchCriteria: Criterion[] = [
+ {
+ label: t("Pick Order Code"),
+ paramName: "pickOrderCode",
+ type: "text",
+ },
+ {
+ label: t("Shop Name"),
+ paramName: "shopName",
+ type: "text",
+ },
+ {
+ label: t("Delivery No"),
+ paramName: "deliveryNo",
+ type: "text",
+ }
+ ];
+
+ const handleDetailClick = useCallback(async (doPickOrder: CompletedDoPickOrder) => {
+ setSelectedDoPickOrder(doPickOrder);
+ setShowDetailView(true);
+
+ // ✅ 修复:使用新的 API 根据 pickOrderId 获取 lot 详情
+ try {
+ const lotDetails = await fetchLotDetailsByPickOrderId(doPickOrder.pickOrderId);
+ setDetailLotData(lotDetails);
+ console.log("✅ Loaded detail lot data for pick order:", doPickOrder.pickOrderCode, lotDetails);
+
+ // ✅ 触发打印按钮状态更新 - 基于详情数据
+ const allCompleted = lotDetails.length > 0 && lotDetails.every(lot =>
+ lot.processingStatus === 'completed'
+ );
+
+ // ✅ 发送事件,包含标签页信息
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: allCompleted,
+ tabIndex: 2 // ✅ 明确指定这是来自标签页 2 的事件
+ }
+ }));
+
+ } catch (error) {
+ console.error("❌ Error loading detail lot data:", error);
+ setDetailLotData([]);
+
+ // ✅ 如果加载失败,禁用打印按钮
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: false,
+ tabIndex: 2
+ }
+ }));
+ }
+ }, []);
+
+
+ // ✅ 返回列表视图
+ const handleBackToList = useCallback(() => {
+ setShowDetailView(false);
+ setSelectedDoPickOrder(null);
+ setDetailLotData([]);
+
+ // ✅ 返回列表时禁用打印按钮
+ window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted: false,
+ tabIndex: 2
+ }
+ }));
+ }, []);
+
+
+ // ✅ 如果显示详情视图,渲染类似 GoodPickExecution 的表格
+ if (showDetailView && selectedDoPickOrder) {
+ return (
+
+
+ {/* 返回按钮和标题 */}
+
+
+
+ {t("Pick Order Details")}: {selectedDoPickOrder.pickOrderCode}
+
+
+
+ {/* FG Pick Orders 信息 */}
+
+ {selectedDoPickOrder.fgPickOrders.map((fgOrder, index) => (
+ {}} // 只读模式
+ />
+ ))}
+
+
+ {/* 类似 GoodPickExecution 的表格 */}
+
+
+
+
+ {t("Pick Order Code")}
+ {t("Item Code")}
+ {t("Item Name")}
+ {t("Lot No")}
+ {t("Location")}
+ {t("Required Qty")}
+ {t("Actual Pick Qty")}
+ {t("Submitted Status")}
+
+
+
+ {detailLotData.map((lot, index) => (
+
+ {lot.pickOrderCode}
+ {lot.itemCode}
+ {lot.itemName}
+ {lot.lotNo}
+ {lot.location}
+ {lot.requiredQty}
+ {lot.actualPickQty}
+
+
+
+
+ ))}
+
+
+
+
+
+ );
+ }
+
+ // ✅ 默认列表视图
+ return (
+
+
+ {/* 搜索框 */}
+
+
+
+
+ {/* 加载状态 */}
+ {completedDoPickOrdersLoading ? (
+
+
+
+ ) : (
+
+ {/* 结果统计 */}
+
+ {t("Total")}: {filteredDoPickOrders.length} {t("completed DO pick orders")}
+
+
+ {/* 列表 */}
+ {filteredDoPickOrders.length === 0 ? (
+
+
+ {t("No completed DO pick orders found")}
+
+
+ ) : (
+
+ {paginatedData.map((doPickOrder) => (
+
+
+
+
+
+ {doPickOrder.pickOrderCode}
+
+
+ {doPickOrder.shopName} - {doPickOrder.deliveryNo}
+
+
+ {t("Completed")}: {new Date(doPickOrder.completedDate).toLocaleString()}
+
+
+
+
+
+ {doPickOrder.fgPickOrders.length} {t("FG orders")}
+
+
+
+
+
+
+
+
+ ))}
+
+ )}
+
+ {/* 分页 */}
+ {filteredDoPickOrders.length > 0 && (
+
+ )}
+
+ )}
+
+
+ );
+};
+
+export default GoodPickExecutionRecord;
\ No newline at end of file
diff --git a/src/components/Jodetail/GoodPickExecutiondetail.tsx b/src/components/Jodetail/GoodPickExecutiondetail.tsx
new file mode 100644
index 0000000..1af86fc
--- /dev/null
+++ b/src/components/Jodetail/GoodPickExecutiondetail.tsx
@@ -0,0 +1,1724 @@
+"use client";
+
+import {
+ Box,
+ Button,
+ Stack,
+ TextField,
+ Typography,
+ Alert,
+ CircularProgress,
+ Table,
+ TableBody,
+ TableCell,
+ TableContainer,
+ TableHead,
+ TableRow,
+ Paper,
+ Checkbox,
+ TablePagination,
+ Modal,
+} from "@mui/material";
+import { fetchLotDetail } from "@/app/api/inventory/actions";
+import { useCallback, useEffect, useState, useRef, useMemo } from "react";
+import { useTranslation } from "react-i18next";
+import { useRouter } from "next/navigation";
+import {
+ fetchALLPickOrderLineLotDetails,
+ updateStockOutLineStatus,
+ createStockOutLine,
+ updateStockOutLine,
+ recordPickExecutionIssue,
+ fetchFGPickOrders, // ✅ Add this import
+ FGPickOrderResponse,
+ autoAssignAndReleasePickOrder,
+ AutoAssignReleaseResponse,
+ checkPickOrderCompletion,
+ fetchAllPickOrderLotsHierarchical,
+ PickOrderCompletionResponse,
+ checkAndCompletePickOrderByConsoCode,
+ updateSuggestedLotLineId,
+ confirmLotSubstitution
+} from "@/app/api/pickOrder/actions";
+
+import LotConfirmationModal from "./LotConfirmationModal";
+//import { fetchItem } from "@/app/api/settings/item";
+import { updateInventoryLotLineStatus, analyzeQrCode } from "@/app/api/inventory/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 "./GoodPickExecutionForm";
+import FGPickOrderCard from "./FGPickOrderCard";
+interface Props {
+ filterArgs: Record;
+}
+
+// ✅ QR Code Modal Component (from LotTable)
+const QrCodeModal: React.FC<{
+ open: boolean;
+ onClose: () => void;
+ lot: any | null;
+ onQrCodeSubmit: (lotNo: string) => void;
+ combinedLotData: any[]; // ✅ Add this prop
+}> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
+ const { t } = useTranslation("pickOrder");
+ 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('');
+ const [fgPickOrder, setFgPickOrder] = useState(null);
+ // 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 PickExecution: React.FC = ({ filterArgs }) => {
+ const { t } = useTranslation("pickOrder");
+ const router = useRouter();
+ const { data: session } = useSession() as { data: SessionWithTokens | null };
+
+ const currentUserId = session?.id ? parseInt(session.id) : undefined;
+ const [allLotsCompleted, setAllLotsCompleted] = useState(false);
+ const [combinedLotData, setCombinedLotData] = useState([]);
+ const [combinedDataLoading, setCombinedDataLoading] = useState(false);
+ const [originalCombinedData, setOriginalCombinedData] = useState([]);
+
+ 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);
+ const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
+const [expectedLotData, setExpectedLotData] = useState(null);
+const [scannedLotData, setScannedLotData] = useState(null);
+const [isConfirmingLot, setIsConfirmingLot] = useState(false);
+ // ✅ 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 after line 352
+ const [isManualScanning, setIsManualScanning] = useState(false);
+ const [processedQrCodes, setProcessedQrCodes] = useState>(new Set());
+ const [lastProcessedQr, setLastProcessedQr] = useState('');
+ const [isRefreshingData, setIsRefreshingData] = useState(false);
+
+ 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
+ };
+
+ const handleLotMismatch = useCallback((expectedLot: any, scannedLot: any) => {
+ console.log("Lot mismatch detected:", { expectedLot, scannedLot });
+ setExpectedLotData(expectedLot);
+ setScannedLotData(scannedLot);
+ setLotConfirmationOpen(true);
+ }, []);
+ const checkAllLotsCompleted = useCallback((lotData: any[]) => {
+ if (lotData.length === 0) {
+ setAllLotsCompleted(false);
+ return false;
+ }
+
+ // Filter out rejected lots
+ const nonRejectedLots = lotData.filter(lot =>
+ lot.lotAvailability !== 'rejected' &&
+ lot.stockOutLineStatus !== 'rejected'
+ );
+
+ if (nonRejectedLots.length === 0) {
+ setAllLotsCompleted(false);
+ return false;
+ }
+
+ // Check if all non-rejected lots are completed
+ const allCompleted = nonRejectedLots.every(lot =>
+ lot.stockOutLineStatus === 'completed'
+ );
+
+ setAllLotsCompleted(allCompleted);
+ return allCompleted;
+ }, []);
+ const fetchAllCombinedLotData = useCallback(async (userId?: number) => {
+ setCombinedDataLoading(true);
+ try {
+ const userIdToUse = userId || currentUserId;
+
+ console.log(" fetchAllCombinedLotData called with userId:", userIdToUse);
+
+ if (!userIdToUse) {
+ console.warn("⚠️ No userId available, skipping API call");
+ setCombinedLotData([]);
+ setOriginalCombinedData([]);
+ setAllLotsCompleted(false);
+ return;
+ }
+
+ // ✅ Use the hierarchical endpoint that includes rejected lots
+ const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
+ console.log("✅ Hierarchical lot details:", hierarchicalData);
+
+ // ✅ Transform hierarchical data to flat structure for the table
+ const flatLotData: any[] = [];
+
+ if (hierarchicalData.pickOrder && hierarchicalData.pickOrderLines) {
+ hierarchicalData.pickOrderLines.forEach((line: any) => {
+ if (line.lots && line.lots.length > 0) {
+ line.lots.forEach((lot: any) => {
+ flatLotData.push({
+ // Pick order info
+ pickOrderId: hierarchicalData.pickOrder.id,
+ pickOrderCode: hierarchicalData.pickOrder.code,
+ pickOrderConsoCode: hierarchicalData.pickOrder.consoCode,
+ pickOrderTargetDate: hierarchicalData.pickOrder.targetDate,
+ pickOrderType: hierarchicalData.pickOrder.type,
+ pickOrderStatus: hierarchicalData.pickOrder.status,
+ pickOrderAssignTo: hierarchicalData.pickOrder.assignTo,
+
+ // Pick order line info
+ pickOrderLineId: line.id,
+ pickOrderLineRequiredQty: line.requiredQty,
+ pickOrderLineStatus: line.status,
+
+ // Item info
+ itemId: line.item.id,
+ itemCode: line.item.code,
+ itemName: line.item.name,
+ uomCode: line.item.uomCode,
+ uomDesc: line.item.uomDesc,
+
+ // Lot info
+ lotId: lot.id,
+ lotNo: lot.lotNo,
+ expiryDate: lot.expiryDate,
+ location: lot.location,
+ stockUnit: lot.stockUnit,
+ availableQty: lot.availableQty,
+ requiredQty: lot.requiredQty,
+ actualPickQty: lot.actualPickQty,
+ inQty: lot.inQty,
+ outQty: lot.outQty,
+ holdQty: lot.holdQty,
+ lotStatus: lot.lotStatus,
+ lotAvailability: lot.lotAvailability,
+ processingStatus: lot.processingStatus,
+ suggestedPickLotId: lot.suggestedPickLotId,
+ stockOutLineId: lot.stockOutLineId,
+ stockOutLineStatus: lot.stockOutLineStatus,
+ stockOutLineQty: lot.stockOutLineQty,
+
+ // Router info
+ routerId: lot.router?.id,
+ routerIndex: lot.router?.index,
+ routerRoute: lot.router?.route,
+ routerArea: lot.router?.area,
+ uomShortDesc: lot.router?.uomId
+ });
+ });
+ }
+ });
+ }
+
+ console.log("✅ Transformed flat lot data:", flatLotData);
+ setCombinedLotData(flatLotData);
+ setOriginalCombinedData(flatLotData);
+
+ // ✅ Check completion status
+ checkAllLotsCompleted(flatLotData);
+ } catch (error) {
+ console.error("❌ Error fetching combined lot data:", error);
+ setCombinedLotData([]);
+ setOriginalCombinedData([]);
+ setAllLotsCompleted(false);
+ } finally {
+ setCombinedDataLoading(false);
+ }
+ }, [currentUserId, checkAllLotsCompleted]);
+
+ // ✅ Add effect to check completion when lot data changes
+ useEffect(() => {
+ if (combinedLotData.length > 0) {
+ checkAllLotsCompleted(combinedLotData);
+ }
+ }, [combinedLotData, checkAllLotsCompleted]);
+
+ // ✅ Add function to expose completion status to parent
+ const getCompletionStatus = useCallback(() => {
+ return allLotsCompleted;
+ }, [allLotsCompleted]);
+
+ // ✅ Expose completion status to parent component
+ useEffect(() => {
+ // Dispatch custom event with completion status
+ const event = new CustomEvent('pickOrderCompletionStatus', {
+ detail: {
+ allLotsCompleted,
+ tabIndex: 1 // ✅ 明确指定这是来自标签页 1 的事件
+ }
+ });
+ window.dispatchEvent(event);
+ }, [allLotsCompleted]);
+ const handleLotConfirmation = useCallback(async () => {
+ if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
+ setIsConfirmingLot(true);
+ try {
+ let newLotLineId = scannedLotData?.inventoryLotLineId;
+ if (!newLotLineId && scannedLotData?.stockInLineId) {
+ const ld = await fetchLotDetail(scannedLotData.stockInLineId);
+ newLotLineId = ld.inventoryLotLineId;
+ }
+ if (!newLotLineId) {
+ console.error("No inventory lot line id for scanned lot");
+ return;
+ }
+
+ await confirmLotSubstitution({
+ pickOrderLineId: selectedLotForQr.pickOrderLineId,
+ stockOutLineId: selectedLotForQr.stockOutLineId,
+ originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId,
+ newInventoryLotLineId: newLotLineId
+ });
+
+ setQrScanError(false);
+ setQrScanSuccess(false);
+ setQrScanInput('');
+ setIsManualScanning(false);
+ stopScan();
+ resetScan();
+ setProcessedQrCodes(new Set());
+ setLastProcessedQr('');
+
+ setLotConfirmationOpen(false);
+ setExpectedLotData(null);
+ setScannedLotData(null);
+ setSelectedLotForQr(null);
+ await fetchAllCombinedLotData();
+ } catch (error) {
+ console.error("Error confirming lot substitution:", error);
+ } finally {
+ setIsConfirmingLot(false);
+ }
+ }, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData]);
+ 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 fetchAllCombinedLotData();
+
+ 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();
+ // ✅ Clear success state after a delay
+
+ //setTimeout(() => {
+ //setQrScanSuccess(false);
+ //}, 2000);
+ } else {
+ console.error(`❌ QR Code processing failed: ${errorCount} errors`);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+
+ // ✅ Clear error state after a delay
+ // setTimeout(() => {
+ // setQrScanError(false);
+ //}, 3000);
+ }
+ } catch (error) {
+ console.error("❌ Error processing QR code:", error);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+
+ // ✅ Still refresh data even on error
+ setIsRefreshingData(true);
+ await fetchAllCombinedLotData();
+
+ // ✅ Clear error state after a delay
+ setTimeout(() => {
+ setQrScanError(false);
+ }, 3000);
+ } finally {
+ // ✅ Clear refresh flag after a short delay
+ setTimeout(() => {
+ setIsRefreshingData(false);
+ }, 1000);
+ }
+ }, [combinedLotData, fetchAllCombinedLotData]);
+ const processOutsideQrCode = useCallback(async (latestQr: string) => {
+ // 1) Parse JSON safely
+ let qrData: any = null;
+ try {
+ qrData = JSON.parse(latestQr);
+ } catch {
+ console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches.");
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ return;
+ }
+
+ try {
+ // Only use the new API when we have JSON with stockInLineId + itemId
+ if (!(qrData?.stockInLineId && qrData?.itemId)) {
+ console.log("QR JSON missing required fields (itemId, stockInLineId).");
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ return;
+ }
+
+ // Call new analyze-qr-code API
+ const analysis = await analyzeQrCode({
+ itemId: qrData.itemId,
+ stockInLineId: qrData.stockInLineId
+ });
+
+ if (!analysis) {
+ console.error("analyzeQrCode returned no data");
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ return;
+ }
+
+ const {
+ itemId: analyzedItemId,
+ itemCode: analyzedItemCode,
+ itemName: analyzedItemName,
+ scanned,
+ } = analysis || {};
+
+ // 1) Find all lots for the same item from current expected list
+ const sameItemLotsInExpected = combinedLotData.filter(l =>
+ (l.itemId && analyzedItemId && l.itemId === analyzedItemId) ||
+ (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode)
+ );
+
+ if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) {
+ // Case 3: No item code match
+ console.error("No item match in expected lots for scanned code");
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ return;
+ }
+
+ // ✅ FIXED: Find the ACTIVE suggested lot (not rejected lots)
+ const activeSuggestedLots = sameItemLotsInExpected.filter(lot =>
+ lot.lotAvailability !== 'rejected' &&
+ lot.stockOutLineStatus !== 'rejected' &&
+ lot.processingStatus !== 'rejected'
+ );
+
+ if (activeSuggestedLots.length === 0) {
+ console.error("No active suggested lots found for this item");
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ return;
+ }
+
+ // 2) Check if scanned lot is exactly in active suggested lots
+ const exactLotMatch = activeSuggestedLots.find(l =>
+ (scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) ||
+ (scanned?.lotNo && l.lotNo === scanned.lotNo)
+ );
+
+ if (exactLotMatch && scanned?.lotNo) {
+ // Case 1: Normal case - item matches AND lot matches -> proceed
+ console.log(`Exact lot match found for ${scanned.lotNo}, submitting QR`);
+ handleQrCodeSubmit(scanned.lotNo);
+ return;
+ }
+
+ // Case 2: Item matches but lot number differs -> open confirmation modal
+ // ✅ FIXED: Use the first ACTIVE suggested lot, not just any lot
+ const expectedLot = activeSuggestedLots[0];
+ if (!expectedLot) {
+ console.error("Could not determine expected lot for confirmation");
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ return;
+ }
+
+ // ✅ Check if the expected lot is already the scanned lot (after substitution)
+ if (expectedLot.lotNo === scanned?.lotNo) {
+ console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`);
+ handleQrCodeSubmit(scanned.lotNo);
+ return;
+ }
+
+ console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`);
+ setSelectedLotForQr(expectedLot);
+ handleLotMismatch(
+ {
+ lotNo: expectedLot.lotNo,
+ itemCode: analyzedItemCode || expectedLot.itemCode,
+ itemName: analyzedItemName || expectedLot.itemName
+ },
+ {
+ lotNo: scanned?.lotNo || '',
+ itemCode: analyzedItemCode || expectedLot.itemCode,
+ itemName: analyzedItemName || expectedLot.itemName,
+ inventoryLotLineId: scanned?.inventoryLotLineId,
+ stockInLineId: qrData.stockInLineId
+ }
+ );
+ } catch (error) {
+ console.error("Error during analyzeQrCode flow:", error);
+ setQrScanError(true);
+ setQrScanSuccess(false);
+ return;
+ }
+ }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]);
+ // ✅ Update the outside QR scanning effect to use enhanced processing
+// ✅ Update the outside QR scanning effect to use enhanced processing
+useEffect(() => {
+ if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
+ return;
+ }
+
+ const latestQr = qrValues[qrValues.length - 1];
+
+ if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
+ console.log("QR code already processed, skipping...");
+ return;
+ }
+
+ if (latestQr && latestQr !== lastProcessedQr) {
+ console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`);
+ setLastProcessedQr(latestQr);
+ setProcessedQrCodes(prev => new Set(prev).add(latestQr));
+
+ processOutsideQrCode(latestQr);
+ }
+}, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData]);
+ // ✅ Only fetch existing data when session is ready, no auto-assignment
+ useEffect(() => {
+ if (session && currentUserId && !initializationRef.current) {
+ console.log("✅ Session loaded, initializing pick order...");
+ initializationRef.current = true;
+
+ // ✅ Only fetch existing data, no auto-assignment
+ fetchAllCombinedLotData();
+ }
+ }, [session, currentUserId, fetchAllCombinedLotData]);
+
+ // ✅ Add event listener for manual assignment
+ useEffect(() => {
+ const handlePickOrderAssigned = () => {
+ console.log("🔄 Pick order assigned event received, refreshing data...");
+ fetchAllCombinedLotData();
+ };
+
+ window.addEventListener('pickOrderAssigned', handlePickOrderAssigned);
+
+ return () => {
+ window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned);
+ };
+ }, [fetchAllCombinedLotData]);
+
+
+
+ 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
+
+
+ try {
+ const stockOutLineUpdate = await updateStockOutLineStatus({
+ id: selectedLotForQr.stockOutLineId,
+ status: 'checked',
+ qty: selectedLotForQr.stockOutLineQty || 0
+ });
+ console.log("Stock out line updated successfully!");
+ setQrScanSuccess(true);
+ setQrScanError(false);
+
+ // 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 fetchAllCombinedLotData();
+ } catch (error) {
+ console.error("Error creating stock out line:", error);
+ }
+ }
+ }, [selectedLotForQr, fetchAllCombinedLotData]);
+
+
+ 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 {
+ // ✅ FIXED: Calculate cumulative quantity correctly
+ const currentActualPickQty = lot.actualPickQty || 0;
+ const cumulativeQty = currentActualPickQty + newQty;
+
+ // ✅ 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: ${newQty}`);
+ 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 (newQty > 0) {
+ await updateInventoryLotLineQuantities({
+ inventoryLotLineId: lot.lotId,
+ qty: newQty,
+ 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 fetchAllCombinedLotData();
+ console.log("Pick quantity submitted successfully!");
+
+ setTimeout(() => {
+ checkAndAutoAssignNext();
+ }, 1000);
+
+ } catch (error) {
+ console.error("Error submitting pick quantity:", error);
+ }
+ }, [pickQtyData, fetchAllCombinedLotData, 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 fetchAllCombinedLotData();
+ console.log("Lot rejected successfully!");
+
+ setTimeout(() => {
+ checkAndAutoAssignNext();
+ }, 1000);
+
+ } catch (error) {
+ console.error("Error rejecting lot:", error);
+ }
+ }, [fetchAllCombinedLotData, 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);
+ setQrScanError(false);
+ setQrScanSuccess(false);
+ setQrScanInput('');
+ setIsManualScanning(false);
+ stopScan();
+ resetScan();
+ setProcessedQrCodes(new Set());
+ setLastProcessedQr('');
+ await fetchAllCombinedLotData();
+ } catch (error) {
+ console.error("Error submitting pick execution form:", error);
+ }
+ }, [fetchAllCombinedLotData]);
+
+ // ✅ 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
+ // Remove the sorting logic and just do pagination
+const paginatedData = useMemo(() => {
+ const startIndex = paginationController.pageNum * paginationController.pageSize;
+ const endIndex = startIndex + paginationController.pageSize;
+ return combinedLotData.slice(startIndex, endIndex); // ✅ No sorting needed
+}, [combinedLotData, paginationController]);
+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 fetchAllCombinedLotData();
+ console.log("Pick quantity submitted successfully!");
+
+ setTimeout(() => {
+ checkAndAutoAssignNext();
+ }, 1000);
+
+ } catch (error) {
+ console.error("Error submitting pick quantity:", error);
+ }
+}, [fetchAllCombinedLotData, checkAndAutoAssignNext]);
+
+
+ // ✅ Add these functions after line 395
+ 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 (
+
+
+
+
+
+ {/* DO Header */}
+ {fgPickOrdersLoading ? (
+
+
+
+ ) : (
+ fgPickOrders.length > 0 && (
+
+
+
+ {t("Shop Name")}: {fgPickOrders[0].shopName || '-'}
+
+
+ {t("Pick Order Code")}:{fgPickOrders[0].pickOrderCode || '-'}
+
+
+ {t("Store ID")}: {fgPickOrders[0].storeId || '-'}
+
+
+ {t("Ticket No.")}: {fgPickOrders[0].ticketNo || '-'}
+
+
+ {t("Departure Time")}: {fgPickOrders[0].DepartureTime || '-'}
+
+
+
+
+ )
+ )}
+
+
+ {/* Combined Lot Table */}
+
+
+
+ {t("All Pick Order Lots")}
+
+
+
+ {!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#")}
+ {/* {t("Target Date")} */}
+ {/* {t("Lot Location")} */}
+ {t("Lot Required Pick Qty")}
+ {/* {t("Original Available Qty")} */}
+ {t("Scan Result")}
+ {t("Submit Required Pick Qty")}
+ {/* {t("Remaining Available Qty")} */}
+
+ {/* {t("Action")} */}
+
+
+
+ {paginatedData.length === 0 ? (
+
+
+
+ {t("No data available")}
+
+
+
+ ) : (
+ paginatedData.map((lot, index) => (
+
+
+
+ {index + 1}
+
+
+
+
+ {lot.routerRoute || '-'}
+
+
+ {lot.itemCode}
+ {lot.itemName+'('+lot.stockUnit+')'}
+
+
+
+ {lot.lotNo}
+
+
+
+ {/* {lot.pickOrderTargetDate} */}
+ {/* {lot.location} */}
+ {/* {calculateRemainingRequiredQty(lot).toLocaleString()} */}
+
+ {(() => {
+ const inQty = lot.inQty || 0;
+ const requiredQty = lot.requiredQty || 0;
+ const actualPickQty = lot.actualPickQty || 0;
+ const outQty = lot.outQty || 0;
+ const result = requiredQty;
+ return result.toLocaleString()+'('+lot.uomShortDesc+')';
+ })()}
+
+
+
+ {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
+
+
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))
+ )}
+
+
+
+ {/* ✅ Status Messages Display - Move here, outside the table */}
+ {/*
+{paginatedData.length > 0 && (
+
+ {paginatedData.map((lot, index) => (
+
+
+ {t("Lot")} {lot.lotNo}: {getStatusMessage(lot)}
+
+
+ ))}
+
+)}
+*/}
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+
+
+
+ {/* ✅ QR Code Modal */}
+ {
+ setQrModalOpen(false);
+ setSelectedLotForQr(null);
+ stopScan();
+ resetScan();
+ }}
+ lot={selectedLotForQr}
+ combinedLotData={combinedLotData} // ✅ Add this prop
+ onQrCodeSubmit={handleQrCodeSubmitFromModal}
+ />
+ {/* ✅ Lot Confirmation Modal */}
+ {lotConfirmationOpen && expectedLotData && scannedLotData && (
+ {
+ setLotConfirmationOpen(false);
+ setExpectedLotData(null);
+ setScannedLotData(null);
+ }}
+ onConfirm={handleLotConfirmation}
+ expectedLot={expectedLotData}
+ scannedLot={scannedLotData}
+ isLoading={isConfirmingLot}
+ />
+ )}
+ {/* ✅ Good 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 PickExecution;
\ No newline at end of file
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/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..4cca3c4
--- /dev/null
+++ b/src/components/Jodetail/JodetailSearch.tsx
@@ -0,0 +1,440 @@
+"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 "./GoodPickExecution";
+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 PickExecutionDetail from "./GoodPickExecutiondetail";
+import GoodPickExecutionRecord from "./GoodPickExecutionRecord";
+interface Props {
+ pickOrders: PickOrderResult[];
+}
+
+type SearchQuery = Partial<
+ Omit
+>;
+
+type SearchParamNames = keyof SearchQuery;
+
+const JodetailSearch: React.FC = ({ pickOrders }) => {
+ const { t } = useTranslation("pickOrder");
+ 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 [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 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 */}
+
+
+
+
+
+
+ {t("Finished Good Order")}
+
+
+
+
+ {/* Last 2 buttons aligned right */}
+
+
+
+
+
+
+ {/* ✅ Updated print buttons with completion status */}
+
+
+{/*
+
+ */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 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);
+ }}
+ />
+
+ >
+ )}
+
+
+ (
+ {
+ const value = e.target.value === 'true';
+ if (!value && Boolean(errors.acceptQty)) {
+ setValue("acceptQty", itemDetail.acceptedQty);
+ }
+ field.onChange(value);
+ }}
+ >
+ } label="接受" />
+
+
+
+ } label="不接受及上報" />
+
+ )}
+ />
+
+
+ {/*
+
+ {t("Escalation Result")}
+
+
+
+
+ */}
+
+
+ >
+ );
+};
+export default QcFormVer2;
diff --git a/src/components/Jodetail/QcSelect.tsx b/src/components/Jodetail/QcSelect.tsx
new file mode 100644
index 0000000..b42732b
--- /dev/null
+++ b/src/components/Jodetail/QcSelect.tsx
@@ -0,0 +1,78 @@
+import React, { useCallback, useMemo } from "react";
+import {
+ Autocomplete,
+ Box,
+ Checkbox,
+ Chip,
+ ListSubheader,
+ MenuItem,
+ TextField,
+ Tooltip,
+} from "@mui/material";
+import { QcItemWithChecks } from "@/app/api/qc";
+import { useTranslation } from "react-i18next";
+
+interface CommonProps {
+ allQcs: QcItemWithChecks[];
+ error?: boolean;
+}
+
+interface SingleAutocompleteProps extends CommonProps {
+ value: number | string | undefined;
+ onQcSelect: (qcItemId: number) => void | Promise;
+ // multiple: false;
+}
+
+type Props = SingleAutocompleteProps;
+
+const QcSelect: React.FC = ({ allQcs, value, error, onQcSelect }) => {
+ const { t } = useTranslation("home");
+ const filteredQc = useMemo(() => {
+ // do filtering here if any
+ return allQcs;
+ }, [allQcs]);
+ const options = useMemo(() => {
+ return [
+ {
+ value: -1, // think think sin
+ label: t("None"),
+ group: "default",
+ },
+ ...filteredQc.map((q) => ({
+ value: q.id,
+ label: `${q.code} - ${q.name}`,
+ group: "existing",
+ })),
+ ];
+ }, [t, filteredQc]);
+
+ const currentValue = options.find((o) => o.value === value) || options[0];
+
+ const onChange = useCallback(
+ (
+ event: React.SyntheticEvent,
+ newValue: { value: number; group: string } | { value: number }[],
+ ) => {
+ const singleNewVal = newValue as {
+ value: number;
+ group: string;
+ };
+ onQcSelect(singleNewVal.value);
+ },
+ [onQcSelect],
+ );
+
+ return (
+ option.label}
+ options={options}
+ renderInput={(params) => }
+ />
+ );
+};
+export default QcSelect;
diff --git a/src/components/Jodetail/SearchResultsTable.tsx b/src/components/Jodetail/SearchResultsTable.tsx
new file mode 100644
index 0000000..5ceb5f8
--- /dev/null
+++ b/src/components/Jodetail/SearchResultsTable.tsx
@@ -0,0 +1,243 @@
+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 SearchItemWithQty {
+ id: number;
+ label: string;
+ qty: number | null;
+ currentStockBalance?: number;
+ uomDesc?: string;
+ targetDate?: string | null;
+ groupId?: number | null;
+}
+
+interface Group {
+ id: number;
+ name: string;
+ targetDate: string;
+}
+
+interface SearchResultsTableProps {
+ items: SearchItemWithQty[];
+ selectedItemIds: (string | number)[];
+ groups: Group[];
+ onItemSelect: (itemId: number, checked: boolean) => void;
+ onQtyChange: (itemId: number, qty: number | null) => void;
+ onQtyBlur: (itemId: number) => void;
+ onGroupChange: (itemId: number, groupId: string) => void;
+ isItemInCreated: (itemId: number) => boolean;
+ pageNum: number;
+ pageSize: number;
+ onPageChange: (event: unknown, newPage: number) => void;
+ onPageSizeChange: (event: React.ChangeEvent) => void;
+}
+
+const SearchResultsTable: React.FC = ({
+ items,
+ selectedItemIds,
+ groups,
+ onItemSelect,
+ onQtyChange,
+ onGroupChange,
+ onQtyBlur,
+ isItemInCreated,
+ pageNum,
+ pageSize,
+ onPageChange,
+ onPageSizeChange,
+}) => {
+ const { t } = useTranslation("pickOrder");
+
+ // Calculate pagination
+ const startIndex = (pageNum - 1) * pageSize;
+ const endIndex = startIndex + pageSize;
+ const paginatedResults = items.slice(startIndex, endIndex);
+
+ const handleQtyChange = useCallback((itemId: number, value: string) => {
+ // Only allow numbers
+ if (value === "" || /^\d+$/.test(value)) {
+ const numValue = value === "" ? null : Number(value);
+ onQtyChange(itemId, numValue);
+ }
+ }, [onQtyChange]);
+
+ 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) => (
+
+
+ onItemSelect(item.id, e.target.checked)}
+ disabled={isItemInCreated(item.id)}
+ />
+
+
+ {/* Item */}
+
+
+
+ {item.label.split(' - ')[1] || item.label}
+
+
+ {item.label.split(' - ')[0] || ''}
+
+
+
+
+ {/* Group */}
+
+
+
+
+
+
+ {/* Current Stock */}
+
+ 0 ? "success.main" : "error.main"}
+ sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }}
+ >
+ {item.currentStockBalance?.toLocaleString()||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);
+ onQtyChange(item.id, numValue);
+ }
+ }}
+ onBlur={() => {
+ // Trigger auto-add check when user finishes input (clicks elsewhere)
+ onQtyBlur(item.id); // ← Change this to call onQtyBlur instead!
+ }}
+ inputProps={{
+ style: { textAlign: 'center' }
+ }}
+ sx={{
+ width: '80px',
+ '& .MuiInputBase-input': {
+ textAlign: 'center',
+ cursor: 'text'
+ }
+ }}
+ disabled={isItemInCreated(item.id)}
+ />
+
+ {/* Target Date */}
+
+
+ {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"}
+
+
+
+ ))
+ )}
+
+
+
+
+
+ `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}`
+ }
+ />
+ >
+ );
+};
+
+export default SearchResultsTable;
\ No newline at end of file
diff --git a/src/components/Jodetail/StockInFormVer2.tsx b/src/components/Jodetail/StockInFormVer2.tsx
new file mode 100644
index 0000000..32b9169
--- /dev/null
+++ b/src/components/Jodetail/StockInFormVer2.tsx
@@ -0,0 +1,321 @@
+"use client";
+
+import {
+ PurchaseQcResult,
+ PurchaseQCInput,
+ StockInInput,
+} from "@/app/api/po/actions";
+import {
+ Box,
+ Card,
+ CardContent,
+ 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 } 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 { DatePicker, LocalizationProvider } from "@mui/x-date-pickers";
+import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
+import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
+import dayjs from "dayjs";
+// 修改接口以支持 PickOrder 数据
+import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions";
+
+// change PurchaseQcResult to stock in entry props
+interface Props {
+ itemDetail: StockInLine | (GetPickOrderLineInfo & { pickOrderCode: string });
+ // qc: QcItemWithChecks[];
+ disabled: boolean;
+}
+type EntryError =
+ | {
+ [field in keyof StockInInput]?: string;
+ }
+ | undefined;
+
+// type PoQcRow = TableRow, EntryError>;
+
+const StockInFormVer2: React.FC = ({
+ // qc,
+ itemDetail,
+ disabled,
+}) => {
+ const {
+ t,
+ i18n: { language },
+ } = useTranslation("purchaseOrder");
+ const apiRef = useGridApiRef();
+ const {
+ register,
+ formState: { errors, defaultValues, touchedFields },
+ watch,
+ control,
+ setValue,
+ getValues,
+ reset,
+ resetField,
+ setError,
+ clearErrors,
+ } = useFormContext();
+ // console.log(itemDetail);
+
+ useEffect(() => {
+ console.log("triggered");
+ // receiptDate default tdy
+ setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT));
+ setValue("status", "received");
+ }, [setValue]);
+
+ useEffect(() => {
+ console.log(errors);
+ }, [errors]);
+
+ const productionDate = watch("productionDate");
+ const expiryDate = watch("expiryDate");
+ const uom = watch("uom");
+
+ useEffect(() => {
+ console.log(uom);
+ console.log(productionDate);
+ console.log(expiryDate);
+ if (expiryDate) clearErrors();
+ if (productionDate) clearErrors();
+ }, [expiryDate, productionDate, clearErrors]);
+
+ // 检查是否为 PickOrder 数据
+ const isPickOrderData = 'pickOrderCode' in itemDetail;
+
+ // 获取 UOM 显示值
+ const getUomDisplayValue = () => {
+ if (isPickOrderData) {
+ // PickOrder 数据
+ const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
+ return pickOrderItem.uomDesc || pickOrderItem.uomCode || '';
+ } else {
+ // StockIn 数据
+ const stockInItem = itemDetail as StockInLine;
+ return uom?.code || stockInItem.uom?.code || '';
+ }
+ };
+
+ // 获取 Item 显示值
+ const getItemDisplayValue = () => {
+ if (isPickOrderData) {
+ // PickOrder 数据
+ const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
+ return pickOrderItem.itemCode || '';
+ } else {
+ // StockIn 数据
+ const stockInItem = itemDetail as StockInLine;
+ return stockInItem.itemNo || '';
+ }
+ };
+
+ // 获取 Item Name 显示值
+ const getItemNameDisplayValue = () => {
+ if (isPickOrderData) {
+ // PickOrder 数据
+ const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
+ return pickOrderItem.itemName || '';
+ } else {
+ // StockIn 数据
+ const stockInItem = itemDetail as StockInLine;
+ return stockInItem.itemName || '';
+ }
+ };
+
+ // 获取 Quantity 显示值
+ const getQuantityDisplayValue = () => {
+ if (isPickOrderData) {
+ // PickOrder 数据
+ const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string };
+ return pickOrderItem.requiredQty || 0;
+ } else {
+ // StockIn 数据
+ const stockInItem = itemDetail as StockInLine;
+ return stockInItem.acceptedQty || 0;
+ }
+ };
+
+ return (
+
+
+
+ {t("stock in information")}
+
+
+
+
+
+
+
+
+
+ {
+ return (
+
+ {
+ console.log(date);
+ if (!date) return;
+ console.log(date.format(INPUT_DATE_FORMAT));
+ setValue("productionDate", date.format(INPUT_DATE_FORMAT));
+ // field.onChange(date);
+ }}
+ inputRef={field.ref}
+ slotProps={{
+ textField: {
+ // required: true,
+ error: Boolean(errors.productionDate?.message),
+ helperText: errors.productionDate?.message,
+ },
+ }}
+ />
+
+ );
+ }}
+ />
+
+
+ {
+ return (
+
+ {
+ console.log(date);
+ if (!date) return;
+ console.log(date.format(INPUT_DATE_FORMAT));
+ setValue("expiryDate", date.format(INPUT_DATE_FORMAT));
+ // field.onChange(date);
+ }}
+ inputRef={field.ref}
+ slotProps={{
+ textField: {
+ // required: true,
+ error: Boolean(errors.expiryDate?.message),
+ helperText: errors.expiryDate?.message,
+ },
+ }}
+ />
+
+ );
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+ {/*
+
+ */}
+
+ );
+};
+export default StockInFormVer2;
diff --git a/src/components/Jodetail/TwoLineCell.tsx b/src/components/Jodetail/TwoLineCell.tsx
new file mode 100644
index 0000000..f32e56a
--- /dev/null
+++ b/src/components/Jodetail/TwoLineCell.tsx
@@ -0,0 +1,24 @@
+import { Box, Tooltip } from "@mui/material";
+import React from "react";
+
+const TwoLineCell: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+export default TwoLineCell;
diff --git a/src/components/Jodetail/UomSelect.tsx b/src/components/Jodetail/UomSelect.tsx
new file mode 100644
index 0000000..1fec4ab
--- /dev/null
+++ b/src/components/Jodetail/UomSelect.tsx
@@ -0,0 +1,73 @@
+
+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 {
+ allUom: ItemCombo[];
+ error?: boolean;
+}
+
+interface SingleAutocompleteProps extends CommonProps {
+ value: number | string | undefined;
+ onUomSelect: (itemId: number) => void | Promise;
+ // multiple: false;
+}
+
+type Props = SingleAutocompleteProps;
+
+const UomSelect: React.FC = ({
+ allUom,
+ value,
+ error,
+ onUomSelect
+}) => {
+ const { t } = useTranslation("item");
+ const filteredUom = useMemo(() => {
+ return allUom
+ }, [allUom])
+
+ const options = useMemo(() => {
+ return [
+ {
+ value: -1, // think think sin
+ label: t("None"),
+ group: "default",
+ },
+ ...filteredUom.map((i) => ({
+ value: i.id as number,
+ label: i.label,
+ group: "existing",
+ })),
+ ];
+ }, [t, filteredUom]);
+
+ const currentValue = options.find((o) => o.value === value) || options[0];
+
+ const onChange = useCallback(
+ (
+ event: React.SyntheticEvent,
+ newValue: { value: number; group: string } | { value: number }[],
+ ) => {
+ const singleNewVal = newValue as {
+ value: number;
+ group: string;
+ };
+ onUomSelect(singleNewVal.value)
+ }
+ , [onUomSelect])
+ return (
+ option.label}
+ options={options}
+ renderInput={(params) => }
+ />
+ );
+}
+export default UomSelect
\ No newline at end of file
diff --git a/src/components/Jodetail/VerticalSearchBox.tsx b/src/components/Jodetail/VerticalSearchBox.tsx
new file mode 100644
index 0000000..3695e96
--- /dev/null
+++ b/src/components/Jodetail/VerticalSearchBox.tsx
@@ -0,0 +1,85 @@
+import { Criterion } from "@/components/SearchBox/SearchBox";
+import { useTranslation } from "react-i18next";
+import { useState } from "react";
+import { Card, CardContent, Typography, Grid, TextField, Button, Stack } from "@mui/material";
+import { RestartAlt, Search } from "@mui/icons-material";
+import { Autocomplete } from "@mui/material";
+
+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 || "")}
+ value={c.options?.find(option => option.value === inputs[c.paramName]) || null}
+ renderInput={(params) => (
+
+ )}
+ />
+ )}
+
+ );
+ })}
+
+
+ }
+ onClick={handleReset}
+ >
+ {t("Reset")}
+
+ }
+ onClick={handleSearch}
+ >
+ {t("Search")}
+
+
+
+
+ );
+};
+
+export default VerticalSearchBox;
\ No newline at end of file
diff --git a/src/components/Jodetail/dummyQcTemplate.tsx b/src/components/Jodetail/dummyQcTemplate.tsx
new file mode 100644
index 0000000..fa5ff5d
--- /dev/null
+++ b/src/components/Jodetail/dummyQcTemplate.tsx
@@ -0,0 +1,78 @@
+import { PutAwayLine } from "@/app/api/po/actions"
+
+export interface QcData {
+ id: number,
+ qcItem: string,
+ qcDescription: string,
+ isPassed: boolean | undefined
+ failedQty: number | undefined
+ remarks: string | undefined
+}
+
+export const dummyQCData: QcData[] = [
+ {
+ id: 1,
+ qcItem: "包裝",
+ qcDescription: "有破爛、污糟、脹袋、積水、與實物不符等任何一種情況,則不合格",
+ isPassed: undefined,
+ failedQty: undefined,
+ remarks: undefined,
+ },
+ {
+ id: 2,
+ qcItem: "肉質",
+ qcDescription: "肉質鬆散,則不合格",
+ isPassed: undefined,
+ failedQty: undefined,
+ remarks: undefined,
+ },
+ {
+ id: 3,
+ qcItem: "顔色",
+ qcDescription: "不是食材應有的顔色、顔色不均匀、出現其他顔色、腌料/醬顔色不均匀,油脂部分變綠色、黃色,",
+ isPassed: undefined,
+ failedQty: undefined,
+ remarks: undefined,
+ },
+ {
+ id: 4,
+ qcItem: "狀態",
+ qcDescription: "有結晶、結霜、解凍跡象、發霉、散發異味等任何一種情況,則不合格",
+ isPassed: undefined,
+ failedQty: undefined,
+ remarks: undefined,
+ },
+ {
+ id: 5,
+ qcItem: "異物",
+ qcDescription: "有不屬於本食材的雜質,則不合格",
+ isPassed: undefined,
+ failedQty: undefined,
+ remarks: undefined,
+ },
+]
+
+export interface EscalationData {
+ id: number,
+ escalation: string,
+ supervisor: string,
+}
+
+
+export const dummyEscalationHistory: EscalationData[] = [
+ {
+ id: 1,
+ escalation: "上報1",
+ supervisor: "陳大文"
+ },
+]
+
+export const dummyPutawayLine: PutAwayLine[] = [
+ {
+ id: 1,
+ qty: 100,
+ warehouseId: 1,
+ warehouse: "W001 - 憶兆 3樓A倉",
+ printQty: 100
+ }
+]
\ No newline at end of file
diff --git a/src/components/Jodetail/index.ts b/src/components/Jodetail/index.ts
new file mode 100644
index 0000000..513ba22
--- /dev/null
+++ b/src/components/Jodetail/index.ts
@@ -0,0 +1 @@
+export { default } from "./FinishedGoodSearchWrapper";
diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx
index 30b8aff..c23515a 100644
--- a/src/components/NavigationContent/NavigationContent.tsx
+++ b/src/components/NavigationContent/NavigationContent.tsx
@@ -208,6 +208,11 @@ const NavigationContent: React.FC = () => {
label: "Job Order",
path: "/jo",
},
+ {
+ icon: ,
+ label: "Job Order detail",
+ path: "/jodetail",
+ },
],
},
{
diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json
index 72d1631..70c8909 100644
--- a/src/i18n/zh/pickOrder.json
+++ b/src/i18n/zh/pickOrder.json
@@ -172,7 +172,7 @@
"Job Order Code": "工單編號",
"QC Check": "QC 檢查",
"QR Code Scan": "QR Code掃描",
- "Pick Order Details": "提料單詳情",
+ "Pick Order Details": "提料單資料",
"Partial quantity submitted. Please submit more or complete the order.": "已提料部分數量。請提交更多或完成訂單。",
"Pick order completed successfully!": "提料單完成成功!",
"Lot has been rejected and marked as unavailable.": "批號已拒絕並標記為不可用。",
@@ -252,16 +252,16 @@
"Shop Name":"商店名稱",
"Shop Address":"商店地址",
"Delivery Date":"目標日期",
- "Pick Execution 2/F":"進行提料 2/F",
- "Pick Execution 4/F":"進行提料 4/F",
- "Pick Execution Detail":"進行提料詳情",
+ "Pick Execution 2/F":"取單 2/F",
+ "Pick Execution 4/F":"取單 4/F",
+ "Finished Good Detail":"成品資料",
"Submit Required Pick Qty":"提交所需提料數量",
"Scan Result":"掃描結果",
"Ticket No.":"提票號碼",
"Start QR Scan":"開始QR掃描",
"Stop QR Scan":"停止QR掃描",
"Scanning...":"掃描中...",
- "Print DN/Label":"列印送貨單/標籤",
+
"Store ID":"儲存編號",
"QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。",
"Lot Number Mismatch":"批次號碼不符",
@@ -270,15 +270,15 @@
"Scanned Lot:":"掃描批次:",
"Confirm":"確認",
"Update your suggested lot to the this scanned lot":"更新您的建議批次為此掃描的批次",
- "Print Draft":"列印草稿",
- "Print Pick Order and DN Label":"列印提料單和送貨單標貼",
- "Print Pick Order":"列印提料單",
- "Print DN Label":"列印送貨單標貼",
+ "Print Draft":"列印送貨單草稿",
+ "Print Pick Order and DN Label":"列印送貨單和標貼",
+ "Print Pick Order":"列印送貨單",
+ "Print DN Label":"列印標貼",
"If you confirm, the system will:":"如果您確認,系統將:",
"QR code verified.":"QR 碼驗證成功。",
"Order Finished":"訂單完成",
"Submitted Status":"提交狀態",
- "Pick Execution Record":"提料執行記錄",
+ "Finished Good Record":"成單記錄",
"Delivery No.":"送貨單編號",
"Total":"總數",
"completed DO pick orders":"已完成送貨單提料單",
@@ -289,8 +289,6 @@
"FG orders":"成品提料單",
"Back to List":"返回列表",
"No completed DO pick orders found":"沒有已完成送貨單提料單",
-
- "Print DN Label":"列印送貨單標貼",
"Enter the number of cartons: ": "請輸入總箱數",
"Number of cartons": "箱數"