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 ( + + + {t('Pick Execution Issue Form')} {/* ✅ Always show issue form title */} + + + + {/* ✅ Add instruction text */} + + + + + {t('Note:')} {t('This form is for reporting issues only. You must report either missing items or bad items.')} + + + + + {/* ✅ Keep the existing form fields */} + + + + + + + + + + handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} + error={!!errors.actualPickQty} + helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} + variant="outlined" + /> + + + + handleInputChange('missQty', parseFloat(e.target.value) || 0)} + error={!!errors.missQty} + // helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} + variant="outlined" + /> + + + + handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} + error={!!errors.badItemQty} + // helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} + variant="outlined" + /> + + + {/* ✅ Show issue description and handler fields when bad items > 0 */} + {(formData.badItemQty && formData.badItemQty > 0) ? ( + <> + + handleInputChange('issueRemark', e.target.value)} + error={!!errors.issueRemark} + helperText={errors.issueRemark} + //placeholder={t('Describe the issue with bad items')} + variant="outlined" + /> + + + + + {t('handler')} + + {errors.handledBy && ( + + {errors.handledBy} + + )} + + + + ) : (<>)} + + + + + + + + + ); +}; + +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 ? ( + + ) : ( + + )} + + {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) => ( + + )} + /> + )} + + ); + })} + + + + + + + + ); + }; + + // 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}) + + + + + )} + + {/* 操作按钮 */} + + + + + + + ); +}; + +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 ( + + + + {t("Lot Number Mismatch")} + + + + + + + {t("The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?")} + + + + + {t("Expected Lot:")} + + + + {t("Item Code")}: {expectedLot.itemCode} + + + {t("Item Name")}: {expectedLot.itemName} + + + {t("Lot No")}: {expectedLot.lotNo} + + + + + + + + + {t("Scanned Lot:")} + + + + {t("Item Code")}: {scannedLot.itemCode} + + + {t("Item Name")}: {scannedLot.itemName} + + + {t("Lot No")}: {scannedLot.lotNo} + + + + + + {t("If you confirm, the system will:")} +
    +
  • {t("Update your suggested lot to the this scanned lot")}
  • +
+
+
+
+ + + + + +
+ ); +}; + +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 = ( + + + + + ); + // 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) => ( + + )} + /> + )} + + ); + })} + + + + + + + + ); +}; + +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": "箱數"