From 0e6a2acfd1077db52367357f78db0baac5b2336d Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sun, 28 Sep 2025 12:13:38 +0800 Subject: [PATCH 1/8] update --- src/app/(main)/jodetail/edit/not-found.tsx | 19 + src/app/(main)/jodetail/edit/page.tsx | 49 + src/app/(main)/jodetail/page.tsx | 39 + .../FinishedGoodSearch/FinishedGoodSearch.tsx | 4 +- .../GoodPickExecutionRecord.tsx | 28 +- src/components/Jodetail/CombinedLotTable.tsx | 231 +++ src/components/Jodetail/CreateForm.tsx | 321 +++ src/components/Jodetail/CreatedItemsTable.tsx | 209 ++ .../Jodetail/EscalationComponent.tsx | 179 ++ src/components/Jodetail/FGPickOrderCard.tsx | 120 ++ .../Jodetail/FinishedGoodSearchWrapper.tsx | 26 + src/components/Jodetail/GoodPickExecution.tsx | 1250 +++++++++++ .../Jodetail/GoodPickExecutionForm.tsx | 368 ++++ .../Jodetail/GoodPickExecutionRecord.tsx | 440 ++++ .../Jodetail/GoodPickExecutiondetail.tsx | 1724 ++++++++++++++++ src/components/Jodetail/ItemSelect.tsx | 79 + src/components/Jodetail/Jobcreatitem.tsx | 1824 +++++++++++++++++ src/components/Jodetail/Jodetail.tsx | 167 ++ src/components/Jodetail/JodetailSearch.tsx | 440 ++++ .../Jodetail/LotConfirmationModal.tsx | 124 ++ src/components/Jodetail/PutawayForm.tsx | 527 +++++ src/components/Jodetail/QCDatagrid.tsx | 395 ++++ src/components/Jodetail/QcFormVer2.tsx | 460 +++++ src/components/Jodetail/QcSelect.tsx | 78 + .../Jodetail/SearchResultsTable.tsx | 243 +++ src/components/Jodetail/StockInFormVer2.tsx | 321 +++ src/components/Jodetail/TwoLineCell.tsx | 24 + src/components/Jodetail/UomSelect.tsx | 73 + src/components/Jodetail/VerticalSearchBox.tsx | 85 + src/components/Jodetail/dummyQcTemplate.tsx | 78 + src/components/Jodetail/index.ts | 1 + .../NavigationContent/NavigationContent.tsx | 5 + src/i18n/zh/pickOrder.json | 22 +- 33 files changed, 9925 insertions(+), 28 deletions(-) create mode 100644 src/app/(main)/jodetail/edit/not-found.tsx create mode 100644 src/app/(main)/jodetail/edit/page.tsx create mode 100644 src/app/(main)/jodetail/page.tsx create mode 100644 src/components/Jodetail/CombinedLotTable.tsx create mode 100644 src/components/Jodetail/CreateForm.tsx create mode 100644 src/components/Jodetail/CreatedItemsTable.tsx create mode 100644 src/components/Jodetail/EscalationComponent.tsx create mode 100644 src/components/Jodetail/FGPickOrderCard.tsx create mode 100644 src/components/Jodetail/FinishedGoodSearchWrapper.tsx create mode 100644 src/components/Jodetail/GoodPickExecution.tsx create mode 100644 src/components/Jodetail/GoodPickExecutionForm.tsx create mode 100644 src/components/Jodetail/GoodPickExecutionRecord.tsx create mode 100644 src/components/Jodetail/GoodPickExecutiondetail.tsx create mode 100644 src/components/Jodetail/ItemSelect.tsx create mode 100644 src/components/Jodetail/Jobcreatitem.tsx create mode 100644 src/components/Jodetail/Jodetail.tsx create mode 100644 src/components/Jodetail/JodetailSearch.tsx create mode 100644 src/components/Jodetail/LotConfirmationModal.tsx create mode 100644 src/components/Jodetail/PutawayForm.tsx create mode 100644 src/components/Jodetail/QCDatagrid.tsx create mode 100644 src/components/Jodetail/QcFormVer2.tsx create mode 100644 src/components/Jodetail/QcSelect.tsx create mode 100644 src/components/Jodetail/SearchResultsTable.tsx create mode 100644 src/components/Jodetail/StockInFormVer2.tsx create mode 100644 src/components/Jodetail/TwoLineCell.tsx create mode 100644 src/components/Jodetail/UomSelect.tsx create mode 100644 src/components/Jodetail/VerticalSearchBox.tsx create mode 100644 src/components/Jodetail/dummyQcTemplate.tsx create mode 100644 src/components/Jodetail/index.ts 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": "箱數" From 83eaa0c399cb0c26c8092445ed1837589ecb92c4 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Sun, 28 Sep 2025 21:43:18 +0800 Subject: [PATCH 2/8] update --- src/app/api/jo/actions.ts | 122 ++ ...nRecord.tsx => FInishedJobOrderRecord.tsx} | 6 +- ...PickExecution.tsx => JobPickExecution.tsx} | 776 ++++++++---- ...utionForm.tsx => JobPickExecutionForm.tsx} | 67 +- ...ail.tsx => JobPickExecutionsecondscan.tsx} | 1095 +++++------------ src/components/Jodetail/JodetailSearch.tsx | 159 +-- 6 files changed, 1102 insertions(+), 1123 deletions(-) rename src/components/Jodetail/{GoodPickExecutionRecord.tsx => FInishedJobOrderRecord.tsx} (98%) rename src/components/Jodetail/{GoodPickExecution.tsx => JobPickExecution.tsx} (63%) rename src/components/Jodetail/{GoodPickExecutionForm.tsx => JobPickExecutionForm.tsx} (86%) rename src/components/Jodetail/{GoodPickExecutiondetail.tsx => JobPickExecutionsecondscan.tsx} (52%) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 135f2d9..0955a79 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -76,7 +76,129 @@ export interface JobOrderDetail { pickLines: any[]; status: string; } +export interface UnassignedJobOrderPickOrder { + pickOrderId: number; + pickOrderCode: string; + pickOrderConsoCode: string; + pickOrderTargetDate: string; + pickOrderStatus: string; + jobOrderId: number; + jobOrderCode: string; + jobOrderName: string; + reqQty: number; + uom: string; + planStart: string; + planEnd: string; +} + +export interface AssignJobOrderResponse { + id: number | null; + code: string | null; + name: string | null; + type: string | null; + message: string | null; + errorPosition: string | null; +} +export const recordSecondScanIssue = cache(async ( + pickOrderId: number, + itemId: number, + data: { + qty: number; + isMissing: boolean; + isBad: boolean; + reason: string; + createdBy: number; + } +) => { + return serverFetchJson( + `${BASE_API_URL}/jo/second-scan-issue/${pickOrderId}/${itemId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + next: { tags: ["jo-second-scan"] }, + }, + ); +}); +export const updateSecondQrScanStatus = cache(async (pickOrderId: number, itemId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/second-scan-qr/${pickOrderId}/${itemId}`, + { + method: "POST", + next: { tags: ["jo-second-scan"] }, + }, + ); +}); + +export const submitSecondScanQuantity = cache(async ( + pickOrderId: number, + itemId: number, + data: { qty: number; isMissing?: boolean; isBad?: boolean; reason?: string } +) => { + return serverFetchJson( + `${BASE_API_URL}/jo/second-scan-submit/${pickOrderId}/${itemId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + next: { tags: ["jo-second-scan"] }, + }, + ); +}); +// 获取未分配的 Job Order pick orders +export const fetchUnassignedJobOrderPickOrders = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/jo/unassigned-job-order-pick-orders`, + { + method: "GET", + next: { tags: ["jo-unassigned"] }, + }, + ); +}); +// 分配 Job Order pick order 给用户 +export const assignJobOrderPickOrder = async (pickOrderId: number, userId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/assign-job-order-pick-order/${pickOrderId}/${userId}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + } + ); +}; + +// 获取 Job Order 分层数据 +export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`, + { + method: "GET", + next: { tags: ["jo-hierarchical"] }, + }, + ); +}); + +// 获取已完成的 Job Order pick orders +export const fetchCompletedJobOrderPickOrders = cache(async (userId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/completed-job-order-pick-orders/${userId}`, + { + method: "GET", + next: { tags: ["jo-completed"] }, + }, + ); +}); + +// 获取已完成的 Job Order pick order records +export const fetchCompletedJobOrderPickOrderRecords = cache(async (userId: number) => { + return serverFetchJson( + `${BASE_API_URL}/jo/completed-job-order-pick-order-records/${userId}`, + { + method: "GET", + next: { tags: ["jo-records"] }, + }, + ); +}); export const fetchJobOrderDetailByCode = cache(async (code: string) => { return serverFetchJson( `${BASE_API_URL}/jo/detailByCode/${code}`, diff --git a/src/components/Jodetail/GoodPickExecutionRecord.tsx b/src/components/Jodetail/FInishedJobOrderRecord.tsx similarity index 98% rename from src/components/Jodetail/GoodPickExecutionRecord.tsx rename to src/components/Jodetail/FInishedJobOrderRecord.tsx index 2bde71e..4756e95 100644 --- a/src/components/Jodetail/GoodPickExecutionRecord.tsx +++ b/src/components/Jodetail/FInishedJobOrderRecord.tsx @@ -59,7 +59,7 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; -import GoodPickExecutionForm from "./GoodPickExecutionForm"; +import GoodPickExecutionForm from "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; interface Props { @@ -99,7 +99,7 @@ interface PickOrderData { lots: any[]; } -const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => { +const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -437,4 +437,4 @@ const GoodPickExecutionRecord: React.FC = ({ filterArgs }) => { ); }; -export default GoodPickExecutionRecord; \ No newline at end of file +export default FInishedJobOrderRecord; \ No newline at end of file diff --git a/src/components/Jodetail/GoodPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx similarity index 63% rename from src/components/Jodetail/GoodPickExecution.tsx rename to src/components/Jodetail/JobPickExecution.tsx index 1bd52b8..87c6bf2 100644 --- a/src/components/Jodetail/GoodPickExecution.tsx +++ b/src/components/Jodetail/JobPickExecution.tsx @@ -15,6 +15,7 @@ import { TableHead, TableRow, Paper, + Checkbox, TablePagination, Modal, } from "@mui/material"; @@ -22,11 +23,10 @@ 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 + fetchFGPickOrders, FGPickOrderResponse, autoAssignAndReleasePickOrder, AutoAssignReleaseResponse, @@ -34,6 +34,12 @@ import { PickOrderCompletionResponse, checkAndCompletePickOrderByConsoCode } from "@/app/api/pickOrder/actions"; +// ✅ 修改:使用 Job Order API +import { + fetchJobOrderLotsHierarchical, + fetchUnassignedJobOrderPickOrders, + assignJobOrderPickOrder +} from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { FormProvider, @@ -47,19 +53,20 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; -import GoodPickExecutionForm from "./GoodPickExecutionForm"; +import GoodPickExecutionForm from "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; + interface Props { filterArgs: Record; } -// ✅ QR Code Modal Component (from LotTable) +// ✅ QR Code Modal Component (from GoodPickExecution) const QrCodeModal: React.FC<{ open: boolean; onClose: () => void; lot: any | null; onQrCodeSubmit: (lotNo: string) => void; - combinedLotData: any[]; // ✅ Add this prop + combinedLotData: any[]; }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { const { t } = useTranslation("pickOrder"); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); @@ -73,7 +80,7 @@ const QrCodeModal: React.FC<{ 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) { @@ -131,7 +138,7 @@ const QrCodeModal: React.FC<{ onClose(); resetScan(); } else { - setQrScanFailed(true); + setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } @@ -147,7 +154,7 @@ const QrCodeModal: React.FC<{ onClose(); resetScan(); } else { - setQrScanFailed(true); + setQrScanFailed(true); setManualInputError(true); setManualInputSubmitted(true); } @@ -162,23 +169,23 @@ const QrCodeModal: React.FC<{ setManualInputSubmitted(false); setManualInputError(false); setIsProcessingQr(false); - setQrScanFailed(false); - setQrScanSuccess(false); + setQrScanFailed(false); + setQrScanSuccess(false); setScannedQrResult(''); setProcessedQrCodes(new Set()); - } + } }, [open]); useEffect(() => { if (lot) { - setManualInput(''); - setManualInputSubmitted(false); - setManualInputError(false); + setManualInput(''); + setManualInputSubmitted(false); + setManualInputError(false); setIsProcessingQr(false); - setQrScanFailed(false); - setQrScanSuccess(false); - setScannedQrResult(''); - setProcessedQrCodes(new Set()); + setQrScanFailed(false); + setQrScanSuccess(false); + setScannedQrResult(''); + setProcessedQrCodes(new Set()); } }, [lot]); @@ -190,7 +197,7 @@ const QrCodeModal: React.FC<{ const timer = setTimeout(() => { setQrScanSuccess(true); onQrCodeSubmit(lot.lotNo); - onClose(); + onClose(); setManualInput(''); setManualInputError(false); setManualInputSubmitted(false); @@ -238,7 +245,7 @@ const QrCodeModal: React.FC<{ {isProcessingQr && ( - {t("Processing QR code...")} + {t("Processing QR code...")} )} @@ -267,8 +274,8 @@ const QrCodeModal: React.FC<{ : '' } /> - + ) : ( + + )} + + {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 Name")} + {t("Item Code")} + {t("Item Name")} {t("Lot#")} - {t("Lot Required Pick Qty")} - - {t("Lot Actual Pick Qty")} - - {t("Action")} - - - + {t("Scan Result")} + {t("Submit Required Pick Qty")} + + + {paginatedData.length === 0 ? ( - + {t("No data available")} @@ -1037,7 +1330,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { > - {lot.routerIndex || index + 1} + {index + 1} @@ -1045,7 +1338,8 @@ const PickExecution: React.FC = ({ filterArgs }) => { {lot.routerRoute || '-'} - {lot.itemName} + {lot.itemCode} + {lot.itemName+'('+lot.uomDesc+')'} = ({ filterArgs }) => { - {(() => { - const inQty = lot.inQty || 0; - const outQty = lot.outQty || 0; - const result = inQty - outQty; - return result.toLocaleString(); + const requiredQty = lot.requiredQty || 0; + return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} + + {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? ( + + + + ) : null} + - {!lot.stockOutLineId ? ( - - ) : ( - // ✅ When stockOutLineId exists, show TextField + Issue button + + - { + - - - )} - - - - - - - + + )) )} - - -
+ +
-*/} - {/* - `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` } - /> + /> - + {/* ✅ QR Code Modal */} { @@ -1210,19 +1477,19 @@ const PickExecution: React.FC = ({ filterArgs }) => { resetScan(); }} lot={selectedLotForQr} - combinedLotData={combinedLotData} // ✅ Add this prop + combinedLotData={combinedLotData} onQrCodeSubmit={handleQrCodeSubmitFromModal} /> - + {/* ✅ Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( - { setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); }} - onSubmit={handlePickExecutionFormSubmit} + onSubmit={handlePickExecutionFormSubmit} selectedLot={selectedLotForExecutionForm} selectedPickOrderLine={{ id: selectedLotForExecutionForm.pickOrderLineId, @@ -1242,9 +1509,8 @@ const PickExecution: React.FC = ({ filterArgs }) => { pickOrderCreateDate={new Date()} /> )} - */} ); }; -export default PickExecution; \ No newline at end of file +export default JobPickExecution \ No newline at end of file diff --git a/src/components/Jodetail/GoodPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx similarity index 86% rename from src/components/Jodetail/GoodPickExecutionForm.tsx rename to src/components/Jodetail/JobPickExecutionForm.tsx index b7fe86d..7af42b7 100644 --- a/src/components/Jodetail/GoodPickExecutionForm.tsx +++ b/src/components/Jodetail/JobPickExecutionForm.tsx @@ -81,7 +81,7 @@ const PickExecutionForm: React.FC = ({ const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(false); const [handlers, setHandlers] = useState>([]); - + const [verifiedQty, setVerifiedQty] = useState(0); // 计算剩余可用数量 const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { const remainingQty = lot.inQty - lot.outQty; @@ -123,17 +123,15 @@ const PickExecutionForm: React.FC = ({ } }; - // 计算剩余可用数量 - const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); - const requiredQty = calculateRequiredQty(selectedLot); + // ✅ Initialize verified quantity to the received quantity (actualPickQty) + const initialVerifiedQty = selectedLot.actualPickQty || 0; + setVerifiedQty(initialVerifiedQty); + console.log("=== PickExecutionForm Debug ==="); console.log("selectedLot:", selectedLot); - console.log("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("initialVerifiedQty:", initialVerifiedQty); console.log("=== End Debug ==="); + setFormData({ pickOrderId: pickOrderId, pickOrderCode: selectedPickOrderLine.pickOrderCode, @@ -147,18 +145,24 @@ const PickExecutionForm: React.FC = ({ lotNo: selectedLot.lotNo, storeLocation: selectedLot.location, requiredQty: selectedLot.requiredQty, - actualPickQty: selectedLot.actualPickQty || 0, + actualPickQty: initialVerifiedQty, // ✅ Use the initial value missQty: 0, - badItemQty: 0, // 初始化为 0,用户需要手动输入 + badItemQty: 0, issueRemark: '', pickerName: '', handledBy: undefined, }); } - }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]); + }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate]); const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); + + // ✅ Update verified quantity state when actualPickQty changes + if (field === 'actualPickQty') { + setVerifiedQty(value); + } + // 清除错误 if (errors[field as keyof FormErrors]) { setErrors(prev => ({ ...prev, [field]: undefined })); @@ -169,21 +173,21 @@ const PickExecutionForm: React.FC = ({ const validateForm = (): boolean => { const newErrors: FormErrors = {}; - if (formData.actualPickQty === undefined || formData.actualPickQty < 0) { + if (verifiedQty === undefined || verifiedQty < 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'); + // ✅ Check if verified qty exceeds received qty + if (verifiedQty > (selectedLot?.actualPickQty || 0)) { + newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity'); } - // ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty) - if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) { + // ✅ Check if verified qty exceeds required qty + if (verifiedQty > (selectedLot?.requiredQty || 0)) { newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty'); } - // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) + // ✅ Require either missQty > 0 OR badItemQty > 0 const hasMissQty = formData.missQty && formData.missQty > 0; const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; @@ -203,7 +207,13 @@ const PickExecutionForm: React.FC = ({ setLoading(true); try { - await onSubmit(formData as PickExecutionIssueData); + // ✅ Use the verified quantity in the submission + const submissionData = { + ...formData, + actualPickQty: verifiedQty + } as PickExecutionIssueData; + + await onSubmit(submissionData); onClose(); } catch (error) { console.error('Error submitting pick execution issue:', error); @@ -215,6 +225,7 @@ const PickExecutionForm: React.FC = ({ const handleClose = () => { setFormData({}); setErrors({}); + setVerifiedQty(0); onClose(); }; @@ -257,8 +268,8 @@ const PickExecutionForm: React.FC = ({ = ({ handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} + value={verifiedQty} // ✅ Use the separate state + onChange={(e) => { + const newValue = parseFloat(e.target.value) || 0; + setVerifiedQty(newValue); + handleInputChange('actualPickQty', newValue); + }} error={!!errors.actualPickQty} - helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} + helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(selectedLot?.actualPickQty || 0, selectedLot?.requiredQty || 0)}`} variant="outlined" /> diff --git a/src/components/Jodetail/GoodPickExecutiondetail.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx similarity index 52% rename from src/components/Jodetail/GoodPickExecutiondetail.tsx rename to src/components/Jodetail/JobPickExecutionsecondscan.tsx index 1af86fc..fbc2122 100644 --- a/src/components/Jodetail/GoodPickExecutiondetail.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -19,31 +19,20 @@ import { 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"; +// ✅ 修改:使用 Job Order API +import { + fetchCompletedJobOrderPickOrders, + fetchUnassignedJobOrderPickOrders, + assignJobOrderPickOrder, + updateSecondQrScanStatus, + submitSecondScanQuantity, + recordSecondScanIssue + +} from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { FormProvider, @@ -57,19 +46,20 @@ import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerP import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; -import GoodPickExecutionForm from "./GoodPickExecutionForm"; +import GoodPickExecutionForm from "./JobPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; + interface Props { filterArgs: Record; } -// ✅ QR Code Modal Component (from LotTable) +// ✅ QR Code Modal Component (from GoodPickExecution) const QrCodeModal: React.FC<{ open: boolean; onClose: () => void; lot: any | null; onQrCodeSubmit: (lotNo: string) => void; - combinedLotData: any[]; // ✅ Add this prop + combinedLotData: any[]; }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { const { t } = useTranslation("pickOrder"); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); @@ -83,7 +73,7 @@ const QrCodeModal: React.FC<{ 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) { @@ -317,17 +307,23 @@ const QrCodeModal: React.FC<{ ); }; -const PickExecution: React.FC = ({ filterArgs }) => { +const JobPickExecution: 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); + + // ✅ 修改:使用 Job Order 数据结构 + const [jobOrderData, setJobOrderData] = useState(null); const [combinedLotData, setCombinedLotData] = useState([]); const [combinedDataLoading, setCombinedDataLoading] = useState(false); const [originalCombinedData, setOriginalCombinedData] = useState([]); + // ✅ 添加未分配订单状态 + const [unassignedOrders, setUnassignedOrders] = useState([]); + const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [qrScanInput, setQrScanInput] = useState(''); @@ -353,131 +349,99 @@ const PickExecution: React.FC = ({ filterArgs }) => { // ✅ 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 + // ✅ Add these missing state variables const [isManualScanning, setIsManualScanning] = useState(false); const [processedQrCodes, setProcessedQrCodes] = useState>(new Set()); const [lastProcessedQr, setLastProcessedQr] = useState(''); const [isRefreshingData, setIsRefreshingData] = useState(false); - const fetchFgPickOrdersData = useCallback(async () => { - if (!currentUserId) return; - - setFgPickOrdersLoading(true); + // ✅ 修改:加载未分配的 Job Order 订单 + const loadUnassignedOrders = useCallback(async () => { + setIsLoadingUnassigned(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); + const orders = await fetchUnassignedJobOrderPickOrders(); + setUnassignedOrders(orders); } catch (error) { - console.error("❌ Error fetching FG pick orders:", error); - setFgPickOrders([]); + console.error("Error loading unassigned orders:", error); } finally { - setFgPickOrdersLoading(false); + setIsLoadingUnassigned(false); } - }, [currentUserId, combinedLotData]); - useEffect(() => { - if (combinedLotData.length > 0) { - fetchFgPickOrdersData(); + }, []); + + // ✅ 修改:分配订单给当前用户 + const handleAssignOrder = useCallback(async (pickOrderId: number) => { + if (!currentUserId) { + console.error("Missing user id in session"); + return; } - }, [combinedLotData, fetchFgPickOrdersData]); - + + try { + const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); + if (result.message === "Successfully assigned") { + console.log("✅ Successfully assigned pick order"); + // 刷新数据 + window.dispatchEvent(new CustomEvent('pickOrderAssigned')); + // 重新加载未分配订单列表 + loadUnassignedOrders(); + } else { + console.warn("⚠️ Assignment failed:", result.message); + alert(`Assignment failed: ${result.message}`); + } + } catch (error) { + console.error("❌ Error assigning order:", error); + alert("Error occurred during assignment"); + } + }, [currentUserId, loadUnassignedOrders]); + + // ✅ Handle QR code button click const handleQrCodeClick = (pickOrderId: number) => { console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); // TODO: Implement QR code functionality }; - 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) => { + // ✅ 修改:使用 Job Order API 获取数据 + const fetchJobOrderData = useCallback(async (userId?: number) => { setCombinedDataLoading(true); try { const userIdToUse = userId || currentUserId; - console.log(" fetchAllCombinedLotData called with userId:", userIdToUse); + console.log(" fetchJobOrderData called with userId:", userIdToUse); if (!userIdToUse) { console.warn("⚠️ No userId available, skipping API call"); + setJobOrderData(null); setCombinedLotData([]); setOriginalCombinedData([]); - setAllLotsCompleted(false); return; } - // ✅ Use the hierarchical endpoint that includes rejected lots - const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse); - console.log("✅ Hierarchical lot details:", hierarchicalData); + // ✅ 使用 Job Order API + const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse); + console.log("✅ Job Order data:", jobOrderData); + + setJobOrderData(jobOrderData); // ✅ Transform hierarchical data to flat structure for the table const flatLotData: any[] = []; - if (hierarchicalData.pickOrder && hierarchicalData.pickOrderLines) { - hierarchicalData.pickOrderLines.forEach((line: any) => { + if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) { + jobOrderData.pickOrderLines.forEach((line: any) => { if (line.lots && line.lots.length > 0) { line.lots.forEach((lot: any) => { flatLotData.push({ // Pick order info - pickOrderId: 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, + pickOrderId: jobOrderData.pickOrder.id, + pickOrderCode: jobOrderData.pickOrder.code, + pickOrderConsoCode: jobOrderData.pickOrder.consoCode, + pickOrderTargetDate: jobOrderData.pickOrder.targetDate, + pickOrderType: jobOrderData.pickOrder.type, + pickOrderStatus: jobOrderData.pickOrder.status, + pickOrderAssignTo: jobOrderData.pickOrder.assignTo, // Pick order line info pickOrderLineId: line.id, @@ -485,38 +449,33 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); 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, + itemId: line.itemId, + itemCode: line.itemCode, + itemName: line.itemName, + uomCode: line.uomCode, + uomDesc: line.uomDesc, // Lot info - lotId: lot.id, + lotId: lot.lotId, 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 + routerIndex: lot.routerIndex, + secondQrScanStatus: lot.secondQrScanStatus, + routerArea: lot.routerArea, + routerRoute: lot.routerRoute, + uomShortDesc: lot.uomShortDesc }); }); } @@ -527,89 +486,76 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); setCombinedLotData(flatLotData); setOriginalCombinedData(flatLotData); - // ✅ Check completion status - checkAllLotsCompleted(flatLotData); + // ✅ 计算完成状态并发送事件 + const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) => + lot.processingStatus === 'completed' + ); + + // ✅ 发送完成状态事件,包含标签页信息 + window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { + detail: { + allLotsCompleted: allCompleted, + tabIndex: 0 // ✅ 明确指定这是来自标签页 0 的事件 + } + })); + } catch (error) { - console.error("❌ Error fetching combined lot data:", error); + console.error("❌ Error fetching job order data:", error); + setJobOrderData(null); setCombinedLotData([]); setOriginalCombinedData([]); - setAllLotsCompleted(false); + + // ✅ 如果加载失败,禁用打印按钮 + window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { + detail: { + allLotsCompleted: false, + tabIndex: 0 + } + })); } finally { setCombinedDataLoading(false); } - }, [currentUserId, checkAllLotsCompleted]); + }, [currentUserId]); - // ✅ Add effect to check completion when lot data changes + // ✅ 修改:初始化时加载数据 useEffect(() => { - if (combinedLotData.length > 0) { - checkAllLotsCompleted(combinedLotData); + if (session && currentUserId && !initializationRef.current) { + console.log("✅ Session loaded, initializing job order..."); + initializationRef.current = true; + + // 加载 Job Order 数据 + fetchJobOrderData(); + // 加载未分配订单 + loadUnassignedOrders(); } - }, [combinedLotData, checkAllLotsCompleted]); + }, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]); - // ✅ Add function to expose completion status to parent - const getCompletionStatus = useCallback(() => { - return allLotsCompleted; - }, [allLotsCompleted]); - - // ✅ Expose completion status to parent component + // ✅ Add event listener for manual assignment 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(''); + const handlePickOrderAssigned = () => { + console.log("🔄 Pick order assigned event received, refreshing data..."); + fetchJobOrderData(); + }; - 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]); + window.addEventListener('pickOrderAssigned', handlePickOrderAssigned); + + return () => { + window.removeEventListener('pickOrderAssigned', handlePickOrderAssigned); + }; + }, [fetchJobOrderData]); + + // ✅ Handle QR code submission for matched lot (external scanning) const handleQrCodeSubmit = useCallback(async (lotNo: string) => { - console.log(`✅ Processing QR Code for lot: ${lotNo}`); + console.log(`✅ Processing Second 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)); + // ✅ Check if this lot was already processed recently + const lotKey = `${lotNo}_${Date.now()}`; + if (processedQrCodes.has(lotNo)) { + console.log(`⏭️ Lot ${lotNo} already processed, skipping...`); + return; + } + const currentLotData = combinedLotData; const matchingLots = currentLotData.filter(lot => lot.lotNo === lotNo || lot.lotNo?.toLowerCase() === lotNo.toLowerCase() @@ -619,305 +565,97 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); 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}`); + // ✅ Check if this specific item was already processed + const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`; + if (processedQrCodes.has(itemKey)) { + console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`); + continue; + } - 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++; - } + // ✅ Use the new second scan API + const result = await updateSecondQrScanStatus( + matchingLot.pickOrderId, + matchingLot.itemId + ); + + if (result.code === "SUCCESS") { + successCount++; + // ✅ Mark this item as processed + setProcessedQrCodes(prev => new Set(prev).add(itemKey)); + console.log(`✅ Second QR scan status updated for item ${matchingLot.itemId}`); } else { - 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++; - } + console.error(`❌ Failed to update second QR scan status: ${result.message}`); } } - // ✅ 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); + await fetchJobOrderData(); // Refresh data } 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); + console.error("❌ Error processing second 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; - } + }, [combinedLotData, fetchJobOrderData, processedQrCodes]); - // 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); + useEffect(() => { + if (qrValues.length > 0 && combinedLotData.length > 0) { + const latestQr = qrValues[qrValues.length - 1]; + + // ✅ Check if this QR was already processed recently + if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { + console.log("⏭️ QR code already processed, skipping..."); return; } - // ✅ FIXED: Find the ACTIVE suggested lot (not rejected lots) - const activeSuggestedLots = sameItemLotsInExpected.filter(lot => - lot.lotAvailability !== 'rejected' && - lot.stockOutLineStatus !== 'rejected' && - lot.processingStatus !== 'rejected' - ); + // ✅ Mark as processed + setProcessedQrCodes(prev => new Set(prev).add(latestQr)); + setLastProcessedQr(latestQr); - if (activeSuggestedLots.length === 0) { - console.error("No active suggested lots found for this item"); - setQrScanError(true); - setQrScanSuccess(false); - return; + // 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, ''); } - // 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; + // For direct lot number QR codes + if (lotNo) { + console.log(`Outside QR scan detected (direct): ${lotNo}`); + handleQrCodeSubmit(lotNo); } - - // 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]); - - - + }, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr]); const handleManualInputSubmit = useCallback(() => { if (qrScanInput.trim() !== '') { handleQrCodeSubmit(qrScanInput.trim()); @@ -933,17 +671,16 @@ useEffect(() => { const lotId = selectedLotForQr.lotId; // Create stock out line - + const stockOutLineData: CreateStockOutLine = { + consoCode: selectedLotForQr.pickOrderConsoCode, + pickOrderLineId: selectedLotForQr.pickOrderLineId, + inventoryLotLineId: selectedLotForQr.lotId, + qty: 0.0 + }; 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); @@ -960,13 +697,50 @@ useEffect(() => { }, 500); // Refresh data - await fetchAllCombinedLotData(); + await fetchJobOrderData(); } catch (error) { console.error("Error creating stock out line:", error); } } - }, [selectedLotForQr, fetchAllCombinedLotData]); + }, [selectedLotForQr, fetchJobOrderData]); + // ✅ Outside QR scanning - process QR codes from outside the page automatically + useEffect(() => { + if (qrValues.length > 0 && combinedLotData.length > 0) { + const latestQr = qrValues[qrValues.length - 1]; + + // Extract lot number from QR code + let lotNo = ''; + try { + const qrData = JSON.parse(latestQr); + if (qrData.stockInLineId && qrData.itemId) { + // For JSON QR codes, we need to fetch the lot number + fetchStockInLineInfo(qrData.stockInLineId) + .then((stockInLineInfo) => { + console.log("Outside QR scan - Stock in line info:", stockInLineInfo); + const extractedLotNo = stockInLineInfo.lotNo; + if (extractedLotNo) { + console.log(`Outside QR scan detected (JSON): ${extractedLotNo}`); + handleQrCodeSubmit(extractedLotNo); + } + }) + .catch((error) => { + console.error("Outside QR scan - Error fetching stock in line info:", error); + }); + return; // Exit early for JSON QR codes + } + } catch (error) { + // Not JSON format, treat as direct lot number + lotNo = latestQr.replace(/[{}]/g, ''); + } + + // For direct lot number QR codes + if (lotNo) { + console.log(`Outside QR scan detected (direct): ${lotNo}`); + handleQrCodeSubmit(lotNo); + } + } + }, [qrValues, combinedLotData, handleQrCodeSubmit]); const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { if (value === '' || value === null || value === undefined) { @@ -995,131 +769,36 @@ useEffect(() => { 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; - } - + + + const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => { try { - // ✅ FIXED: Calculate cumulative quantity correctly - const currentActualPickQty = lot.actualPickQty || 0; - const cumulativeQty = currentActualPickQty + newQty; + // ✅ Use the new second scan submit API + const result = await submitSecondScanQuantity( + lot.pickOrderId, + lot.itemId, + { + qty: submitQty, + isMissing: false, + isBad: false, + reason: undefined // ✅ Fix TypeScript error + } + ); - // ✅ 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'; + if (result.code === "SUCCESS") { + console.log(`✅ Second scan quantity submitted: ${submitQty}`); + await fetchJobOrderData(); // Refresh data } else { - newStatus = 'checked'; // QR scanned but no quantity submitted yet + console.error(`❌ Failed to submit second scan quantity: ${result.message}`); } - - 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); + console.error("Error submitting second scan quantity:", error); } - }, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]); - + }, [fetchJobOrderData]); // ✅ 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) => { @@ -1142,8 +821,21 @@ useEffect(() => { const handlePickExecutionFormSubmit = useCallback(async (data: any) => { try { console.log("Pick execution form submitted:", data); - - const result = await recordPickExecutionIssue(data); + if (!currentUserId) { + console.error("❌ No current user ID available"); + return; + } + const result = await recordSecondScanIssue( + selectedLotForExecutionForm.pickOrderId, + selectedLotForExecutionForm.itemId, + { + qty: data.actualPickQty, + isMissing: data.missQty > 0, + isBad: data.badItemQty > 0, + reason: data.issueRemark || '', + createdBy: currentUserId + } + ); console.log("Pick execution issue recorded:", result); if (result && result.code === "SUCCESS") { @@ -1154,19 +846,12 @@ useEffect(() => { setPickExecutionFormOpen(false); setSelectedLotForExecutionForm(null); - setQrScanError(false); - setQrScanSuccess(false); - setQrScanInput(''); - setIsManualScanning(false); - stopScan(); - resetScan(); - setProcessedQrCodes(new Set()); - setLastProcessedQr(''); - await fetchAllCombinedLotData(); + + await fetchJobOrderData(); } catch (error) { console.error("Error submitting pick execution form:", error); } - }, [fetchAllCombinedLotData]); + }, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]); // ✅ Calculate remaining required quantity const calculateRemainingRequiredQty = useCallback((lot: any) => { @@ -1248,92 +933,32 @@ useEffect(() => { }, []); // 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...`); + // ✅ Sort by routerIndex first, then by other criteria + const sortedData = [...combinedLotData].sort((a, b) => { + const aIndex = a.routerIndex || 0; + const bIndex = b.routerIndex || 0; - 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); + // Primary sort: by routerIndex + if (aIndex !== bIndex) { + return aIndex - bIndex; } - } - - await fetchAllCombinedLotData(); - console.log("Pick quantity submitted successfully!"); - - setTimeout(() => { - checkAndAutoAssignNext(); - }, 1000); + + // 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 || ''); + }); - } catch (error) { - console.error("Error submitting pick quantity:", error); - } -}, [fetchAllCombinedLotData, checkAndAutoAssignNext]); + const startIndex = paginationController.pageNum * paginationController.pageSize; + const endIndex = startIndex + paginationController.pageSize; + return sortedData.slice(startIndex, endIndex); + }, [combinedLotData, paginationController]); - - // ✅ Add these functions after line 395 + // ✅ Add these functions for manual scanning const handleStartScan = useCallback(() => { console.log(" Starting manual QR scan..."); setIsManualScanning(true); @@ -1352,6 +977,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe stopScan(); resetScan(); }, [stopScan, resetScan]); + const getStatusMessage = useCallback((lot: any) => { switch (lot.stockOutLineStatus?.toLowerCase()) { case 'pending': @@ -1370,43 +996,32 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe return t("Please finish QR code scan and pick order."); } }, [t]); + return ( - - - - {/* DO Header */} - {fgPickOrdersLoading ? ( - - - - ) : ( - fgPickOrders.length > 0 && ( + {/* Job Order Header */} + {jobOrderData && ( - {t("Shop Name")}: {fgPickOrders[0].shopName || '-'} - - - {t("Pick Order Code")}:{fgPickOrders[0].pickOrderCode || '-'} + {t("Job Order")}: {jobOrderData.pickOrder?.jobOrder?.name || '-'} - {t("Store ID")}: {fgPickOrders[0].storeId || '-'} + {t("Pick Order Code")}: {jobOrderData.pickOrder?.code || '-'} - {t("Ticket No.")}: {fgPickOrders[0].ticketNo || '-'} + {t("Target Date")}: {jobOrderData.pickOrder?.targetDate || '-'} - {t("Departure Time")}: {fgPickOrders[0].DepartureTime || '-'} + {t("Status")}: {jobOrderData.pickOrder?.status || '-'} - - ) )} + {/* Combined Lot Table */} @@ -1445,7 +1060,6 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe )} - @@ -1469,21 +1083,15 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe {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")} @@ -1512,7 +1120,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe {lot.itemCode} - {lot.itemName+'('+lot.stockUnit+')'} + {lot.itemName+'('+lot.uomDesc+')'} - {/* {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+')'; + return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} @@ -1549,12 +1150,12 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe height: '100%' }}> - )) )} - {/* ✅ Status Messages Display - Move here, outside the table */} - {/* -{paginatedData.length > 0 && ( - - {paginatedData.map((lot, index) => ( - - - {t("Lot")} {lot.lotNo}: {getStatusMessage(lot)} - - - ))} - -)} -*/} + - {/* ✅ 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 */} + + {/* ✅ Pick Execution Form Modal */} {pickExecutionFormOpen && selectedLotForExecutionForm && ( = ({ pickOrders }) => { const [tabIndex, setTabIndex] = useState(0); const [totalCount, setTotalCount] = useState(); const [isAssigning, setIsAssigning] = useState(false); + const [unassignedOrders, setUnassignedOrders] = useState([]); +const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState( typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' ); @@ -125,7 +135,47 @@ const JodetailSearch: React.FC = ({ pickOrders }) => { } }; // ✅ Manual assignment handler - uses the action function - + const loadUnassignedOrders = useCallback(async () => { + setIsLoadingUnassigned(true); + try { + const orders = await fetchUnassignedJobOrderPickOrders(); + setUnassignedOrders(orders); + } catch (error) { + console.error("Error loading unassigned orders:", error); + } finally { + setIsLoadingUnassigned(false); + } + }, []); + + // 分配订单给当前用户 + const handleAssignOrder = useCallback(async (pickOrderId: number) => { + if (!currentUserId) { + console.error("Missing user id in session"); + return; + } + + try { + const result = await assignJobOrderPickOrder(pickOrderId, currentUserId); + if (result.message === "Successfully assigned") { + console.log("✅ Successfully assigned pick order"); + // 刷新数据 + window.dispatchEvent(new CustomEvent('pickOrderAssigned')); + // 重新加载未分配订单列表 + loadUnassignedOrders(); + } else { + console.warn("⚠️ Assignment failed:", result.message); + alert(`Assignment failed: ${result.message}`); + } + } catch (error) { + console.error("❌ Error assigning order:", error); + alert("Error occurred during assignment"); + } + }, [currentUserId, loadUnassignedOrders]); + + // 在组件加载时获取未分配订单 + useEffect(() => { + loadUnassignedOrders(); + }, [loadUnassignedOrders]); const handleTabChange = useCallback>( (_e, newValue) => { @@ -333,80 +383,33 @@ const JodetailSearch: React.FC = ({ pickOrders }) => { - - - {t("Finished Good Order")} - - + {/* Last 2 buttons aligned right */} - - - - + {/* Unassigned Job Orders */} +{unassignedOrders.length > 0 && ( + + + {t("Unassigned Job Orders")} ({unassignedOrders.length}) + + + {unassignedOrders.map((order) => ( + + ))} + + +)} - {/* ✅ Updated print buttons with completion status */} - - -{/* - - */} - - - - - - @@ -419,8 +422,8 @@ const JodetailSearch: React.FC = ({ pickOrders }) => { }}> - - + + @@ -429,9 +432,9 @@ const JodetailSearch: React.FC = ({ pickOrders }) => { - {tabIndex === 0 && } - {tabIndex === 1 && } - {tabIndex === 2 && } + {tabIndex === 0 && } + {tabIndex === 1 && } + {tabIndex === 2 && } ); From 6915e01daebf589d00cfdafb97c73291104a6d3d Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 29 Sep 2025 01:00:02 +0800 Subject: [PATCH 3/8] update --- src/app/(main)/jodetail/page.tsx | 4 +-- .../Jodetail/FinishedGoodSearchWrapper.tsx | 4 +-- src/components/Jodetail/JobPickExecution.tsx | 6 ++--- .../Jodetail/JobPickExecutionsecondscan.tsx | 6 ++--- src/components/Jodetail/JodetailSearch.tsx | 4 +-- .../NavigationContent/NavigationContent.tsx | 2 +- src/i18n/zh/common.json | 16 ++++++++++- src/i18n/zh/jo.json | 27 ++++++++++++++++++- 8 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/app/(main)/jodetail/page.tsx b/src/app/(main)/jodetail/page.tsx index 19e2640..26b13e7 100644 --- a/src/app/(main)/jodetail/page.tsx +++ b/src/app/(main)/jodetail/page.tsx @@ -7,7 +7,7 @@ import React, { Suspense } from "react"; import GeneralLoading from "@/components/General/GeneralLoading"; export const metadata: Metadata = { - title: "Job Order detail" + title: "Job Order Detail" } const jo: React.FC = async () => { @@ -24,7 +24,7 @@ const jo: React.FC = async () => { rowGap={2} > - {t("Job Order detail")} + {t("Job Order Detail")} diff --git a/src/components/Jodetail/FinishedGoodSearchWrapper.tsx b/src/components/Jodetail/FinishedGoodSearchWrapper.tsx index 1df245d..da633c5 100644 --- a/src/components/Jodetail/FinishedGoodSearchWrapper.tsx +++ b/src/components/Jodetail/FinishedGoodSearchWrapper.tsx @@ -1,6 +1,6 @@ import { fetchPickOrders } from "@/app/api/pickOrder"; import GeneralLoading from "../General/GeneralLoading"; -import PickOrderSearch from "./FinishedGoodSearch"; +import PickOrderSearch from "./FinishedGoodSearchWrapper"; interface SubComponents { Loading: typeof GeneralLoading; @@ -18,7 +18,7 @@ const FinishedGoodSearchWrapper: React.FC & SubComponents = async () => { }), ]); - return ; + return ; }; FinishedGoodSearchWrapper.Loading = GeneralLoading; diff --git a/src/components/Jodetail/JobPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx index 87c6bf2..11c27a9 100644 --- a/src/components/Jodetail/JobPickExecution.tsx +++ b/src/components/Jodetail/JobPickExecution.tsx @@ -68,7 +68,7 @@ const QrCodeModal: React.FC<{ onQrCodeSubmit: (lotNo: string) => void; combinedLotData: any[]; }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { - const { t } = useTranslation("pickOrder"); + const { t } = useTranslation("jo"); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [manualInput, setManualInput] = useState(''); @@ -315,7 +315,7 @@ const QrCodeModal: React.FC<{ }; const JobPickExecution: React.FC = ({ filterArgs }) => { - const { t } = useTranslation("pickOrder"); + const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -1301,7 +1301,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { {t("Route")} {t("Item Code")} {t("Item Name")} - {t("Lot#")} + {t("Lot No")} {t("Lot Required Pick Qty")} {t("Scan Result")} {t("Submit Required Pick Qty")} diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx index fbc2122..1e56bd0 100644 --- a/src/components/Jodetail/JobPickExecutionsecondscan.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -61,7 +61,7 @@ const QrCodeModal: React.FC<{ onQrCodeSubmit: (lotNo: string) => void; combinedLotData: any[]; }> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => { - const { t } = useTranslation("pickOrder"); + const { t } = useTranslation("jo"); const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [manualInput, setManualInput] = useState(''); @@ -308,7 +308,7 @@ const QrCodeModal: React.FC<{ }; const JobPickExecution: React.FC = ({ filterArgs }) => { - const { t } = useTranslation("pickOrder"); + const { t } = useTranslation("jo"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -1082,7 +1082,7 @@ const paginatedData = useMemo(() => { {t("Route")} {t("Item Code")} {t("Item Name")} - {t("Lot#")} + {t("Lot No")} {t("Lot Required Pick Qty")} {t("Scan Result")} {t("Submit Required Pick Qty")} diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx index 96f07b6..e5d819c 100644 --- a/src/components/Jodetail/JodetailSearch.tsx +++ b/src/components/Jodetail/JodetailSearch.tsx @@ -43,7 +43,7 @@ type SearchQuery = Partial< type SearchParamNames = keyof SearchQuery; const JodetailSearch: React.FC = ({ pickOrders }) => { - const { t } = useTranslation("pickOrder"); + const { t } = useTranslation("jo"); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; @@ -422,7 +422,7 @@ const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); }}> - + diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index c23515a..22ae2a9 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -210,7 +210,7 @@ const NavigationContent: React.FC = () => { }, { icon: , - label: "Job Order detail", + label: "Job Order Detail", path: "/jodetail", }, ], diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index f898cbf..2fd3abf 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -89,6 +89,20 @@ "Put Away Scan": "上架掃碼", "Finished Good Order": "成品出倉", "finishedGood": "成品", - "Router": "執貨路線" + "Router": "執貨路線", + "Job Order Detail": "工單細節", + "No data available": "沒有資料", + "Start Scan": "開始掃碼", + "Stop Scan": "停止掃碼", + "Scan Result": "掃碼結果", + "Expiry Date": "有效期", + "Pick Order Code": "提料單編號", + "Target Date": "需求日期", + "Lot Required Pick Qty": "批號需求數量", + "Job Order Match": "工單匹配", + "All Pick Order Lots": "所有提料單批號", + "Row per page": "每頁行數", + "No data available": "沒有資料", + "jodetail": "工單細節" } diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 0ed0760..874a134 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -17,5 +17,30 @@ "Planning": "計劃中", "Scanned": "已掃碼", "Scan Status": "掃碼狀態", - "Start Job Order": "開始工單" + "Start Job Order": "開始工單", + "Job Order Detail": "工單細節", + "Pick Order Detail": "提料單細節", + "Finished Job Order Record": "已完成工單記錄", + "Index": "索引", + "Route": "路線", + "Item Code": "物料編號", + "Item Name": "物料名稱", + "Qty": "數量", + "Unit": "單位", + "Location": "位置", + "Scan Result": "掃碼結果", + "Expiry Date": "有效期", + "Pick Order Code": "提料單編號", + "Target Date": "需求日期", + "Lot Required Pick Qty": "批號需求數量", + "Job Order Match": "工單匹配", + "Lot No": "批號", + "Submit Required Pick Qty": "提交需求數量", + "All Pick Order Lots": "所有提料單批號", + "Row per page": "每頁行數", + "No data available": "沒有資料", + "jodetail": "工單細節", + "Start QR Scan": "開始QR掃碼", + "Stop QR Scan": "停止QR掃碼", + "Rows per page": "每頁行數" } From 5590308d6eded9b4123402265c871e364a4601d1 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 29 Sep 2025 13:53:55 +0800 Subject: [PATCH 4/8] update --- src/app/api/jo/actions.ts | 13 + .../Jodetail/FInishedJobOrderRecord.tsx | 506 +++++++++++------- src/i18n/zh/jo.json | 26 +- 3 files changed, 348 insertions(+), 197 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 0955a79..8113b1d 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -267,4 +267,17 @@ export const manualCreateJo = cache(async (data: SaveJo) => { body: JSON.stringify(data), headers: { "Content-Type": "application/json" } }) +}) + +export const fetchCompletedJobOrderPickOrdersWithCompletedSecondScan = cache(async (userId: number) => { + return serverFetchJson(`${BASE_API_URL}/jo/completed-job-order-pick-orders-with-completed-second-scan/${userId}`, { + method: "GET", + headers: { "Content-Type": "application/json" } + }) +}) +export const fetchCompletedJobOrderPickOrderLotDetails = cache(async (pickOrderId: number) => { + return serverFetchJson(`${BASE_API_URL}/jo/completed-job-order-pick-order-lot-details/${pickOrderId}`, { + method: "GET", + headers: { "Content-Type": "application/json" } + }) }) \ No newline at end of file diff --git a/src/components/Jodetail/FInishedJobOrderRecord.tsx b/src/components/Jodetail/FInishedJobOrderRecord.tsx index 4756e95..5845588 100644 --- a/src/components/Jodetail/FInishedJobOrderRecord.tsx +++ b/src/components/Jodetail/FInishedJobOrderRecord.tsx @@ -24,102 +24,101 @@ import { Accordion, AccordionSummary, AccordionDetails, + Checkbox, // ✅ Add Checkbox import } from "@mui/material"; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import { useCallback, useEffect, useState, useRef, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { useRouter } from "next/navigation"; import { - fetchALLPickOrderLineLotDetails, - updateStockOutLineStatus, - createStockOutLine, - recordPickExecutionIssue, - fetchFGPickOrders, - FGPickOrderResponse, - autoAssignAndReleasePickOrder, - AutoAssignReleaseResponse, - checkPickOrderCompletion, - PickOrderCompletionResponse, - checkAndCompletePickOrderByConsoCode, - fetchCompletedDoPickOrders, // ✅ 新增:使用新的 API - CompletedDoPickOrderResponse, - CompletedDoPickOrderSearchParams, - fetchLotDetailsByPickOrderId // ✅ 修复:导入类型 -} from "@/app/api/pickOrder/actions"; + fetchCompletedJobOrderPickOrdersWithCompletedSecondScan, + fetchCompletedJobOrderPickOrderLotDetails +} from "@/app/api/jo/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions"; import { FormProvider, useForm, } from "react-hook-form"; import SearchBox, { Criterion } from "../SearchBox"; -import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; -import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; -import QrCodeIcon from '@mui/icons-material/QrCode'; -import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; -import { fetchStockInLineInfo } from "@/app/api/po/actions"; -import GoodPickExecutionForm from "./JobPickExecutionForm"; -import FGPickOrderCard from "./FGPickOrderCard"; interface Props { filterArgs: Record; } -// ✅ 新增:已完成的 DO Pick Order 接口 -interface CompletedDoPickOrder { +// ✅ 修改:已完成的 Job Order Pick Order 接口 +interface CompletedJobOrderPickOrder { id: number; pickOrderId: number; pickOrderCode: string; pickOrderConsoCode: string; + pickOrderTargetDate: 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[]; + jobOrderId: number; + jobOrderCode: string; + jobOrderName: string; + reqQty: number; + uom: string; + planStart: string; + planEnd: string; + secondScanCompleted: boolean; + totalItems: number; + completedItems: number; } -// ✅ 新增:Pick Order 数据接口 -interface PickOrderData { +// ✅ 新增:Lot 详情接口 +interface LotDetail { + lotId: number; + lotNo: string; + expiryDate: string; + location: string; + availableQty: number; + requiredQty: number; + actualPickQty: number; + processingStatus: string; + lotAvailability: string; pickOrderId: number; pickOrderCode: string; pickOrderConsoCode: string; - pickOrderStatus: string; - completedDate: string; - lots: any[]; + pickOrderLineId: number; + stockOutLineId: number; + stockOutLineStatus: string; + routerIndex: number; + routerArea: string; + routerRoute: string; + uomShortDesc: string; + secondQrScanStatus: string; + itemId: number; + itemCode: string; + itemName: string; + uomCode: string; + uomDesc: string; } const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { - const { t } = useTranslation("pickOrder"); + const { t } = useTranslation("jo"); 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); + // ✅ 修改:已完成 Job Order Pick Orders 状态 + const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState([]); + const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false); - // ✅ 新增:详情视图状态 - const [selectedDoPickOrder, setSelectedDoPickOrder] = useState(null); + // ✅ 修改:详情视图状态 + const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState(null); const [showDetailView, setShowDetailView] = useState(false); - const [detailLotData, setDetailLotData] = useState([]); + const [detailLotData, setDetailLotData] = useState([]); + const [detailLotDataLoading, setDetailLotDataLoading] = useState(false); - // ✅ 新增:搜索状态 + // ✅ 修改:搜索状态 const [searchQuery, setSearchQuery] = useState>({}); - const [filteredDoPickOrders, setFilteredDoPickOrders] = useState([]); + const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState([]); - // ✅ 新增:分页状态 + // ✅ 修改:分页状态 const [paginationController, setPaginationController] = useState({ pageNum: 0, pageSize: 10, @@ -128,58 +127,82 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { const formProps = useForm(); const errors = formProps.formState.errors; - // ✅ 修改:使用新的 API 获取已完成的 DO Pick Orders - const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => { + // ✅ 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders + const fetchCompletedJobOrderPickOrdersData = useCallback(async () => { if (!currentUserId) return; - setCompletedDoPickOrdersLoading(true); + setCompletedJobOrderPickOrdersLoading(true); try { - console.log("🔍 Fetching completed DO pick orders with params:", searchParams); + console.log("🔍 Fetching completed Job Order pick orders..."); - const completedDoPickOrders = await fetchCompletedDoPickOrders(currentUserId, searchParams); + const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersWithCompletedSecondScan(currentUserId); - setCompletedDoPickOrders(completedDoPickOrders); - setFilteredDoPickOrders(completedDoPickOrders); - console.log("✅ Fetched completed DO pick orders:", completedDoPickOrders); + setCompletedJobOrderPickOrders(completedJobOrderPickOrders); + setFilteredJobOrderPickOrders(completedJobOrderPickOrders); + console.log("✅ Fetched completed Job Order pick orders:", completedJobOrderPickOrders); } catch (error) { - console.error("❌ Error fetching completed DO pick orders:", error); - setCompletedDoPickOrders([]); - setFilteredDoPickOrders([]); + console.error("❌ Error fetching completed Job Order pick orders:", error); + setCompletedJobOrderPickOrders([]); + setFilteredJobOrderPickOrders([]); } finally { - setCompletedDoPickOrdersLoading(false); + setCompletedJobOrderPickOrdersLoading(false); } }, [currentUserId]); - // ✅ 初始化时获取数据 + // ✅ 新增:获取 lot 详情数据 + const fetchLotDetailsData = useCallback(async (pickOrderId: number) => { + setDetailLotDataLoading(true); + try { + console.log("🔍 Fetching lot details for pick order:", pickOrderId); + + const lotDetails = await fetchCompletedJobOrderPickOrderLotDetails(pickOrderId); + + setDetailLotData(lotDetails); + console.log("✅ Fetched lot details:", lotDetails); + } catch (error) { + console.error("❌ Error fetching lot details:", error); + setDetailLotData([]); + } finally { + setDetailLotDataLoading(false); + } + }, []); + + // ✅ 修改:初始化时获取数据 useEffect(() => { if (currentUserId) { - fetchCompletedDoPickOrdersData(); + fetchCompletedJobOrderPickOrdersData(); } - }, [currentUserId, fetchCompletedDoPickOrdersData]); + }, [currentUserId, fetchCompletedJobOrderPickOrdersData]); - // ✅ 修改:搜索功能使用新的 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 filtered = completedJobOrderPickOrders.filter((pickOrder) => { + const pickOrderCodeMatch = !query.pickOrderCode || + pickOrder.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); + + const jobOrderCodeMatch = !query.jobOrderCode || + pickOrder.jobOrderCode?.toLowerCase().includes((query.jobOrderCode || "").toLowerCase()); + + const jobOrderNameMatch = !query.jobOrderName || + pickOrder.jobOrderName?.toLowerCase().includes((query.jobOrderName || "").toLowerCase()); + + return pickOrderCodeMatch && jobOrderCodeMatch && jobOrderNameMatch; + }); + + setFilteredJobOrderPickOrders(filtered); + console.log("Filtered Job Order pick orders count:", filtered.length); + }, [completedJobOrderPickOrders]); - // ✅ 修复:重命名函数避免重复声明 + // ✅ 修改:重置搜索 const handleSearchReset = useCallback(() => { setSearchQuery({}); - fetchCompletedDoPickOrdersData(); // 重新获取所有数据 - }, [fetchCompletedDoPickOrdersData]); + setFilteredJobOrderPickOrders(completedJobOrderPickOrders); + }, [completedJobOrderPickOrders]); - // ✅ 分页功能 + // ✅ 修改:分页功能 const handlePageChange = useCallback((event: unknown, newPage: number) => { setPaginationController(prev => ({ ...prev, @@ -195,14 +218,14 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { }); }, []); - // ✅ 分页数据 + // ✅ 修改:分页数据 const paginatedData = useMemo(() => { const startIndex = paginationController.pageNum * paginationController.pageSize; const endIndex = startIndex + paginationController.pageSize; - return filteredDoPickOrders.slice(startIndex, endIndex); - }, [filteredDoPickOrders, paginationController]); + return filteredJobOrderPickOrders.slice(startIndex, endIndex); + }, [filteredJobOrderPickOrders, paginationController]); - // ✅ 搜索条件 + // ✅ 修改:搜索条件 const searchCriteria: Criterion[] = [ { label: t("Pick Order Code"), @@ -210,59 +233,42 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { type: "text", }, { - label: t("Shop Name"), - paramName: "shopName", + label: t("Job Order Code"), + paramName: "jobOrderCode", type: "text", }, { - label: t("Delivery No"), - paramName: "deliveryNo", + label: t("Job Order Item Name"), + paramName: "jobOrderName", type: "text", } ]; - const handleDetailClick = useCallback(async (doPickOrder: CompletedDoPickOrder) => { - setSelectedDoPickOrder(doPickOrder); + // ✅ 修改:详情点击处理 + const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => { + setSelectedJobOrderPickOrder(jobOrderPickOrder); 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 - } - })); - } - }, []); - + // ✅ 获取 lot 详情数据 + await fetchLotDetailsData(jobOrderPickOrder.pickOrderId); + + // ✅ 触发打印按钮状态更新 - 基于详情数据 + const allCompleted = jobOrderPickOrder.secondScanCompleted; + + // ✅ 发送事件,包含标签页信息 + window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { + detail: { + allLotsCompleted: allCompleted, + tabIndex: 2 // ✅ 明确指定这是来自标签页 2 的事件 + } + })); + + }, [fetchLotDetailsData]); - // ✅ 返回列表视图 + // ✅ 修改:返回列表视图 const handleBackToList = useCallback(() => { setShowDetailView(false); - setSelectedDoPickOrder(null); + setSelectedJobOrderPickOrder(null); setDetailLotData([]); // ✅ 返回列表时禁用打印按钮 @@ -274,9 +280,8 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { })); }, []); - - // ✅ 如果显示详情视图,渲染类似 GoodPickExecution 的表格 - if (showDetailView && selectedDoPickOrder) { + // ✅ 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息 + if (showDetailView && selectedJobOrderPickOrder) { return ( @@ -286,64 +291,166 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { {t("Back to List")} - {t("Pick Order Details")}: {selectedDoPickOrder.pickOrderCode} + {t("Job Order Pick Order Details")}: {selectedJobOrderPickOrder.pickOrderCode} - {/* FG Pick Orders 信息 */} - - {selectedDoPickOrder.fgPickOrders.map((fgOrder, index) => ( - {}} // 只读模式 - /> - ))} - + {/* Job Order 信息卡片 */} + + + + + {t("Pick Order Code")}: {selectedJobOrderPickOrder.pickOrderCode} + + + {t("Job Order Code")}: {selectedJobOrderPickOrder.jobOrderCode} + + + {t("Job Order Item Name")}: {selectedJobOrderPickOrder.jobOrderName} + + + {t("Target Date")}: {selectedJobOrderPickOrder.pickOrderTargetDate} + + + + + + {t("Required Qty")}: {selectedJobOrderPickOrder.reqQty} {selectedJobOrderPickOrder.uom} + + + + + - {/* 类似 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} - - - - - ))} - -
-
+ {/* ✅ 修改:Lot 详情表格 - 添加复选框列 */} + + + + {t("Lot Details")} + + + {detailLotDataLoading ? ( + + + + ) : ( + + + + + {t("Index")} + {t("Route")} + {t("Item Code")} + {t("Item Name")} + {t("Lot No")} + {t("Location")} + {t("Required Qty")} + {t("Actual Pick Qty")} + {t("Processing Status")} + {t("Second Scan Status")} + + + + {detailLotData.length === 0 ? ( + + {/* ✅ 恢复原来的 colSpan */} + + {t("No lot details available")} + + + + ) : ( + detailLotData.map((lot, index) => ( + + + + {index + 1} + + + + + {lot.routerRoute || '-'} + + + {lot.itemCode} + {lot.itemName} + {lot.lotNo} + {lot.location} + + {lot.requiredQty?.toLocaleString() || 0} ({lot.uomShortDesc}) + + + {lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc}) + + {/* ✅ 修改:Processing Status 使用复选框 */} + + + + + + {/* ✅ 修改:Second Scan Status 使用复选框 */} + + + + + + + )) + )} + +
+
+ )} +
+
); } - // ✅ 默认列表视图 + // ✅ 修改:默认列表视图 return ( @@ -357,7 +464,7 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { {/* 加载状态 */} - {completedDoPickOrdersLoading ? ( + {completedJobOrderPickOrdersLoading ? ( @@ -365,50 +472,59 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { {/* 结果统计 */} - {t("Total")}: {filteredDoPickOrders.length} {t("completed DO pick orders")} + {t("Total")}: {filteredJobOrderPickOrders.length} {t("completed Job Order pick orders with matching")} {/* 列表 */} - {filteredDoPickOrders.length === 0 ? ( + {filteredJobOrderPickOrders.length === 0 ? ( - {t("No completed DO pick orders found")} + {t("No completed Job Order pick orders with matching found")} ) : ( - {paginatedData.map((doPickOrder) => ( - + {paginatedData.map((jobOrderPickOrder) => ( + - {doPickOrder.pickOrderCode} + {jobOrderPickOrder.pickOrderCode} + + + {jobOrderPickOrder.jobOrderName} - {jobOrderPickOrder.jobOrderCode} - {doPickOrder.shopName} - {doPickOrder.deliveryNo} + {t("Completed")}: {new Date(jobOrderPickOrder.completedDate).toLocaleString()} - {t("Completed")}: {new Date(doPickOrder.completedDate).toLocaleString()} + {t("Target Date")}: {jobOrderPickOrder.pickOrderTargetDate} - {doPickOrder.fgPickOrders.length} {t("FG orders")} + {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")} + @@ -419,10 +535,10 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { )} {/* 分页 */} - {filteredDoPickOrders.length > 0 && ( + {filteredJobOrderPickOrders.length > 0 && ( Date: Mon, 29 Sep 2025 15:08:46 +0800 Subject: [PATCH 5/8] update --- src/app/(main)/jodetail/page.tsx | 4 ++-- src/components/Jodetail/JobPickExecution.tsx | 4 +--- src/components/Jodetail/JobPickExecutionsecondscan.tsx | 4 +--- src/components/NavigationContent/NavigationContent.tsx | 2 +- src/i18n/zh/common.json | 2 +- src/i18n/zh/jo.json | 8 ++++---- 6 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/app/(main)/jodetail/page.tsx b/src/app/(main)/jodetail/page.tsx index 26b13e7..3f5b222 100644 --- a/src/app/(main)/jodetail/page.tsx +++ b/src/app/(main)/jodetail/page.tsx @@ -7,7 +7,7 @@ import React, { Suspense } from "react"; import GeneralLoading from "@/components/General/GeneralLoading"; export const metadata: Metadata = { - title: "Job Order Detail" + title: "Job Order Pickexcution" } const jo: React.FC = async () => { @@ -24,7 +24,7 @@ const jo: React.FC = async () => { rowGap={2} > - {t("Job Order Detail")} + {t("Job Order Pickexcution")} diff --git a/src/components/Jodetail/JobPickExecution.tsx b/src/components/Jodetail/JobPickExecution.tsx index 11c27a9..03f345e 100644 --- a/src/components/Jodetail/JobPickExecution.tsx +++ b/src/components/Jodetail/JobPickExecution.tsx @@ -1244,9 +1244,7 @@ const JobPickExecution: React.FC = ({ filterArgs }) => { {/* Combined Lot Table */} - - {t("All Pick Order Lots")} - + {!isManualScanning ? ( diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx index 1e56bd0..eedc840 100644 --- a/src/components/Jodetail/JobPickExecutionsecondscan.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -1025,9 +1025,7 @@ const paginatedData = useMemo(() => { {/* Combined Lot Table */} - - {t("All Pick Order Lots")} - + {!isManualScanning ? ( diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 22ae2a9..aa5b1df 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -210,7 +210,7 @@ const NavigationContent: React.FC = () => { }, { icon: , - label: "Job Order Detail", + label: "Job Order Pickexcution", path: "/jodetail", }, ], diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 2fd3abf..401e38c 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -90,7 +90,7 @@ "Finished Good Order": "成品出倉", "finishedGood": "成品", "Router": "執貨路線", - "Job Order Detail": "工單細節", + "Job Order Pickexcution": "工單提料", "No data available": "沒有資料", "Start Scan": "開始掃碼", "Stop Scan": "停止掃碼", diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index e0bc03f..176ef94 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -18,13 +18,13 @@ "Scanned": "已掃碼", "Scan Status": "掃碼狀態", "Start Job Order": "開始工單", - "Job Order Detail": "工單細節", + "Job Order Pickexcution": "工單提料", "Pick Order Detail": "提料單細節", "Finished Job Order Record": "已完成工單記錄", "Index": "編號", "Route": "路線", - "Item Code": "物料編號", - "Item Name": "物料名稱", + "Item Code": "成品/半成品編號", + "Item Name": "成品/半成品名稱", "Qty": "數量", "Unit": "單位", "Location": "位置", @@ -63,6 +63,6 @@ "Pick Order Status": "提料單狀態", "Second Scan Status": "對料狀態", "Job Order Pick Order Details": "工單提料單詳情", - "Scanning": "掃碼中", + "Scanning...": "掃碼中", "Unassigned Job Orders": "未分配工單" } From 396c00bfe6a8a28e6984d22779742db77e7ac0e1 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Sep 2025 19:01:51 +0800 Subject: [PATCH 6/8] update job order --- src/app/api/jo/actions.ts | 20 ++++++++++++---- src/components/JoSave/ActionButtons.tsx | 21 +++++++++++----- src/components/JoSave/JoSave.tsx | 32 +++++++++++++++++++++---- src/components/JoSave/PickTable.tsx | 7 +++--- src/i18n/zh/jo.json | 12 ++++++++-- 5 files changed, 71 insertions(+), 21 deletions(-) diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 8113b1d..217bbc0 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -38,15 +38,17 @@ export interface SearchJoResult { status: JoStatus; } -export interface ReleaseJoRequest { +// For Jo Button Actions +export interface CommonActionJoRequest { id: number; } -export interface ReleaseJoResponse { +export interface CommonActionJoResponse { id: number; entity: { status: JoStatus } } +// For Jo Process export interface IsOperatorExistResponse { id: number | null; name: string; @@ -251,8 +253,17 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => { return response }) -export const releaseJo = cache(async (data: ReleaseJoRequest) => { - return serverFetchJson(`${BASE_API_URL}/jo/release`, +export const releaseJo = cache(async (data: CommonActionJoRequest) => { + return serverFetchJson(`${BASE_API_URL}/jo/release`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }) +}) + +export const startJo = cache(async (data: CommonActionJoRequest) => { + return serverFetchJson(`${BASE_API_URL}/jo/start`, { method: "POST", body: JSON.stringify(data), @@ -261,7 +272,6 @@ export const releaseJo = cache(async (data: ReleaseJoRequest) => { }) export const manualCreateJo = cache(async (data: SaveJo) => { - console.log(data) return serverFetchJson(`${BASE_API_URL}/jo/manualCreate`, { method: "POST", body: JSON.stringify(data), diff --git a/src/components/JoSave/ActionButtons.tsx b/src/components/JoSave/ActionButtons.tsx index 480576d..12e0892 100644 --- a/src/components/JoSave/ActionButtons.tsx +++ b/src/components/JoSave/ActionButtons.tsx @@ -14,6 +14,7 @@ type Props = { interface ErrorEntry { qtyErr: boolean; scanErr: boolean; + pickErr: boolean; } const ActionButtons: React.FC = ({ @@ -36,24 +37,31 @@ const ActionButtons: React.FC = ({ const errors: ErrorEntry = useMemo(() => { let qtyErr = false; let scanErr = false; + let pickErr = false pickLines.forEach((line) => { if (!qtyErr) { const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0 - qtyErr = pickedQty > 0 && pickedQty >= line.reqQty + qtyErr = pickedQty <= 0 || pickedQty < line.reqQty } if (!scanErr) { scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false } + + if (!pickErr) { + pickErr = line.pickedLotNo === null + } }) return { qtyErr: qtyErr, - scanErr: scanErr + scanErr: scanErr, + pickErr: pickErr } - }, [pickLines]) + }, [pickLines, status]) + console.log(pickLines) return ( {status === "planning" && ( @@ -71,12 +79,13 @@ const ActionButtons: React.FC = ({ variant="outlined" startIcon={} onClick={handleStart} - disabled={errors.qtyErr || errors.scanErr} + disabled={errors.qtyErr || errors.scanErr || errors.pickErr} > {t("Start Job Order")} - {errors.scanErr && ({t("Please scan the item qr code.")})} - {errors.qtyErr && ({t("Please make sure the qty is enough.")})} + {errors.pickErr && ({t("Please make sure all required items are picked")})} + {errors.scanErr && ({t("Please scan the item qr code")})} + {errors.qtyErr && ({t("Please make sure the qty is enough")})} )}
diff --git a/src/components/JoSave/JoSave.tsx b/src/components/JoSave/JoSave.tsx index 000768c..1372bef 100644 --- a/src/components/JoSave/JoSave.tsx +++ b/src/components/JoSave/JoSave.tsx @@ -8,12 +8,13 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } fro import { Button, Stack, Typography } from "@mui/material"; import StartIcon from "@mui/icons-material/Start"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import { releaseJo } from "@/app/api/jo/actions"; +import { releaseJo, startJo } from "@/app/api/jo/actions"; import InfoCard from "./InfoCard"; import PickTable from "./PickTable"; import ActionButtons from "./ActionButtons"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; +import { submitDialog } from "../Swal/CustomAlerts"; type Props = { id?: number; @@ -93,12 +94,18 @@ const JoSave: React.FC = ({ shouldValidate: true, shouldDirty: true, }); + + // Ask user and confirm to start JO + await submitDialog(() => handleStart(), t, { + title: t("Do you want to start job order"), + confirmButtonText: t("Start Job Order") + }) } } } } finally { - scanner.resetScan() - setIsUploading(false) + scanner.resetScan() + setIsUploading(false) } }, []) @@ -126,7 +133,6 @@ const JoSave: React.FC = ({ formProps.setValue("status", response.entity.status) } } - } catch (e) { // backend error setServerError(t("An error has occurred. Please try again later.")); @@ -137,7 +143,23 @@ const JoSave: React.FC = ({ }, []) const handleStart = useCallback(async () => { - console.log("first") + try { + setIsUploading(true) + if (id) { + const response = await startJo({ id: id }) + if (response) { + formProps.setValue("status", response.entity.status) + + pickLines.map((line) => ({...line, status: "completed"})) + } + } + } catch (e) { + // backend error + setServerError(t("An error has occurred. Please try again later.")); + console.log(e); + } finally { + setIsUploading(false) + } }, []) // --------------------------------------------- Form Submit --------------------------------------------- // diff --git a/src/components/JoSave/PickTable.tsx b/src/components/JoSave/PickTable.tsx index 064b946..c0def2e 100644 --- a/src/components/JoSave/PickTable.tsx +++ b/src/components/JoSave/PickTable.tsx @@ -53,8 +53,8 @@ const PickTable: React.FC = ({ if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { return notPickedStatusColumn } - const scanStatus = params.row.pickedLotNo.map((pln) => Boolean(pln.isScanned)) - return isEmpty(scanStatus) ? notPickedStatusColumn : {scanStatus.map((status) => scanStatusColumn(status))} + const scanStatus = params.row.pickedLotNo.map((pln) => params.row.status === "completed" ? true : Boolean(pln.isScanned)) + return isEmpty(scanStatus) ? notPickedStatusColumn : {scanStatus.map((status) => scanStatusColumn(status))} }, }, { @@ -107,10 +107,11 @@ const PickTable: React.FC = ({ align: "right", headerAlign: "right", renderCell: (params: GridRenderCellParams) => { + const status = Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))) || params.row.status === "completed" return ( <> {params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))} - {scanStatusColumn(Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))))} + {scanStatusColumn(status)} ) }, diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 176ef94..2785d6e 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -16,6 +16,11 @@ "Pending for pick": "待提料", "Planning": "計劃中", "Scanned": "已掃碼", + "Processing": "已開始工序", + "Storing": "入倉中", + "completed": "已完成", + "Completed": "已完成", + "Cancel": "取消", "Scan Status": "掃碼狀態", "Start Job Order": "開始工單", "Job Order Pickexcution": "工單提料", @@ -30,7 +35,6 @@ "Location": "位置", "Scan Result": "掃碼結果", "Expiry Date": "有效期", - "Pick Order Code": "提料單編號", "Target Date": "需求日期", "Lot Required Pick Qty": "批號需求數量", "Job Order Match": "工單匹配", @@ -64,5 +68,9 @@ "Second Scan Status": "對料狀態", "Job Order Pick Order Details": "工單提料單詳情", "Scanning...": "掃碼中", - "Unassigned Job Orders": "未分配工單" + "Unassigned Job Orders": "未分配工單", + "Please scan the item qr code": "請掃描物料二維碼", + "Please make sure the qty is enough": "請確保物料數量是足夠", + "Please make sure all required items are picked": "請確保所有物料已被提取", + "Do you want to start job order": "是否開始工單" } From c04a70fbff9e37940383fd6d9140cc65797f42c0 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Sep 2025 19:09:41 +0800 Subject: [PATCH 7/8] quick update --- src/components/JoSave/JoSave.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/JoSave/JoSave.tsx b/src/components/JoSave/JoSave.tsx index 1372bef..1359f86 100644 --- a/src/components/JoSave/JoSave.tsx +++ b/src/components/JoSave/JoSave.tsx @@ -151,6 +151,8 @@ const JoSave: React.FC = ({ formProps.setValue("status", response.entity.status) pickLines.map((line) => ({...line, status: "completed"})) + + handleBack() } } } catch (e) { From 511f669a0b7685420b2ab350edf9d7d4ac73544c Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Mon, 29 Sep 2025 20:45:02 +0800 Subject: [PATCH 8/8] update --- .../GoodPickExecutionForm.tsx | 201 +++++++++--------- .../GoodPickExecutiondetail.tsx | 22 +- src/i18n/zh/jo.json | 2 +- src/i18n/zh/pickOrder.json | 5 +- 4 files changed, 118 insertions(+), 112 deletions(-) diff --git a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx index b7fe86d..89d50b6 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx @@ -20,7 +20,7 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { GetPickOrderLineInfo, PickExecutionIssueData } from "@/app/api/pickOrder/actions"; import { fetchEscalationCombo } from "@/app/api/user/actions"; - +import { useRef } from "react"; interface LotPickData { id: number; lotId: number; @@ -81,7 +81,6 @@ const PickExecutionForm: React.FC = ({ const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(false); const [handlers, setHandlers] = useState>([]); - // 计算剩余可用数量 const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { const remainingQty = lot.inQty - lot.outQty; @@ -92,7 +91,18 @@ const PickExecutionForm: React.FC = ({ // The actualPickQty in the form should be independent of the database value return lot.requiredQty || 0; }, []); - + const remaining = selectedLot ? calculateRemainingAvailableQty(selectedLot) : 0; + const req = selectedLot ? calculateRequiredQty(selectedLot) : 0; + + const ap = Number(formData.actualPickQty) || 0; + const miss = Number(formData.missQty) || 0; + const bad = Number(formData.badItemQty) || 0; + + // Max the user can type + const maxPick = Math.min(remaining, req); + const maxIssueTotal = Math.max(0, req - ap); // remaining room for miss+bad + + const clamp0 = (v: any) => Math.max(0, Number(v) || 0); // 获取处理人员列表 useEffect(() => { const fetchHandlers = async () => { @@ -107,55 +117,49 @@ const PickExecutionForm: React.FC = ({ fetchHandlers(); }, []); - // 初始化表单数据 - 每次打开时都重新初始化 + const initKeyRef = useRef(null); + useEffect(() => { - if (open && selectedLot && selectedPickOrderLine && pickOrderId) { - const getSafeDate = (dateValue: any): string => { - if (!dateValue) return new Date().toISOString().split('T')[0]; - try { - const date = new Date(dateValue); - if (isNaN(date.getTime())) { - return new Date().toISOString().split('T')[0]; - } - return date.toISOString().split('T')[0]; - } catch { - return new Date().toISOString().split('T')[0]; - } - }; + if (!open || !selectedLot || !selectedPickOrderLine || !pickOrderId) return; - // 计算剩余可用数量 - const remainingAvailableQty = calculateRemainingAvailableQty(selectedLot); - const requiredQty = calculateRequiredQty(selectedLot); - console.log("=== PickExecutionForm Debug ==="); - console.log("selectedLot:", selectedLot); - console.log("inQty:", selectedLot.inQty); - console.log("outQty:", selectedLot.outQty); - console.log("holdQty:", selectedLot.holdQty); - console.log("availableQty:", selectedLot.availableQty); - console.log("calculated remainingAvailableQty:", remainingAvailableQty); - console.log("=== End Debug ==="); - setFormData({ - pickOrderId: pickOrderId, - pickOrderCode: selectedPickOrderLine.pickOrderCode, - pickOrderCreateDate: getSafeDate(pickOrderCreateDate), - pickExecutionDate: new Date().toISOString().split('T')[0], - pickOrderLineId: selectedPickOrderLine.id, - itemId: selectedPickOrderLine.itemId, - itemCode: selectedPickOrderLine.itemCode, - itemDescription: selectedPickOrderLine.itemName, - lotId: selectedLot.lotId, - lotNo: selectedLot.lotNo, - storeLocation: selectedLot.location, - requiredQty: selectedLot.requiredQty, - actualPickQty: selectedLot.actualPickQty || 0, - missQty: 0, - badItemQty: 0, // 初始化为 0,用户需要手动输入 - issueRemark: '', - pickerName: '', - handledBy: undefined, - }); - } - }, [open, selectedLot, selectedPickOrderLine, pickOrderId, pickOrderCreateDate, calculateRemainingAvailableQty]); + // Only initialize once per (pickOrderLineId + lotId) while dialog open + const key = `${selectedPickOrderLine.id}-${selectedLot.lotId}`; + if (initKeyRef.current === key) return; + + const getSafeDate = (dateValue: any): string => { + if (!dateValue) return new Date().toISOString().split('T')[0]; + try { + const d = new Date(dateValue); + return isNaN(d.getTime()) ? new Date().toISOString().split('T')[0] : d.toISOString().split('T')[0]; + } catch { + return new Date().toISOString().split('T')[0]; + } + }; + + setFormData({ + pickOrderId: pickOrderId, + pickOrderCode: selectedPickOrderLine.pickOrderCode, + pickOrderCreateDate: getSafeDate(pickOrderCreateDate), + pickExecutionDate: new Date().toISOString().split('T')[0], + pickOrderLineId: selectedPickOrderLine.id, + itemId: selectedPickOrderLine.itemId, + itemCode: selectedPickOrderLine.itemCode, + itemDescription: selectedPickOrderLine.itemName, + lotId: selectedLot.lotId, + lotNo: selectedLot.lotNo, + storeLocation: selectedLot.location, + requiredQty: selectedLot.requiredQty, + actualPickQty: selectedLot.actualPickQty || 0, + missQty: 0, + badItemQty: 0, + issueRemark: '', + pickerName: '', + handledBy: undefined, + }); + + initKeyRef.current = key; + }, [open, selectedPickOrderLine?.id, selectedLot?.lotId, pickOrderId, pickOrderCreateDate]); + // Mutually exclusive inputs: picking vs reporting issues const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => { setFormData(prev => ({ ...prev, [field]: value })); @@ -168,30 +172,23 @@ const PickExecutionForm: React.FC = ({ // ✅ Update form validation to require either missQty > 0 OR badItemQty > 0 const validateForm = (): boolean => { const newErrors: FormErrors = {}; - - if (formData.actualPickQty === undefined || formData.actualPickQty < 0) { - newErrors.actualPickQty = t('Qty is required'); - } - - // ✅ FIXED: Check if actual pick qty exceeds remaining available qty - if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) { - newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty'); - } - - // ✅ FIXED: Check if actual pick qty exceeds required qty (use original required qty) - if (formData.actualPickQty && formData.actualPickQty > (selectedLot?.requiredQty || 0)) { - newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty'); + const req = selectedLot?.requiredQty || 0; + const ap = formData.actualPickQty || 0; + const miss = formData.missQty || 0; + const bad = formData.badItemQty || 0; + + if (ap < 0) newErrors.actualPickQty = t('Qty is required'); + if (ap > Math.min(remainingAvailableQty, req)) newErrors.actualPickQty = t('Qty is not allowed to be greater than required/available qty'); + if (miss < 0) newErrors.missQty = t('Invalid qty'); + if (bad < 0) newErrors.badItemQty = t('Invalid qty'); + if (ap + miss + bad > req) { + newErrors.actualPickQty = t('Total exceeds required qty'); + newErrors.missQty = t('Total exceeds required qty'); } - - // ✅ NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported) - const hasMissQty = formData.missQty && formData.missQty > 0; - const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0; - - if (!hasMissQty && !hasBadItemQty) { - newErrors.missQty = t('At least one issue must be reported'); - newErrors.badItemQty = t('At least one issue must be reported'); + if (ap === 0 && miss === 0 && bad === 0) { + newErrors.actualPickQty = t('Enter pick qty or issue qty'); + newErrors.missQty = t('Enter pick qty or issue qty'); } - setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -266,42 +263,42 @@ const PickExecutionForm: React.FC = ({
- handleInputChange('actualPickQty', parseFloat(e.target.value) || 0)} - error={!!errors.actualPickQty} - helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} - variant="outlined" - /> + handleInputChange('actualPickQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} + error={!!errors.actualPickQty} + helperText={errors.actualPickQty || `${t('Max')}: ${Math.min(remainingAvailableQty, selectedLot?.requiredQty || 0)}`} + variant="outlined" + /> - handleInputChange('missQty', parseFloat(e.target.value) || 0)} - error={!!errors.missQty} - // helperText={errors.missQty || t('Enter missing quantity (required if no bad items)')} - variant="outlined" - /> + handleInputChange('missQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} + error={!!errors.missQty} + variant="outlined" + //disabled={(formData.actualPickQty || 0) > 0} + /> - handleInputChange('badItemQty', parseFloat(e.target.value) || 0)} - error={!!errors.badItemQty} - // helperText={errors.badItemQty || t('Enter bad item quantity (required if no missing items)')} - variant="outlined" - /> + handleInputChange('badItemQty', e.target.value === '' ? undefined : Math.max(0, Number(e.target.value) || 0))} + error={!!errors.badItemQty} + variant="outlined" + //disabled={(formData.actualPickQty || 0) > 0} + /> {/* ✅ Show issue description and handler fields when bad items > 0 */} diff --git a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx index 1af86fc..b3a8912 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx @@ -586,12 +586,20 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); setQrScanError(false); setQrScanSuccess(false); setQrScanInput(''); - setIsManualScanning(false); - stopScan(); - resetScan(); + //setIsManualScanning(false); + //stopScan(); + //resetScan(); setProcessedQrCodes(new Set()); setLastProcessedQr(''); - + setQrModalOpen(false); + setPickExecutionFormOpen(false); + if(selectedLotForQr?.stockOutLineId){ + const stockOutLineUpdate = await updateStockOutLineStatus({ + id: selectedLotForQr.stockOutLineId, + status: 'checked', + qty: 0 + }); + } setLotConfirmationOpen(false); setExpectedLotData(null); setScannedLotData(null); @@ -709,9 +717,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); setQrScanSuccess(true); setQrScanError(false); setQrScanInput(''); // Clear input after successful processing - setIsManualScanning(false); - stopScan(); - resetScan(); + //setIsManualScanning(false); + // stopScan(); + // resetScan(); // ✅ Clear success state after a delay //setTimeout(() => { diff --git a/src/i18n/zh/jo.json b/src/i18n/zh/jo.json index 176ef94..bcd383b 100644 --- a/src/i18n/zh/jo.json +++ b/src/i18n/zh/jo.json @@ -33,7 +33,7 @@ "Pick Order Code": "提料單編號", "Target Date": "需求日期", "Lot Required Pick Qty": "批號需求數量", - "Job Order Match": "工單匹配", + "Job Order Match": "工單對料", "Lot No": "批號", "Submit Required Pick Qty": "提交需求數量", "All Pick Order Lots": "所有提料單批號", diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index 70c8909..4dc9389 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -278,7 +278,7 @@ "QR code verified.":"QR 碼驗證成功。", "Order Finished":"訂單完成", "Submitted Status":"提交狀態", - "Finished Good Record":"成單記錄", + "Finished Good Record":"已完成出倉記錄", "Delivery No.":"送貨單編號", "Total":"總數", "completed DO pick orders":"已完成送貨單提料單", @@ -290,7 +290,8 @@ "Back to List":"返回列表", "No completed DO pick orders found":"沒有已完成送貨單提料單", "Enter the number of cartons: ": "請輸入總箱數", - "Number of cartons": "箱數" + "Number of cartons": "箱數", + "Total exceeds required qty":"總數超出所需數量"