diff --git a/src/app/(main)/finishedGood/detail/page.tsx b/src/app/(main)/finishedGood/detail/page.tsx new file mode 100644 index 0000000..b3d616d --- /dev/null +++ b/src/app/(main)/finishedGood/detail/page.tsx @@ -0,0 +1,30 @@ +import { PreloadPickOrder } from "@/app/api/pickOrder"; +import { SearchParams } from "@/app/utils/fetchUtil"; +import FinishedGoodSearchWrapper from "@/components/FinishedGoodSearch"; +import { getServerI18n, I18nProvider } from "@/i18n"; +import { Stack, Typography } from "@mui/material"; +import { Metadata } from "next"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Finished Good Detail", +}; +type Props = {} & SearchParams; + +const PickOrder: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("pickOrder"); + + PreloadPickOrder(); + + return ( + <> + + }> + + + + + ); +}; + +export default PickOrder; diff --git a/src/app/(main)/finishedGood/page.tsx b/src/app/(main)/finishedGood/page.tsx new file mode 100644 index 0000000..ee74e4f --- /dev/null +++ b/src/app/(main)/finishedGood/page.tsx @@ -0,0 +1,29 @@ +import { PreloadPickOrder } from "@/app/api/pickOrder"; +import FinishedGoodSearch from "@/components/FinishedGoodSearch/"; +import { getServerI18n } from "@/i18n"; +import { I18nProvider } from "@/i18n"; +import { Stack, Typography } from "@mui/material"; +import { Metadata } from "next"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Pick Order", +}; + +const PickOrder: React.FC = async () => { + const { t } = await getServerI18n("pickOrder"); + + PreloadPickOrder(); + + return ( + <> + + }> + + + + + ); +}; + +export default PickOrder; diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index bbfc9b3..bc11775 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -287,11 +287,65 @@ export interface PickOrderLotDetailResponse { lotStatus: string; lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; } - - -export const fetchAllPickOrderDetails = cache(async () => { +interface ALLPickOrderLotDetailResponse { + // Pick Order Information + pickOrderId: number; + pickOrderCode: string; + pickOrderTargetDate: string; + pickOrderType: string; + pickOrderStatus: string; + pickOrderAssignTo: number; + groupName: string; + + // Pick Order Line Information + pickOrderLineId: number; + pickOrderLineRequiredQty: number; + pickOrderLineStatus: string; + + // Item Information + itemId: number; + itemCode: string; + itemName: string; + uomCode: string; + uomDesc: string; + + // Lot Information + lotId: number; + lotNo: string; + expiryDate: string; + location: string; + stockUnit: string; + availableQty: number; + requiredQty: number; + actualPickQty: number; + suggestedPickLotId: number; + lotStatus: string; + stockOutLineId?: number; + stockOutLineStatus?: string; + stockOutLineQty?: number; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + processingStatus: string; +} +export const fetchALLPickOrderLineLotDetails = cache(async (userId?: number) => { + const url = userId + ? `${BASE_API_URL}/pickOrder/all-lots-with-details?userId=${userId}` + : `${BASE_API_URL}/pickOrder/all-lots-with-details`; + + return serverFetchJson( + url, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); +}); +export const fetchAllPickOrderDetails = cache(async (userId?: number) => { + const url = userId + ? `${BASE_API_URL}/pickOrder/detail?userId=${userId}` + : `${BASE_API_URL}/pickOrder/detail`; + return serverFetchJson( - `${BASE_API_URL}/pickOrder/detail`, + url, { method: "GET", next: { tags: ["pickorder"] }, @@ -340,7 +394,17 @@ export const assignPickOrder = async (ids: number[]) => { // revalidateTag("po"); return pickOrder; }; - +export const consolidatePickOrder = async (ids: number[]) => { + const pickOrder = await serverFetchJson( + `${BASE_API_URL}/pickOrder/conso`, + { + method: "POST", + body: JSON.stringify({ ids: ids }), + headers: { "Content-Type": "application/json" }, + }, + ); + return pickOrder; +}; export const consolidatePickOrder_revert = async (ids: number[]) => { const pickOrder = await serverFetchJson( `${BASE_API_URL}/pickOrder/deconso`, diff --git a/src/components/FinishedGoodSearch/AssignAndRelease.tsx b/src/components/FinishedGoodSearch/AssignAndRelease.tsx new file mode 100644 index 0000000..8c84893 --- /dev/null +++ b/src/components/FinishedGoodSearch/AssignAndRelease.tsx @@ -0,0 +1,607 @@ +"use client"; +import { + Autocomplete, + Box, + Button, + CircularProgress, + FormControl, + Grid, + Modal, + TextField, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Checkbox, + TablePagination, +} from "@mui/material"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + newassignPickOrder, + AssignPickOrderInputs, +} from "@/app/api/pickOrder/actions"; +import { fetchNameList, NameList ,fetchNewNameList, NewNameList} from "@/app/api/user/actions"; +import { FormProvider, useForm } from "react-hook-form"; +import { isEmpty, sortBy, uniqBy, upperFirst, groupBy } from "lodash"; +import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import dayjs from "dayjs"; +import arraySupport from "dayjs/plugin/arraySupport"; +import SearchBox, { Criterion } from "../SearchBox"; +import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions"; + +dayjs.extend(arraySupport); + +interface Props { + filterArgs: Record; +} + +// 使用 fetchPickOrderItemsByPageClient 返回的数据结构 +interface ItemRow { + id: string; + pickOrderId: number; + pickOrderCode: string; + itemId: number; + itemCode: string; + itemName: string; + requiredQty: number; + currentStock: number; + unit: string; + targetDate: any; + status: string; + consoCode?: string; + assignTo?: number; + groupName?: string; +} + +// 分组后的数据结构 +interface GroupedItemRow { + pickOrderId: number; + pickOrderCode: string; + targetDate: any; + status: string; + consoCode?: string; + items: ItemRow[]; +} + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + width: { xs: "100%", sm: "100%", md: "100%" }, +}; + +const AssignAndRelease: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("pickOrder"); + const { setIsUploading } = useUploadContext(); + + // 修复:选择状态改为按 pick order ID 存储 + const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); + const [isLoadingItems, setIsLoadingItems] = useState(false); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }); + const [totalCountItems, setTotalCountItems] = useState(); + const [modalOpen, setModalOpen] = useState(false); + const [usernameList, setUsernameList] = useState([]); + const [searchQuery, setSearchQuery] = useState>({}); + const [originalItemData, setOriginalItemData] = useState([]); + + const formProps = useForm(); + const errors = formProps.formState.errors; + + // 将项目按 pick order 分组 + const groupedItems = useMemo(() => { + const grouped = groupBy(filteredItems, 'pickOrderId'); + return Object.entries(grouped).map(([pickOrderId, items]) => { + const firstItem = items[0]; + return { + pickOrderId: parseInt(pickOrderId), + pickOrderCode: firstItem.pickOrderCode, + targetDate: firstItem.targetDate, + status: firstItem.status, + consoCode: firstItem.consoCode, + items: items + } as GroupedItemRow; + }); + }, [filteredItems]); + + // 修复:处理 pick order 选择 + const handlePickOrderSelect = useCallback((pickOrderId: number, checked: boolean) => { + if (checked) { + setSelectedPickOrderIds(prev => [...prev, pickOrderId]); + } else { + setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); + } + }, []); + + // 修复:检查 pick order 是否被选中 + const isPickOrderSelected = useCallback((pickOrderId: number) => { + return selectedPickOrderIds.includes(pickOrderId); + }, [selectedPickOrderIds]); + + // 使用 fetchPickOrderItemsByPageClient 获取数据 + const fetchNewPageItems = useCallback( + async (pagingController: Record, filterArgs: Record) => { + console.log("=== fetchNewPageItems called ==="); + console.log("pagingController:", pagingController); + console.log("filterArgs:", filterArgs); + + setIsLoadingItems(true); + try { + const params = { + ...pagingController, + ...filterArgs, + // 新增:排除状态为 "assigned" 的提料单 + //status: "pending,released,completed,cancelled" // 或者使用其他方式过滤 + }; + console.log("Final params:", params); + + const res = await fetchPickOrderItemsByPageClient(params); + console.log("API Response:", res); + + if (res && res.records) { + console.log("Records received:", res.records.length); + console.log("First record:", res.records[0]); + + // 新增:在前端也过滤掉 "assigned" 状态的项目 + const filteredRecords = res.records.filter((item: any) => item.status !== "assigned"); + + const itemRows: ItemRow[] = filteredRecords.map((item: any) => ({ + id: item.id, + pickOrderId: item.pickOrderId, + pickOrderCode: item.pickOrderCode, + itemId: item.itemId, + itemCode: item.itemCode, + itemName: item.itemName, + requiredQty: item.requiredQty, + currentStock: item.currentStock ?? 0, + unit: item.unit, + targetDate: item.targetDate, + status: item.status, + consoCode: item.consoCode, + assignTo: item.assignTo, + groupName: item.groupName, + })); + + setOriginalItemData(itemRows); + setFilteredItems(itemRows); + setTotalCountItems(filteredRecords.length); // 使用过滤后的数量 + } else { + console.log("No records in response"); + setFilteredItems([]); + setTotalCountItems(0); + } + } catch (error) { + console.error("Error fetching items:", error); + setFilteredItems([]); + setTotalCountItems(0); + } finally { + setIsLoadingItems(false); + } + }, + [], + ); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Pick Order Code"), + paramName: "pickOrderCode", + type: "text", + }, + { + label: t("Item Code"), + paramName: "itemCode", + type: "text" + }, + { + label: t("Group Code"), + paramName: "groupName", + type: "text", + }, + { + label: t("Item Name"), + paramName: "itemName", + type: "text", + }, + { + label: t("Target Date From"), + label2: t("Target Date To"), + paramName: "targetDate", + type: "dateRange", + }, + + { + label: t("Pick Order Status"), + paramName: "status", + type: "autocomplete", + options: sortBy( + uniqBy( + originalItemData.map((item) => ({ + value: item.status, + label: t(upperFirst(item.status)), + })), + "value", + ), + "label", + ), + }, + ], + [originalItemData, t], + ); + + const handleSearch = useCallback((query: Record) => { + setSearchQuery({ ...query }); + console.log("Search query:", query); + + const filtered = originalItemData.filter((item) => { + const itemTargetDateStr = arrayToDayjs(item.targetDate); + + const itemCodeMatch = !query.itemCode || + item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); + + const itemNameMatch = !query.itemName || + item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); + + const pickOrderCodeMatch = !query.pickOrderCode || + item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); + const groupNameMatch = !query.groupName || + item.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); + // 日期范围搜索 + let dateMatch = true; + if (query.targetDate || query.targetDateTo) { + try { + if (query.targetDate && !query.targetDateTo) { + const fromDate = dayjs(query.targetDate); + dateMatch = itemTargetDateStr.isSame(fromDate, 'day') || + itemTargetDateStr.isAfter(fromDate, 'day'); + } else if (!query.targetDate && query.targetDateTo) { + const toDate = dayjs(query.targetDateTo); + dateMatch = itemTargetDateStr.isSame(toDate, 'day') || + itemTargetDateStr.isBefore(toDate, 'day'); + } else if (query.targetDate && query.targetDateTo) { + const fromDate = dayjs(query.targetDate); + const toDate = dayjs(query.targetDateTo); + dateMatch = (itemTargetDateStr.isSame(fromDate, 'day') || + itemTargetDateStr.isAfter(fromDate, 'day')) && + (itemTargetDateStr.isSame(toDate, 'day') || + itemTargetDateStr.isBefore(toDate, 'day')); + } + } catch (error) { + console.error("Date parsing error:", error); + dateMatch = true; + } + } + + const statusMatch = !query.status || + query.status.toLowerCase() === "all" || + item.status?.toLowerCase().includes((query.status || "").toLowerCase()); + + return itemCodeMatch && itemNameMatch && groupNameMatch && pickOrderCodeMatch && dateMatch && statusMatch; + }); + + console.log("Filtered items count:", filtered.length); + setFilteredItems(filtered); + }, [originalItemData]); + + const handleReset = useCallback(() => { + setSearchQuery({}); + setFilteredItems(originalItemData); + setTimeout(() => { + setSearchQuery({}); + }, 0); + }, [originalItemData]); + + // 修复:处理分页变化 + const handlePageChange = useCallback((event: unknown, newPage: number) => { + const newPagingController = { + ...pagingController, + pageNum: newPage + 1, // API 使用 1-based 分页 + }; + setPagingController(newPagingController); + }, [pagingController]); + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + const newPagingController = { + pageNum: 1, // 重置到第一页 + pageSize: newPageSize, + }; + setPagingController(newPagingController); + }, []); + + const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => { + if (selectedPickOrderIds.length === 0) return; + + setIsUploading(true); + try { + // 修复:直接使用选中的 pick order IDs + const assignRes = await newassignPickOrder({ + pickOrderIds: selectedPickOrderIds, + assignTo: data.assignTo, + }); + + if (assignRes && assignRes.code === "SUCCESS") { + console.log("Assign successful:", assignRes); + setModalOpen(false); + setSelectedPickOrderIds([]); // 清空选择 + fetchNewPageItems(pagingController, filterArgs); + } else { + console.error("Assign failed:", assignRes); + } + } catch (error) { + console.error("Error in assign:", error); + } finally { + setIsUploading(false); + } + }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); + + const openAssignModal = useCallback(() => { + setModalOpen(true); + formProps.reset(); + }, [formProps]); + + // 组件挂载时加载数据 + useEffect(() => { + console.log("=== Component mounted ==="); + fetchNewPageItems(pagingController, filterArgs || {}); + }, []); // 只在组件挂载时执行一次 + + // 当 pagingController 或 filterArgs 变化时重新调用 API + useEffect(() => { + console.log("=== Dependencies changed ==="); + if (pagingController && (filterArgs || {})) { + fetchNewPageItems(pagingController, filterArgs || {}); + } + }, [pagingController, filterArgs, fetchNewPageItems]); + + useEffect(() => { + const loadUsernameList = async () => { + try { + const res = await fetchNewNameList(); + if (res) { + setUsernameList(res); + } + } catch (error) { + console.error("Error loading username list:", error); + } + }; + loadUsernameList(); + }, []); + + // 自定义分组表格组件 + const CustomGroupedTable = () => { + return ( + <> + + + + + {t("Selected")} + {t("Pick Order Code")} + {t("Group Code")} + {t("Item Code")} + {t("Item Name")} + {t("Order Quantity")} + {t("Current Stock")} + {t("Stock Unit")} + {t("Target Date")} + {t("Pick Order Status")} + + + + {groupedItems.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + groupedItems.map((group) => ( + group.items.map((item, index) => ( + + {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} + + {index === 0 ? ( + handlePickOrderSelect(group.pickOrderId, e.target.checked)} + disabled={!isEmpty(item.consoCode)} + /> + ) : null} + + {/* Pick Order Code - 只在第一个项目显示 */} + + {index === 0 ? item.pickOrderCode : null} + + {/* Group Name */} + + {index === 0 ? (item.groupName || "No Group") : null} + + {/* Item Code */} + {item.itemCode} + {/* Item Name */} + {item.itemName} + + {/* Order Quantity */} + {item.requiredQty} + + {/* Current Stock */} + + 0 ? "success.main" : "error.main"} + sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }} + > + {item.currentStock.toLocaleString()} + + + + {/* Unit */} + {item.unit} + + {/* Target Date - 只在第一个项目显示 */} + + {index === 0 ? ( + arrayToDayjs(item.targetDate) + .add(-1, "month") + .format(OUTPUT_DATE_FORMAT) + ) : null} + + + + + {/* Pick Order Status - 只在第一个项目显示 */} + + {index === 0 ? upperFirst(item.status) : null} + + + )) + )) + )} + +
+
+ + {/* 修复:添加分页组件 */} + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); + }; + + return ( + <> + + + + {isLoadingItems ? ( + + ) : ( + + )} + + + + + + + + + {modalOpen ? ( + setModalOpen(false)} + aria-labelledby="modal-modal-title" + aria-describedby="modal-modal-description" + > + + + + + {t("Assign Pick Orders")} + + + + + {t("Selected Pick Orders")}: {selectedPickOrderIds.length} + + + + +
+ + + + { + // 修改:显示更详细的用户信息 + const title = option.title ? ` (${option.title})` : ''; + const department = option.department ? ` - ${option.department}` : ''; + return `${option.name}${title}${department}`; + }} + renderOption={(props, option) => ( + + + {option.name} + {option.title && ` (${option.title})`} + {option.department && ` - ${option.department}`} + + + )} + onChange={(_, value) => { + formProps.setValue("assignTo", value?.id || 0); + }} + renderInput={(params) => ( + + )} + /> + + + + + {t("This action will assign the selected pick orders to picker.")} + + + + + + + + + +
+
+
+
+
+
+ ) : undefined} + + ); +}; + +export default AssignAndRelease; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/CombinedLotTable.tsx b/src/components/FinishedGoodSearch/CombinedLotTable.tsx new file mode 100644 index 0000000..4342043 --- /dev/null +++ b/src/components/FinishedGoodSearch/CombinedLotTable.tsx @@ -0,0 +1,228 @@ +"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("Stock Unit")} + {t("Available Qty")} + {t("Required Qty")} + {t("Actual Pick Qty")} + {t("Pick Qty")} + {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.stockUnit} + {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', + } + }} + /> + + + + + + + + + ); + }) + )} + +
+
+ + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); +}; + +export default CombinedLotTable; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/ConsolidatePickOrderItemSum.tsx b/src/components/FinishedGoodSearch/ConsolidatePickOrderItemSum.tsx new file mode 100644 index 0000000..6f0c5ab --- /dev/null +++ b/src/components/FinishedGoodSearch/ConsolidatePickOrderItemSum.tsx @@ -0,0 +1,91 @@ +"use client"; +import dayjs from "dayjs"; +import arraySupport from "dayjs/plugin/arraySupport"; +import StyledDataGrid from "../StyledDataGrid"; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { GridColDef } from "@mui/x-data-grid"; +import { CircularProgress, Grid, Typography } from "@mui/material"; +import { ByItemsSummary } from "@/app/api/pickOrder"; +import { useTranslation } from "react-i18next"; + +dayjs.extend(arraySupport); + +interface Props { + rows: ByItemsSummary[] | undefined; + setRows: Dispatch>; +} + +const ConsolidatePickOrderItemSum: React.FC = ({ rows, setRows }) => { + console.log(rows); + const { t } = useTranslation("pickOrder"); + + const columns = useMemo( + () => [ + { + field: "name", + headerName: "name", + flex: 1, + renderCell: (params) => { + console.log(params.row.name); + return params.row.name; + }, + }, + { + field: "requiredQty", + headerName: "requiredQty", + flex: 1, + renderCell: (params) => { + console.log(params.row.requiredQty); + const requiredQty = params.row.requiredQty ?? 0; + return `${requiredQty} ${params.row.uomDesc}`; + }, + }, + { + field: "availableQty", + headerName: "availableQty", + flex: 1, + renderCell: (params) => { + console.log(params.row.availableQty); + const availableQty = params.row.availableQty ?? 0; + return `${availableQty} ${params.row.uomDesc}`; + }, + }, + ], + [], + ); + return ( + + + + {t("Items Included")} + + + + {!rows ? ( + + ) : ( + + )} + + + ); +}; + +export default ConsolidatePickOrderItemSum; diff --git a/src/components/FinishedGoodSearch/ConsolidatePickOrderSum.tsx b/src/components/FinishedGoodSearch/ConsolidatePickOrderSum.tsx new file mode 100644 index 0000000..74771f7 --- /dev/null +++ b/src/components/FinishedGoodSearch/ConsolidatePickOrderSum.tsx @@ -0,0 +1,116 @@ +"use client"; +import dayjs from "dayjs"; +import arraySupport from "dayjs/plugin/arraySupport"; +import StyledDataGrid from "../StyledDataGrid"; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; +import { GridColDef, GridInputRowSelectionModel } from "@mui/x-data-grid"; +import { Box, CircularProgress, Grid, Typography } from "@mui/material"; +import { PickOrderResult } from "@/app/api/pickOrder"; +import { useTranslation } from "react-i18next"; + +dayjs.extend(arraySupport); + +interface Props { + consoCode: string; + rows: Omit[] | undefined; + setRows: Dispatch< + SetStateAction[] | undefined> + >; + revertIds: GridInputRowSelectionModel; + setRevertIds: Dispatch>; +} + +const ConsolidatePickOrderSum: React.FC = ({ + consoCode, + rows, + setRows, + revertIds, + setRevertIds, +}) => { + const { t } = useTranslation("pickOrder"); + const columns = useMemo( + () => [ + { + field: "code", + headerName: "code", + flex: 0.6, + }, + + { + field: "pickOrderLines", + headerName: "items", + flex: 1, + renderCell: (params) => { + console.log(params); + const pickOrderLine = params.row.pickOrderLines as any[]; + return ( + + {pickOrderLine.map((item, index) => ( + {`${item.itemName} x ${item.requiredQty} ${item.uomDesc}`} // Render each name in a span + ))} + + ); + }, + }, + ], + [], + ); + + return ( + + + + {t("Pick Order Included")} + + + + {!rows ? ( + + ) : ( + { + setRevertIds(newRowSelectionModel); + }} + getRowHeight={(params) => { + return 100; + }} + rows={rows} + columns={columns} + /> + )} + + + ); +}; + +export default ConsolidatePickOrderSum; diff --git a/src/components/FinishedGoodSearch/ConsolidatedPickOrders.tsx b/src/components/FinishedGoodSearch/ConsolidatedPickOrders.tsx new file mode 100644 index 0000000..b2c2c29 --- /dev/null +++ b/src/components/FinishedGoodSearch/ConsolidatedPickOrders.tsx @@ -0,0 +1,370 @@ +import { + Autocomplete, + Box, + Button, + CircularProgress, + FormControl, + Grid, + Modal, + ModalProps, + TextField, + Typography, +} from "@mui/material"; +import { GridToolbarContainer } from "@mui/x-data-grid"; +import { + FooterPropsOverrides, + GridColDef, + GridRowSelectionModel, + useGridApiRef, +} from "@mui/x-data-grid"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import StyledDataGrid from "../StyledDataGrid"; +import SearchResults, { + Column, + defaultPagingController, +} from "../SearchResults/SearchResults"; +import { + ByItemsSummary, + ConsoPickOrderResult, + PickOrderLine, + PickOrderResult, +} from "@/app/api/pickOrder"; +import { useRouter, useSearchParams } from "next/navigation"; +import ConsolidatePickOrderItemSum from "./ConsolidatePickOrderItemSum"; +import ConsolidatePickOrderSum from "./ConsolidatePickOrderSum"; +import { GridInputRowSelectionModel } from "@mui/x-data-grid"; +import { + fetchConsoDetail, + fetchConsoPickOrderClient, + releasePickOrder, + ReleasePickOrderInputs, +} from "@/app/api/pickOrder/actions"; +import { EditNote } from "@mui/icons-material"; +import { fetchNameList, NameList } from "@/app/api/user/actions"; +import { useField } from "@mui/x-date-pickers/internals"; +import { + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { pickOrderStatusMap } from "@/app/utils/formatUtil"; + +interface Props { + filterArgs: Record; +} + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + // width: 1500, + width: { xs: "100%", sm: "100%", md: "100%" }, +}; +interface DisableButton { + releaseBtn: boolean; + removeBtn: boolean; +} + +const ConsolidatedPickOrders: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("pickOrder"); + const router = useRouter(); + const apiRef = useGridApiRef(); + const [filteredPickOrders, setFilteredPickOrders] = useState( + [] as ConsoPickOrderResult[], + ); + const [isLoading, setIsLoading] = useState(false); + const [modalOpen, setModalOpen] = useState(false); //change back to false + const [consoCode, setConsoCode] = useState(); ///change back to undefined + const [revertIds, setRevertIds] = useState([]); + const [totalCount, setTotalCount] = useState(); + const [usernameList, setUsernameList] = useState([]); + + const [byPickOrderRows, setByPickOrderRows] = useState< + Omit[] | undefined + >(undefined); + const [byItemsRows, setByItemsRows] = useState( + undefined, + ); + const [disableRelease, setDisableRelease] = useState(true); + + const formProps = useForm(); + const errors = formProps.formState.errors; + + const openDetailModal = useCallback((consoCode: string) => { + setConsoCode(consoCode); + setModalOpen(true); + }, []); + + const closeDetailModal = useCallback(() => { + setModalOpen(false); + setConsoCode(undefined); + }, []); + + const onDetailClick = useCallback( + (pickOrder: any) => { + console.log(pickOrder); + const status = pickOrder.status; + if (pickOrderStatusMap[status] >= 3) { + router.push(`/pickOrder/detail?consoCode=${pickOrder.consoCode}`); + } else { + openDetailModal(pickOrder.consoCode); + } + }, + [router, openDetailModal], + ); + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("Detail"), + onClick: onDetailClick, + buttonIcon: , + }, + { + name: "consoCode", + label: t("consoCode"), + }, + { + name: "status", + label: t("status"), + }, + ], + [onDetailClick, t], + ); + const [pagingController, setPagingController] = useState( + defaultPagingController, + ); + + // pass conso code back to assign + // pass user back to assign + const fetchNewPageConsoPickOrder = useCallback( + async ( + pagingController: Record, + filterArgs: Record, + ) => { + setIsLoading(true); + const params = { + ...pagingController, + ...filterArgs, + }; + const res = await fetchConsoPickOrderClient(params); + if (res) { + console.log(res); + setFilteredPickOrders(res.records); + setTotalCount(res.total); + } + setIsLoading(false); + }, + [], + ); + + useEffect(() => { + fetchNewPageConsoPickOrder(pagingController, filterArgs); + }, [fetchNewPageConsoPickOrder, pagingController, filterArgs]); + + const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => { + let isReleasable = true; + for (const item of itemList) { + isReleasable = item.requiredQty >= item.availableQty; + if (!isReleasable) return isReleasable; + } + return isReleasable; + }, []); + + const fetchConso = useCallback( + async (consoCode: string) => { + const res = await fetchConsoDetail(consoCode); + const nameListRes = await fetchNameList(); + if (res) { + console.log(res); + setByPickOrderRows(res.pickOrders); + // for testing + // for (const item of res.items) { + // item.availableQty = 1000; + // } + setByItemsRows(res.items); + setDisableRelease(isReleasable(res.items)); + } else { + console.log("error"); + console.log(res); + } + if (nameListRes) { + console.log(nameListRes); + setUsernameList(nameListRes); + } + }, + [isReleasable], + ); + + const closeHandler = useCallback>( + (...args) => { + closeDetailModal(); + // reset(); + }, + [closeDetailModal], + ); + + const onChange = useCallback( + (event: React.SyntheticEvent, newValue: NameList) => { + console.log(newValue); + formProps.setValue("assignTo", newValue.id); + }, + [formProps], + ); + + const onSubmit = useCallback>( + async (data, event) => { + console.log(data); + try { + const res = await releasePickOrder(data); + console.log(res); + if (res.consoCode.length > 0) { + console.log(res); + router.push(`/pickOrder/detail?consoCode=${res.consoCode}`); + } else { + console.log(res); + } + } catch (error) { + console.log(error); + } + }, + [router], + ); + const onSubmitError = useCallback>( + (errors) => {}, + [], + ); + + const handleConsolidate_revert = useCallback(() => { + console.log(revertIds); + }, [revertIds]); + + useEffect(() => { + if (consoCode) { + fetchConso(consoCode); + formProps.setValue("consoCode", consoCode); + } + }, [consoCode, fetchConso, formProps]); + + return ( + <> + + + {isLoading ? ( + + ) : ( + + items={filteredPickOrders} + columns={columns} + pagingController={pagingController} + setPagingController={setPagingController} + totalCount={totalCount} + /> + )} + + + {consoCode != undefined ? ( + + + + + + + {consoCode} + + + + + option.name} + options={usernameList} + onChange={onChange} + renderInput={(params) => } + /> + + + + + + + + + + + + + + + + + + + + + + + ) : undefined} + + ); +}; + +export default ConsolidatedPickOrders; diff --git a/src/components/FinishedGoodSearch/CreateForm.tsx b/src/components/FinishedGoodSearch/CreateForm.tsx new file mode 100644 index 0000000..45e7514 --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/CreatePickOrderModal.tsx b/src/components/FinishedGoodSearch/CreatePickOrderModal.tsx new file mode 100644 index 0000000..4432e6f --- /dev/null +++ b/src/components/FinishedGoodSearch/CreatePickOrderModal.tsx @@ -0,0 +1,98 @@ +import { createPickOrder, SavePickOrderRequest } from "@/app/api/pickOrder/actions"; +import { Box, Button, Modal, ModalProps, Stack } from "@mui/material"; +import dayjs from "dayjs"; +import arraySupport from "dayjs/plugin/arraySupport"; +import { useCallback } from "react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import CreateForm from "./CreateForm"; +import { ItemCombo } from "@/app/api/settings/item/actions"; +import { Check } from "@mui/icons-material"; +dayjs.extend(arraySupport); + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + overflow: "scroll", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + display: "block", + width: { xs: "100%", sm: "100%", md: "100%" }, +}; + +interface Props extends Omit { + items: ItemCombo[] +} + +const CreatePickOrderModal: React.FC = ({ + open, + onClose, + items + }) => { + const { t } = useTranslation("pickOrder"); + const formProps = useForm(); + const errors = formProps.formState.errors; + const closeHandler = useCallback>( + (...args) => { + onClose?.(...args); + // reset(); + }, + [onClose] + ); + const onSubmit = useCallback>( + async (data, event) => { + console.log(data) + try { + const res = await createPickOrder(data) + if (res.id) { + closeHandler({}, "backdropClick"); + } + } catch (error) { + console.log(error) + throw error + } + // formProps.reset() + }, + [closeHandler] + ); + return ( + <> + + + + + + + + + + + + + ); +}; +export default CreatePickOrderModal; diff --git a/src/components/FinishedGoodSearch/CreatedItemsTable.tsx b/src/components/FinishedGoodSearch/CreatedItemsTable.tsx new file mode 100644 index 0000000..e60bf2f --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/EscalationComponent.tsx b/src/components/FinishedGoodSearch/EscalationComponent.tsx new file mode 100644 index 0000000..53761a8 --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/FinishedGood.tsx b/src/components/FinishedGoodSearch/FinishedGood.tsx new file mode 100644 index 0000000..5fb16f9 --- /dev/null +++ b/src/components/FinishedGoodSearch/FinishedGood.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 PickOrders: 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 PickOrders; diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx new file mode 100644 index 0000000..882daff --- /dev/null +++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx @@ -0,0 +1,292 @@ +"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 PickOrders from "./FinishedGood"; +import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; +import PickExecution from "./GoodPickExecution"; +import CreatePickOrderModal from "./CreatePickOrderModal"; +import NewCreateItem from "./newcreatitem"; +import AssignAndRelease from "./AssignAndRelease"; +import AssignTo from "./assignTo"; +import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; +import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; +import Jobcreatitem from "./Jobcreatitem"; + +interface Props { + pickOrders: PickOrderResult[]; +} + +type SearchQuery = Partial< + Omit +>; + +type SearchParamNames = keyof SearchQuery; + +const PickOrderSearch: React.FC = ({ pickOrders }) => { + const { t } = useTranslation("pickOrder"); + + const [isOpenCreateModal, setIsOpenCreateModal] = useState(false) + const [items, setItems] = useState([]) + const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); + const [filterArgs, setFilterArgs] = useState>({}); + const [searchQuery, setSearchQuery] = useState>({}); + const [tabIndex, setTabIndex] = useState(0); + const [totalCount, setTotalCount] = useState(); + + 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]); + + 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("Pick Order")} + + + {/* + + + {isOpenCreateModal && + + } + + */} + + + + + {/* Tabs section */} + + + + + + + + + {/* Content section - NO overflow: 'auto' here */} + + {tabIndex === 2 && } + {tabIndex === 0 && } + {tabIndex === 1 && } + + + ); +}; + +export default PickOrderSearch; diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearchWrapper.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearchWrapper.tsx new file mode 100644 index 0000000..1df245d --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/GoodPickExecution.tsx b/src/components/FinishedGoodSearch/GoodPickExecution.tsx new file mode 100644 index 0000000..ff70b6b --- /dev/null +++ b/src/components/FinishedGoodSearch/GoodPickExecution.tsx @@ -0,0 +1,475 @@ +"use client"; + +import { + Box, + Button, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useRouter } from "next/navigation"; +import { + fetchALLPickOrderLineLotDetails, + updateStockOutLineStatus, + createStockOutLine, +} 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 CombinedLotTable from './CombinedLotTable'; +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; // ✅ Import the custom session type + +interface Props { + filterArgs: Record; +} + +const PickExecution: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("pickOrder"); + const router = useRouter(); + const { data: session } = useSession() as { data: SessionWithTokens | null }; // ✅ Cast to custom type + + // ✅ Get current user ID from session with proper typing + const currentUserId = session?.id ? parseInt(session.id) : undefined; + + // ✅ Combined approach states + const [combinedLotData, setCombinedLotData] = useState([]); + const [combinedDataLoading, setCombinedDataLoading] = useState(false); + const [originalCombinedData, setOriginalCombinedData] = useState([]); + + // ✅ QR Scanner context + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); + + // ✅ QR scan input states + const [qrScanInput, setQrScanInput] = useState(''); + const [qrScanError, setQrScanError] = useState(false); + const [qrScanSuccess, setQrScanSuccess] = useState(false); + + // ✅ Pick quantity states + const [pickQtyData, setPickQtyData] = useState>({}); + + // ✅ Search states + const [searchQuery, setSearchQuery] = useState>({}); + + // ✅ Add pagination state + const [paginationController, setPaginationController] = useState({ + pageNum: 0, + pageSize: 10, + }); + + // ✅ Keep only essential states + const [usernameList, setUsernameList] = useState([]); + + const formProps = useForm(); + const errors = formProps.formState.errors; + + // ✅ Start QR scanning on component mount + useEffect(() => { + startScan(); + return () => { + stopScan(); + resetScan(); + }; + }, [startScan, stopScan, resetScan]); + + // ✅ Fetch all combined lot data - Updated to use current user ID + const fetchAllCombinedLotData = useCallback(async (userId?: number) => { + setCombinedDataLoading(true); + try { + // ✅ Use passed userId or current user ID + const userIdToUse = userId || currentUserId; + const allLotDetails = await fetchALLPickOrderLineLotDetails(userIdToUse); + console.log("All combined lot details:", allLotDetails); + setCombinedLotData(allLotDetails); + setOriginalCombinedData(allLotDetails); // Store original for filtering + } catch (error) { + console.error("Error fetching combined lot data:", error); + setCombinedLotData([]); + setOriginalCombinedData([]); + } finally { + setCombinedDataLoading(false); + } + }, [currentUserId]); // ✅ Add currentUserId as dependency + + + // ✅ Load data on component mount - Now uses current user ID + useEffect(() => { + fetchAllCombinedLotData(); // This will now use currentUserId + }, [fetchAllCombinedLotData]); + + // ✅ Handle QR code submission for matched lot - FIXED: Handle multiple pick order lines + const handleQrCodeSubmit = useCallback(async (lotNo: string) => { + console.log(`✅ Processing QR Code for lot: ${lotNo}`); + console.log(`🔍 Available lots:`, combinedLotData.map(lot => lot.lotNo)); + + // Find ALL matching lots (same lot number can be used by multiple pick order lines) + const matchingLots = combinedLotData.filter(lot => + lot.lotNo === lotNo || + lot.lotNo?.toLowerCase() === lotNo.toLowerCase() + ); + + if (matchingLots.length === 0) { + console.error(`❌ Lot not found: ${lotNo}`); + console.error(`❌ Available lots:`, combinedLotData.map(lot => lot.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; + + // ✅ Process each matching lot (each pick order line that uses this lot) + for (const matchingLot of matchingLots) { + console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`); + + // ✅ Check if stockOutLineId is null before creating + if (matchingLot.stockOutLineId) { + console.log(`✅ Stock out line already exists for line ${matchingLot.pickOrderLineId}`); + existsCount++; + } else { + // Create stock out line for this specific pick order line + const stockOutLineData: CreateStockOutLine = { + consoCode: matchingLot.pickOrderCode, // Use pick order code as conso code + 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); + + // ✅ Handle different response codes + 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(`❌ Unexpected response for line ${matchingLot.pickOrderLineId}:`, result); + errorCount++; + } + } + + // Auto-set pick quantity to required quantity for this specific line + const lotKey = `${matchingLot.pickOrderLineId}-${matchingLot.lotId}`; + setPickQtyData(prev => ({ + ...prev, + [lotKey]: matchingLot.requiredQty + })); + } + + // ✅ Set success state if at least one operation succeeded + if (successCount > 0 || existsCount > 0) { + setQrScanSuccess(true); + setQrScanError(false); + console.log(`✅ QR Code processing completed: ${successCount} created, ${existsCount} already existed, ${errorCount} errors`); + } else { + setQrScanError(true); + setQrScanSuccess(false); + console.error(`❌ All operations failed for lot ${lotNo}`); + return; + } + + // Refresh data + await fetchAllCombinedLotData(); + + // Clear input after successful match + setQrScanInput(''); + + console.log("Stock out line process completed successfully!"); + } catch (error) { + console.error("Error creating stock out line:", error); + setQrScanError(true); + setQrScanSuccess(false); + } + }, [combinedLotData, fetchAllCombinedLotData]); + + // ✅ Process scanned QR codes automatically - FIXED: Only process when data is loaded + useEffect(() => { + if (qrValues.length > 0 && combinedLotData.length > 0) { + const latestQr = qrValues[qrValues.length - 1]; + const qrContent = latestQr.replace(/[{}]/g, ''); + setQrScanInput(qrContent); + + // Auto-process the QR code + handleQrCodeSubmit(qrContent); + } + }, [qrValues, combinedLotData, handleQrCodeSubmit]); + + // ✅ Handle manual input submission + const handleManualInputSubmit = useCallback(() => { + if (qrScanInput.trim() !== '') { + handleQrCodeSubmit(qrScanInput.trim()); + } + }, [qrScanInput, handleQrCodeSubmit]); + + // ✅ Handle pick quantity change - FIXED: Better input handling + const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => { + // ✅ Handle empty string as 0 + if (value === '' || value === null || value === undefined) { + setPickQtyData(prev => ({ + ...prev, + [lotKey]: 0 + })); + return; + } + + // ✅ Convert to number properly + const numericValue = typeof value === 'string' ? parseFloat(value) : value; + + // ✅ Handle NaN case + if (isNaN(numericValue)) { + setPickQtyData(prev => ({ + ...prev, + [lotKey]: 0 + })); + return; + } + + setPickQtyData(prev => ({ + ...prev, + [lotKey]: numericValue + })); + }, []); + + // ✅ 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 + const currentActualPickQty = lot.actualPickQty || 0; + const cumulativeQty = currentActualPickQty + newQty; + + // ✅ FIXED: Check cumulative quantity against required quantity + 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 // ✅ Submit the cumulative quantity + }); + + // Update inventory + if (newQty > 0) { + await updateInventoryLotLineQuantities({ + inventoryLotLineId: lot.lotId, + qty: newQty, // ✅ Only update inventory with the new quantity + status: 'available', + operation: 'pick' + }); + } + + // Refresh data + await fetchAllCombinedLotData(); + console.log("Pick quantity submitted successfully!"); + } catch (error) { + console.error("Error submitting pick quantity:", error); + } + }, [pickQtyData, fetchAllCombinedLotData]); + + // ✅ 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 + }); + + // Refresh data + await fetchAllCombinedLotData(); + console.log("Lot rejected successfully!"); + } catch (error) { + console.error("Error rejecting lot:", error); + } + }, [fetchAllCombinedLotData]); + + // ✅ 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", + }, + ]; + + // ✅ Search handler + 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]); + + // ✅ Reset handler + const handleReset = useCallback(() => { + setSearchQuery({}); + if (originalCombinedData) { + setCombinedLotData(originalCombinedData); + } + }, [originalCombinedData]); + + // ✅ Pagination handlers + 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, + }); + }, []); + + return ( + + + {/* Search Box */} + + + + + {/* Combined Lot Table with QR Scan Input */} + + + + {t("All Pick Order Lots")} + + + setQrScanInput(e.target.value)} + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleManualInputSubmit(); + } + }} + error={qrScanError} + color={qrScanSuccess ? 'success' : undefined} + helperText={ + qrScanError + ? t("Lot number not found") + : qrScanSuccess + ? t("Lot processed successfully") + : t("Enter lot number or scan QR code") + } + placeholder={t("Enter lot number...")} + sx={{ minWidth: '250px' }} + InputProps={{ + startAdornment: , + }} + /> + + + + + + + + + ); +}; + +export default PickExecution; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/ItemSelect.tsx b/src/components/FinishedGoodSearch/ItemSelect.tsx new file mode 100644 index 0000000..f611e0e --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/Jobcreatitem.tsx b/src/components/FinishedGoodSearch/Jobcreatitem.tsx new file mode 100644 index 0000000..9231102 --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/LotTable.tsx b/src/components/FinishedGoodSearch/LotTable.tsx new file mode 100644 index 0000000..7f4beda --- /dev/null +++ b/src/components/FinishedGoodSearch/LotTable.tsx @@ -0,0 +1,737 @@ +"use client"; + +import { + Box, + Button, + Checkbox, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + TablePagination, + Modal, +} from "@mui/material"; +import { useCallback, useMemo, useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import QrCodeIcon from '@mui/icons-material/QrCode'; +import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; +import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; +import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions"; +import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; +interface LotPickData { + id: number; + lotId: number; + lotNo: string; + expiryDate: string; + location: string; + stockUnit: string; + availableQty: number; + requiredQty: number; + actualPickQty: number; + lotStatus: string; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + stockOutLineId?: number; + stockOutLineStatus?: string; + stockOutLineQty?: number; +} + +interface PickQtyData { + [lineId: number]: { + [lotId: number]: number; + }; +} + +interface LotTableProps { + lotData: LotPickData[]; + selectedRowId: number | null; + selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; + pickQtyData: PickQtyData; + selectedLotRowId: string | null; + selectedLotId: number | null; + onLotSelection: (uniqueLotId: string, lotId: number) => void; + onPickQtyChange: (lineId: number, lotId: number, value: number) => void; + onSubmitPickQty: (lineId: number, lotId: number) => void; + onCreateStockOutLine: (inventoryLotLineId: number) => void; + onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void; + onLotSelectForInput: (lot: LotPickData) => void; + showInputBody: boolean; + setShowInputBody: (show: boolean) => void; + selectedLotForInput: LotPickData | null; + generateInputBody: () => any; + onDataRefresh: () => Promise; +} + +// ✅ QR Code Modal Component +const QrCodeModal: React.FC<{ + open: boolean; + onClose: () => void; + lot: LotPickData | null; + onQrCodeSubmit: (lotNo: string) => void; +}> = ({ open, onClose, lot, onQrCodeSubmit }) => { + const { t } = useTranslation("pickOrder"); + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); + const [manualInput, setManualInput] = useState(''); + + // ✅ Add state to track manual input submission + const [manualInputSubmitted, setManualInputSubmitted] = useState(false); + const [manualInputError, setManualInputError] = useState(false); + + // ✅ Process scanned QR codes + useEffect(() => { + if (qrValues.length > 0 && lot) { + const latestQr = qrValues[qrValues.length - 1]; + const qrContent = latestQr.replace(/[{}]/g, ''); + + if (qrContent === lot.lotNo) { + onQrCodeSubmit(lot.lotNo); + onClose(); + resetScan(); + } else { + // ✅ Set error state for helper text + setManualInputError(true); + setManualInputSubmitted(true); + } + } + }, [qrValues, lot, onQrCodeSubmit, onClose, resetScan]); + + // ✅ Clear states when modal opens or lot changes + useEffect(() => { + if (open) { + setManualInput(''); + setManualInputSubmitted(false); + setManualInputError(false); + } + }, [open]); + + useEffect(() => { + if (lot) { + setManualInput(''); + setManualInputSubmitted(false); + setManualInputError(false); + } + }, [lot]); + +{/* +const handleManualSubmit = () => { + if (manualInput.trim() === lot?.lotNo) { + // ✅ Success - no error helper text needed + onQrCodeSubmit(lot.lotNo); + onClose(); + setManualInput(''); + } else { + // ✅ Show error helper text after submit + setManualInputError(true); + setManualInputSubmitted(true); + // Don't clear input - let user see what they typed + } + }; + + return ( + + + + {t("QR Code Scan for Lot")}: {lot?.lotNo} + + + + + Scanner Status: {isScanning ? 'Scanning...' : 'Ready'} + + + + + + + + + + + Manual Input: + + setManualInput(e.target.value)} + sx={{ mb: 1 }} + // ✅ Only show error after submit button is clicked + error={manualInputSubmitted && manualInputError} + helperText={ + // ✅ Show helper text only after submit with error + manualInputSubmitted && manualInputError + ? `The input is not the same as the expected lot number. Expected: ${lot?.lotNo}` + : '' + } + /> + + + + {qrValues.length > 0 && ( + + + QR Scan Result: {qrValues[qrValues.length - 1]} + + {manualInputError && ( + + ❌ Mismatch! Expected: {lot?.lotNo} + + )} + + )} + + + + + + + ); +}; +*/} +useEffect(() => { + if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '') { + // Auto-submit when manual input matches the expected lot number + console.log('🔄 Auto-submitting manual input:', manualInput.trim()); + + // Add a small delay to ensure proper execution order + const timer = setTimeout(() => { + onQrCodeSubmit(lot.lotNo); + onClose(); + setManualInput(''); + setManualInputError(false); + setManualInputSubmitted(false); + }, 200); // 200ms delay + + return () => clearTimeout(timer); + } +}, [manualInput, lot, onQrCodeSubmit, onClose]); + const handleManualSubmit = () => { + if (manualInput.trim() === lot?.lotNo) { + // ✅ Success - no error helper text needed + onQrCodeSubmit(lot.lotNo); + onClose(); + setManualInput(''); + } else { + // ✅ Show error helper text after submit + setManualInputError(true); + setManualInputSubmitted(true); + // Don't clear input - let user see what they typed + } + }; +useEffect(() => { + if (open) { + startScan(); + } + }, [open, startScan]); + return ( + + + + QR Code Scan for Lot: {lot?.lotNo} + + + {/* Manual Input with Submit-Triggered Helper Text */} + + + Manual Input: + + setManualInput(e.target.value)} + sx={{ mb: 1 }} + error={manualInputSubmitted && manualInputError} + helperText={ + manualInputSubmitted && manualInputError + ? `The input is not the same as the expected lot number.` + : '' + } + /> + + + + {/* Show QR Scan Status */} + {qrValues.length > 0 && ( + + + QR Scan Result: {qrValues[qrValues.length - 1]} + + {manualInputError && ( + + ❌ Mismatch! Expected! + + )} + + )} + + + + + + + ); +}; + + +const LotTable: React.FC = ({ + lotData, + selectedRowId, + selectedRow, + pickQtyData, + selectedLotRowId, + selectedLotId, + onLotSelection, + onPickQtyChange, + onSubmitPickQty, + onCreateStockOutLine, + onQcCheck, + onLotSelectForInput, + showInputBody, + setShowInputBody, + selectedLotForInput, + generateInputBody, + onDataRefresh, +}) => { + const { t } = useTranslation("pickOrder"); + + // ✅ Add QR scanner context + const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext(); + + // ✅ Add state for QR input modal + const [qrModalOpen, setQrModalOpen] = useState(false); + const [selectedLotForQr, setSelectedLotForQr] = useState(null); + const [manualQrInput, setManualQrInput] = useState(''); + + // 分页控制器 + const [lotTablePagingController, setLotTablePagingController] = useState({ + pageNum: 0, + pageSize: 10, + }); + + // ✅ 添加状态消息生成函数 + const getStatusMessage = useCallback((lot: LotPickData) => { + if (!lot.stockOutLineId) { + return t("Please finish QR code scan, QC check and pick order."); + } + + switch (lot.stockOutLineStatus?.toLowerCase()) { + case 'pending': + return t("Please finish QC check 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("QC check failed. 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, QC check and pick order."); + } + }, []); + + const prepareLotTableData = useMemo(() => { + return lotData.map((lot) => ({ + ...lot, + id: lot.lotId, + })); + }, [lotData]); + + // 分页数据 + const paginatedLotTableData = useMemo(() => { + const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize; + const endIndex = startIndex + lotTablePagingController.pageSize; + return prepareLotTableData.slice(startIndex, endIndex); + }, [prepareLotTableData, lotTablePagingController]); + + // 分页处理函数 + const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => { + setLotTablePagingController(prev => ({ + ...prev, + pageNum: newPage, + })); + }, []); + + const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + setLotTablePagingController({ + pageNum: 0, + pageSize: newPageSize, + }); + }, []); + + // ✅ Handle QR code submission + const handleQrCodeSubmit = useCallback(async (lotNo: string) => { + if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) { + console.log(`✅ QR Code verified for lot: ${lotNo}`); + + // ✅ Store the required quantity before creating stock out line + const requiredQty = selectedLotForQr.requiredQty; + const lotId = selectedLotForQr.lotId; + + // ✅ Create stock out line and wait for it to complete + await onCreateStockOutLine(selectedLotForQr.lotId); + + // ✅ Close modal + setQrModalOpen(false); + setSelectedLotForQr(null); + + // ✅ Set pick quantity AFTER stock out line creation and refresh is complete + if (selectedRowId) { + // Add a small delay to ensure the data refresh from onCreateStockOutLine is complete + setTimeout(() => { + onPickQtyChange(selectedRowId, lotId, requiredQty); + console.log(`✅ Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`); + }, 500); // 500ms delay to ensure refresh is complete + } + + // ✅ Show success message + console.log("Stock out line created successfully!"); + } + }, [selectedLotForQr, onCreateStockOutLine, selectedRowId, onPickQtyChange]); + + return ( + <> + + + + + {t("Selected")} + {t("Lot#")} + {t("Lot Expiry Date")} + {t("Lot Location")} + {t("Available Lot")} + {t("Lot Required Pick Qty")} + {t("Stock Unit")} + {t("QR Code Scan")} + {t("QC Check")} + {t("Lot Actual Pick Qty")} + {t("Submit")} + + + + {paginatedLotTableData.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + paginatedLotTableData.map((lot, index) => ( + + + onLotSelection(`row_${index}`, lot.lotId)} + // ✅ Allow selection of available AND insufficient_stock lots + //disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} + value={`row_${index}`} + name="lot-selection" + /> + + + + {lot.lotNo} + {lot.lotAvailability !== 'available' && ( + + ({lot.lotAvailability === 'expired' ? 'Expired' : + lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : + 'Unavailable'}) + + )} + + + {lot.expiryDate} + {lot.location} + {lot.availableQty.toLocaleString()} + {lot.requiredQty.toLocaleString()} + {lot.stockUnit} + + {/* QR Code Scan Button */} + + + + + + + + + {/* QC Check Button */} + {/* + + + */} + + + {/* Lot Actual Pick Qty */} + + { + if (selectedRowId) { + const inputValue = e.target.value; + // ✅ Fixed: Handle empty string and prevent leading zeros + if (inputValue === '') { + // Allow empty input (user can backspace to clear) + onPickQtyChange(selectedRowId, lot.lotId, 0); + } else { + // Parse the number and prevent leading zeros + const numValue = parseInt(inputValue, 10); + if (!isNaN(numValue)) { + onPickQtyChange(selectedRowId, lot.lotId, numValue); + } + } + } + }} + onBlur={(e) => { + // ✅ Fixed: When input loses focus, ensure we have a valid number + if (selectedRowId) { + const currentValue = pickQtyData[selectedRowId]?.[lot.lotId]; + if (currentValue === undefined || currentValue === null) { + // Set to 0 if no value + onPickQtyChange(selectedRowId, lot.lotId, 0); + } + } + }} + inputProps={{ + min: 0, + max: lot.availableQty, + step: 1 // Allow only whole numbers + }} + // ✅ Allow input for available AND insufficient_stock lots + disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} + sx={{ width: '80px' }} + placeholder="0" // Show placeholder instead of default value + /> + + + + + {/* Submit Button */} + + + + + )) + )} + +
+
+ + {/* ✅ Status Messages Display */} + {paginatedLotTableData.length > 0 && ( + + {paginatedLotTableData.map((lot, index) => ( + + + 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} + onQrCodeSubmit={handleQrCodeSubmit} + /> + + ); +}; + +export default LotTable; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx b/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx new file mode 100644 index 0000000..7cacf6e --- /dev/null +++ b/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx @@ -0,0 +1,288 @@ +"use client"; +// 修改为 PickOrder 相关的导入 +import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { PurchaseQcResult } from "@/app/api/po/actions"; +import { + Box, + Button, + Grid, + Modal, + ModalProps, + Stack, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, +} from "@mui/material"; +import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate"; +import { submitDialogWithWarning } from "../Swal/CustomAlerts"; + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + display: "block", + width: { xs: "60%", sm: "60%", md: "60%" }, +}; + +// 修改接口定义 +interface CommonProps extends Omit { + itemDetail: GetPickOrderLineInfo & { + pickOrderCode: string; + qcResult?: PurchaseQcResult[] + }; + setItemDetail: Dispatch< + SetStateAction< + | (GetPickOrderLineInfo & { + pickOrderCode: string; + warehouseId?: number; + }) + | undefined + > + >; + qc?: QcItemWithChecks[]; + warehouse?: any[]; +} + +interface Props extends CommonProps { + itemDetail: GetPickOrderLineInfo & { + pickOrderCode: string; + qcResult?: PurchaseQcResult[] + }; +} + +// 修改组件名称 +const PickQcStockInModalVer2: React.FC = ({ + open, + onClose, + itemDetail, + setItemDetail, + qc, + warehouse, +}) => { + console.log(warehouse); + // 修改翻译键 + const { + t, + i18n: { language }, + } = useTranslation("pickOrder"); + + const [qcItems, setQcItems] = useState(dummyQCData) + const formProps = useForm({ + defaultValues: { + ...itemDetail, + }, + }); + + const closeHandler = useCallback>( + (...args) => { + onClose?.(...args); + }, + [onClose], + ); + + // QC submission handler + const onSubmitQc = useCallback>( + async (data, event) => { + console.log("QC Submission:", event!.nativeEvent); + + // Get QC data from the shared form context + const qcAccept = data.qcAccept; + const acceptQty = data.acceptQty; + + // Validate QC data + const validationErrors : string[] = []; + // Check if all QC items have results + const itemsWithoutResult = qcItems.filter(item => item.isPassed === undefined); + if (itemsWithoutResult.length > 0) { + validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.qcItem).join(', ')}`); + } + + // Check if failed items have failed quantity + const failedItemsWithoutQty = qcItems.filter(item => + item.isPassed === false && (!item.failedQty || item.failedQty <= 0) + ); + if (failedItemsWithoutQty.length > 0) { + validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`); + } + + // Check if accept quantity is valid + if (acceptQty === undefined || acceptQty <= 0) { + validationErrors.push("Accept quantity must be greater than 0"); + } + + if (validationErrors.length > 0) { + console.error("QC Validation failed:", validationErrors); + alert(`未完成品檢: ${validationErrors}`); + return; + } + + const qcData = { + qcAccept: qcAccept, + acceptQty: acceptQty, + qcItems: qcItems.map(item => ({ + id: item.id, + qcItem: item.qcItem, + qcDescription: item.qcDescription, + isPassed: item.isPassed, + failedQty: (item.failedQty && !item.isPassed) || 0, + remarks: item.remarks || '' + })) + }; + + console.log("QC Data for submission:", qcData); + // await submitQcData(qcData); + + if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { + submitDialogWithWarning(() => { + console.log("QC accepted with failed items"); + onClose(); + }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); + return; + } + + if (qcData.qcAccept) { + console.log("QC accepted"); + onClose(); + } else { + console.log("QC rejected"); + onClose(); + } + }, + [qcItems, onClose, t], + ); + + const handleQcItemChange = useCallback((index: number, field: keyof QcData, value: any) => { + setQcItems(prev => prev.map((item, i) => + i === index ? { ...item, [field]: value } : item + )); + }, []); + + return ( + <> + + + + + + + GroupA - {itemDetail.pickOrderCode} + + + 記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標 + + + + {/* QC 表格 */} + + + + + + QC模板代號 + 檢查項目 + QC Result + Failed Qty + Remarks + + + + {qcItems.map((item, index) => ( + + {item.id} + {item.qcDescription} + + + + + handleQcItemChange(index, 'failedQty', parseInt(e.target.value) || 0)} + disabled={item.isPassed !== false} + /> + + + handleQcItemChange(index, 'remarks', e.target.value)} + /> + + + ))} + +
+
+
+ + {/* 按钮 */} + + + + + + + +
+
+
+
+ + ); +}; + +export default PickQcStockInModalVer2; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx b/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx new file mode 100644 index 0000000..6857bfb --- /dev/null +++ b/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx @@ -0,0 +1,683 @@ +"use client"; + +import { GetPickOrderLineInfo, updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { PurchaseQcResult } from "@/app/api/po/actions"; +import { + Box, + Button, + Grid, + Modal, + ModalProps, + Stack, + Typography, + TextField, + Radio, + RadioGroup, + FormControlLabel, + FormControl, + Tab, + Tabs, + TabsProps, + Paper, +} from "@mui/material"; +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; +import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { dummyQCData } from "../PoDetail/dummyQcTemplate"; +import StyledDataGrid from "../StyledDataGrid"; +import { GridColDef } from "@mui/x-data-grid"; +import { submitDialogWithWarning } from "../Swal/CustomAlerts"; +import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; +import EscalationComponent from "../PoDetail/EscalationComponent"; +import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions"; +import { + updateInventoryLotLineStatus +} from "@/app/api/inventory/actions"; // ✅ 导入新的 API +import { dayjsToInputDateString } from "@/app/utils/formatUtil"; +import dayjs from "dayjs"; + +// Define QcData interface locally +interface ExtendedQcItem extends QcItemWithChecks { + qcPassed?: boolean; + failQty?: number; + remarks?: string; + order?: number; // ✅ Add order property + stableId?: string; // ✅ Also add stableId for better row identification +} +interface Props extends CommonProps { + itemDetail: GetPickOrderLineInfo & { + pickOrderCode: string; + qcResult?: PurchaseQcResult[] + }; + qcItems: ExtendedQcItem[]; + setQcItems: Dispatch>; + selectedLotId?: number; + onStockOutLineUpdate?: () => void; + lotData: LotPickData[]; + // ✅ Add missing props + pickQtyData?: PickQtyData; + selectedRowId?: number; +} + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + display: "block", + width: { xs: "80%", sm: "80%", md: "80%" }, + maxHeight: "90vh", + overflowY: "auto", +}; +interface PickQtyData { + [lineId: number]: { + [lotId: number]: number; + }; +} +interface CommonProps extends Omit { + itemDetail: GetPickOrderLineInfo & { + pickOrderCode: string; + qcResult?: PurchaseQcResult[] + }; + setItemDetail: Dispatch< + SetStateAction< + | (GetPickOrderLineInfo & { + pickOrderCode: string; + warehouseId?: number; + }) + | undefined + > + >; + qc?: QcItemWithChecks[]; + warehouse?: any[]; +} + +interface Props extends CommonProps { + itemDetail: GetPickOrderLineInfo & { + pickOrderCode: string; + qcResult?: PurchaseQcResult[] + }; + qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem + setQcItems: Dispatch>; // Change to ExtendedQcItem + // ✅ Add props for stock out line update + selectedLotId?: number; + onStockOutLineUpdate?: () => void; + lotData: LotPickData[]; +} +interface LotPickData { + id: number; + lotId: number; + lotNo: string; + expiryDate: string; + location: string; + stockUnit: string; + availableQty: number; + requiredQty: number; + actualPickQty: number; + lotStatus: string; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; + stockOutLineId?: number; + stockOutLineStatus?: string; + stockOutLineQty?: number; +} +const PickQcStockInModalVer3: React.FC = ({ + open, + onClose, + itemDetail, + setItemDetail, + qc, + warehouse, + qcItems, + setQcItems, + selectedLotId, + onStockOutLineUpdate, + lotData, + pickQtyData, + selectedRowId, +}) => { + const { + t, + i18n: { language }, + } = useTranslation("pickOrder"); + + const [tabIndex, setTabIndex] = useState(0); + //const [qcItems, setQcItems] = useState(dummyQCData); + const [isCollapsed, setIsCollapsed] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [feedbackMessage, setFeedbackMessage] = useState(""); + + // Add state to store submitted data + const [submittedData, setSubmittedData] = useState([]); + + const formProps = useForm({ + defaultValues: { + qcAccept: true, + acceptQty: null, + qcDecision: "1", // Default to accept + ...itemDetail, + }, + }); + const { control, register, formState: { errors }, watch, setValue } = formProps; + + const qcDecision = watch("qcDecision"); + const accQty = watch("acceptQty"); + + const closeHandler = useCallback>( + (...args) => { + onClose?.(...args); + }, + [onClose], + ); + + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [], + ); + + // Save failed QC results only + const saveQcResults = async (qcData: any) => { + try { + const qcResults = qcData.qcItems + .map((item: any) => ({ + qcItemId: item.id, + itemId: itemDetail.itemId, + stockInLineId: null, + stockOutLineId: 1, // Fixed to 1 as requested + failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed + type: "pick_order_qc", + remarks: item.remarks || "", + qcPassed: item.isPassed, // ✅ This will now be included + })); + + // Store the submitted data for debug display + setSubmittedData(qcResults); + console.log("Saving QC results:", qcResults); + + // Use the corrected API function instead of manual fetch + for (const qcResult of qcResults) { + const response = await savePickOrderQcResult(qcResult); + console.log("QC Result save success:", response); + + // Check if the response indicates success + if (!response.id) { + throw new Error(`Failed to save QC result: ${response.message || 'Unknown error'}`); + } + } + return true; + } catch (error) { + console.error("Error saving QC results:", error); + return false; + } + }; + + // ✅ 修改:在组件开始时自动设置失败数量 + useEffect(() => { + if (itemDetail && qcItems.length > 0 && selectedLotId) { + // ✅ 获取选中的批次数据 + const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); + if (selectedLot) { + // ✅ 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty + const updatedQcItems = qcItems.map((item, index) => ({ + ...item, + failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty + // ✅ Add stable order and ID fields + order: index, + stableId: `qc-${item.id}-${index}` + })); + setQcItems(updatedQcItems); + } + } + }, [itemDetail, qcItems.length, selectedLotId, lotData]); + + // ✅ Add this helper function at the top of the component + const safeClose = useCallback(() => { + if (onClose) { + // Create a mock event object that satisfies the Modal onClose signature + const mockEvent = { + target: null, + currentTarget: null, + type: 'close', + preventDefault: () => {}, + stopPropagation: () => {}, + bubbles: false, + cancelable: false, + defaultPrevented: false, + isTrusted: false, + timeStamp: Date.now(), + nativeEvent: null, + isDefaultPrevented: () => false, + isPropagationStopped: () => false, + persist: () => {}, + eventPhase: 0, + isPersistent: () => false + } as any; + + // ✅ Fixed: Pass both event and reason parameters + onClose(mockEvent, 'escapeKeyDown'); // 'escapeKeyDown' is a valid reason + } + }, [onClose]); + + // ✅ 修改:移除 alert 弹窗,改为控制台日志 + const onSubmitQc = useCallback>( + async (data, event) => { + setIsSubmitting(true); + + try { + const qcAccept = qcDecision === "1"; + const acceptQty = Number(accQty) || null; + + const validationErrors : string[] = []; + const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); + + // ✅ Add safety check for selectedLot + if (!selectedLot) { + console.error("Selected lot not found"); + return; + } + + const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); + if (itemsWithoutResult.length > 0) { + validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); + } + + if (validationErrors.length > 0) { + console.error(`QC validation failed: ${validationErrors.join(", ")}`); + return; + } + + const qcData = { + qcAccept, + acceptQty, + qcItems: qcItems.map(item => ({ + id: item.id, + qcItem: item.code, + qcDescription: item.description || "", + isPassed: item.qcPassed, + failQty: item.qcPassed ? 0 : (selectedLot.requiredQty || 0), + remarks: item.remarks || "", + })), + }; + + console.log("Submitting QC data:", qcData); + + const saveSuccess = await saveQcResults(qcData); + if (!saveSuccess) { + console.error("Failed to save QC results"); + return; + } + + // ✅ Handle different QC decisions + if (selectedLotId) { + try { + const allPassed = qcData.qcItems.every(item => item.isPassed); + + if (qcDecision === "2") { + // ✅ QC Decision 2: Report and Re-pick + console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable"); + + // ✅ Inventory lot line status: unavailable + if (selectedLot) { + try { + console.log("=== DEBUG: Updating inventory lot line status ==="); + console.log("Selected lot:", selectedLot); + console.log("Selected lot ID:", selectedLotId); + + // ✅ FIX: Only send the fields that the backend expects + const updateData = { + inventoryLotLineId: selectedLot.lotId, + status: 'unavailable' + // ❌ Remove qty and operation - backend doesn't expect these + }; + + console.log("Update data:", updateData); + + const result = await updateInventoryLotLineStatus(updateData); + console.log("✅ Inventory lot line status updated successfully:", result); + + } catch (error) { + console.error("❌ Error updating inventory lot line status:", error); + console.error("Error details:", { + selectedLot, + selectedLotId, + acceptQty + }); + + // Show user-friendly error message + + return; // Stop execution if this fails + } + } else { + console.error("❌ Selected lot not found for inventory update"); + alert("Selected lot not found. Cannot update inventory status."); + return; + } + + // ✅ Close modal and refresh data + safeClose(); // ✅ Fixed: Use safe close function with both parameters + if (onStockOutLineUpdate) { + onStockOutLineUpdate(); + } + + } else if (qcDecision === "1") { + // ✅ QC Decision 1: Accept + console.log("QC Decision 1 - Accept: QC passed"); + + // ✅ Stock out line status: checked (QC completed) + await updateStockOutLineStatus({ + id: selectedLotId, + status: 'checked', + qty: acceptQty || 0 + }); + + // ✅ Inventory lot line status: NO CHANGE needed + // Keep the existing status from handleSubmitPickQty + + // ✅ Close modal and refresh data + safeClose(); // ✅ Fixed: Use safe close function with both parameters + if (onStockOutLineUpdate) { + onStockOutLineUpdate(); + } + } + + console.log("Stock out line status updated successfully after QC"); + + // Call callback to refresh data + if (onStockOutLineUpdate) { + onStockOutLineUpdate(); + } + } catch (error) { + console.error("Error updating stock out line status after QC:", error); + } + } + + console.log("QC results saved successfully!"); + + // ✅ Show warning dialog for failed QC items when accepting + if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) { + submitDialogWithWarning(() => { + closeHandler?.({}, 'escapeKeyDown'); + }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); + return; + } + + closeHandler?.({}, 'escapeKeyDown'); + } catch (error) { + console.error("Error in QC submission:", error); + if (error instanceof Error) { + console.error("Error details:", error.message); + console.error("Error stack:", error.stack); + } + } finally { + setIsSubmitting(false); + } + }, + [qcItems, closeHandler, t, itemDetail, qcDecision, accQty, selectedLotId, onStockOutLineUpdate, lotData, pickQtyData, selectedRowId], + ); + // DataGrid columns (QcComponent style) + const qcColumns: GridColDef[] = useMemo( + () => [ + { + field: "code", + headerName: t("qcItem"), + flex: 2, + renderCell: (params) => ( + + {`${params.api.getRowIndexRelativeToVisibleRows(params.id) + 1}. ${params.value}`}
+ {params.row.name}
+
+ ), + }, + { + field: "qcPassed", + headerName: t("qcResult"), + flex: 1.5, + renderCell: (params) => { + const current = params.row; + return ( + + { + const value = e.target.value === "true"; + // ✅ Simple state update + setQcItems(prev => + prev.map(item => + item.id === params.id + ? { ...item, qcPassed: value } + : item + ) + ); + }} + name={`qcPassed-${params.id}`} + > + } + label="合格" + sx={{ + color: current.qcPassed === true ? "green" : "inherit", + "& .Mui-checked": {color: "green"} + }} + /> + } + label="不合格" + sx={{ + color: current.qcPassed === false ? "red" : "inherit", + "& .Mui-checked": {color: "red"} + }} + /> + + + ); + }, + }, + { + field: "failQty", + headerName: t("failedQty"), + flex: 1, + 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, failQty: next } : r)) + // ); + // }} + onClick={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + inputProps={{ min: 0, max: itemDetail?.requiredQty || 0 }} + sx={{ width: "100%" }} + /> + ), + }, + { + field: "remarks", + headerName: t("remarks"), + flex: 2, + renderCell: (params) => ( + { + const remarks = e.target.value; + setQcItems((prev) => + prev.map((r) => (r.id === params.id ? { ...r, remarks } : r)) + ); + }} + onClick={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + sx={{ width: "100%" }} + /> + ), + }, + ], + [t], + ); + + // ✅ Add stable update function + const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => { + setQcItems(prevItems => + prevItems.map(item => + item.id === itemId + ? { ...item, qcPassed } + : item + ) + ); + }, []); + + // ✅ Remove duplicate functions + const getRowId = useCallback((row: any) => { + return row.id; // Just use the original ID + }, []); + + // ✅ Remove complex sorting logic + // const stableQcItems = useMemo(() => { ... }); // Remove + // const sortedQcItems = useMemo(() => { ... }); // Remove + + // ✅ Use qcItems directly in DataGrid + return ( + <> + + + + + + + + + + + + {tabIndex == 0 && ( + <> + + + + Group A - 急凍貨類 (QCA1-MEAT01) + + + + 品檢類型:OQC + + + 記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全
+ 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標 +
+ +
+ + +
+ + )} + + {tabIndex == 1 && ( + <> + + + + + )} + + + + ( + { + const value = e.target.value.toString(); + if (value != "1" && Boolean(errors.acceptQty)) { + setValue("acceptQty", itemDetail.requiredQty ?? 0); + } + field.onChange(value); + }} + > + } + label={t("Accept Stock Out")} + /> + + + {/* ✅ Combirne options 2 & 3 into one */} + } + sx={{"& .Mui-checked": {color: "blue"}}} + label={t("Report and Pick another lot")} + /> + + )} + /> + + + +{/* ✅ Show escalation component when QC Decision = 2 (Report and Re-pick) */} + + + + + + + + +
+
+
+
+ + ); +}; + +export default PickQcStockInModalVer3; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/PutawayForm.tsx b/src/components/FinishedGoodSearch/PutawayForm.tsx new file mode 100644 index 0000000..aea7779 --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/QCDatagrid.tsx b/src/components/FinishedGoodSearch/QCDatagrid.tsx new file mode 100644 index 0000000..b9947db --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/QcFormVer2.tsx b/src/components/FinishedGoodSearch/QcFormVer2.tsx new file mode 100644 index 0000000..ebea29d --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/QcSelect.tsx b/src/components/FinishedGoodSearch/QcSelect.tsx new file mode 100644 index 0000000..b42732b --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/SearchResultsTable.tsx b/src/components/FinishedGoodSearch/SearchResultsTable.tsx new file mode 100644 index 0000000..5ceb5f8 --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/StockInFormVer2.tsx b/src/components/FinishedGoodSearch/StockInFormVer2.tsx new file mode 100644 index 0000000..32b9169 --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/TwoLineCell.tsx b/src/components/FinishedGoodSearch/TwoLineCell.tsx new file mode 100644 index 0000000..f32e56a --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/UomSelect.tsx b/src/components/FinishedGoodSearch/UomSelect.tsx new file mode 100644 index 0000000..1fec4ab --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/VerticalSearchBox.tsx b/src/components/FinishedGoodSearch/VerticalSearchBox.tsx new file mode 100644 index 0000000..3695e96 --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/assignTo copy.tsx b/src/components/FinishedGoodSearch/assignTo copy.tsx new file mode 100644 index 0000000..87ab821 --- /dev/null +++ b/src/components/FinishedGoodSearch/assignTo copy.tsx @@ -0,0 +1,511 @@ +"use client"; +import { + Autocomplete, + Box, + Button, + CircularProgress, + FormControl, + Grid, + Modal, + TextField, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Checkbox, + TablePagination, +} from "@mui/material"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + newassignPickOrder, + AssignPickOrderInputs, + releaseAssignedPickOrders, + fetchPickOrderWithStockClient, // Add this import +} from "@/app/api/pickOrder/actions"; +import { fetchNameList, NameList } from "@/app/api/user/actions"; +import { + FormProvider, + useForm, +} from "react-hook-form"; +import { isEmpty, upperFirst, groupBy } from "lodash"; +import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import dayjs from "dayjs"; +import arraySupport from "dayjs/plugin/arraySupport"; +import SearchBox, { Criterion } from "../SearchBox"; +import { sortBy, uniqBy } from "lodash"; +import { createStockOutLine, CreateStockOutLine, fetchPickOrderDetails } from "@/app/api/pickOrder/actions"; +dayjs.extend(arraySupport); + +interface Props { + filterArgs: Record; +} + +// Update the interface to match the new API response structure +interface PickOrderRow { + id: string; + code: string; + targetDate: string; + type: string; + status: string; + assignTo: number; + groupName: string; + consoCode?: string; + pickOrderLines: PickOrderLineRow[]; +} + +interface PickOrderLineRow { + id: number; + itemId: number; + itemCode: string; + itemName: string; + availableQty: number | null; + requiredQty: number; + uomCode: string; + uomDesc: string; + suggestedList: any[]; +} + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + width: { xs: "100%", sm: "100%", md: "100%" }, +}; + +const AssignTo: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("pickOrder"); + const { setIsUploading } = useUploadContext(); + const [isUploading, setIsUploadingLocal] = useState(false); + // Update state to use pick order data directly + const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); + const [filteredPickOrders, setFilteredPickOrders] = useState([]); + const [isLoadingItems, setIsLoadingItems] = useState(false); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }); + const [totalCountItems, setTotalCountItems] = useState(); + const [modalOpen, setModalOpen] = useState(false); + const [usernameList, setUsernameList] = useState([]); + const [searchQuery, setSearchQuery] = useState>({}); + const [originalPickOrderData, setOriginalPickOrderData] = useState([]); + + const formProps = useForm(); + const errors = formProps.formState.errors; + + // Update the handler functions to work with string IDs + const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => { + if (checked) { + setSelectedPickOrderIds(prev => [...prev, pickOrderId]); + } else { + setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); + } + }, []); + + const isPickOrderSelected = useCallback((pickOrderId: string) => { + return selectedPickOrderIds.includes(pickOrderId); + }, [selectedPickOrderIds]); + + // Update the fetch function to use the correct endpoint + const fetchNewPageItems = useCallback( + async (pagingController: Record, filterArgs: Record) => { + setIsLoadingItems(true); + try { + const params = { + ...pagingController, + ...filterArgs, + pageNum: (pagingController.pageNum || 1) - 1, + pageSize: pagingController.pageSize || 10, + // Filter for assigned status only + status: "assigned" + }; + + const res = await fetchPickOrderWithStockClient(params); + + if (res && res.records) { + // Convert pick order data to the expected format + const pickOrderRows: PickOrderRow[] = res.records.map((pickOrder: any) => ({ + id: pickOrder.id, + code: pickOrder.code, + targetDate: pickOrder.targetDate, + type: pickOrder.type, + status: pickOrder.status, + assignTo: pickOrder.assignTo, + groupName: pickOrder.groupName || "No Group", + consoCode: pickOrder.consoCode, + pickOrderLines: pickOrder.pickOrderLines || [] + })); + + setOriginalPickOrderData(pickOrderRows); + setFilteredPickOrders(pickOrderRows); + setTotalCountItems(res.total); + } else { + setFilteredPickOrders([]); + setTotalCountItems(0); + } + } catch (error) { + console.error("Error fetching pick orders:", error); + setFilteredPickOrders([]); + setTotalCountItems(0); + } finally { + setIsLoadingItems(false); + } + }, + [], + ); + + // Handle Release operation +// Handle Release operation +const handleRelease = useCallback(async () => { + if (selectedPickOrderIds.length === 0) return; + + setIsUploading(true); + try { + // Get the assigned user from the selected pick orders + const selectedPickOrders = filteredPickOrders.filter(pickOrder => + selectedPickOrderIds.includes(pickOrder.id) + ); + + // Check if all selected pick orders have the same assigned user + const assignedUsers = selectedPickOrders.map(po => po.assignTo).filter(Boolean); + if (assignedUsers.length === 0) { + alert("Selected pick orders are not assigned to any user."); + return; + } + + const assignToValue = assignedUsers[0]; + + // Validate that all pick orders are assigned to the same user + const allSameUser = assignedUsers.every(userId => userId === assignToValue); + if (!allSameUser) { + alert("All selected pick orders must be assigned to the same user."); + return; + } + + console.log("Using assigned user:", assignToValue); + console.log("selectedPickOrderIds:", selectedPickOrderIds); + + const releaseRes = await releaseAssignedPickOrders({ + pickOrderIds: selectedPickOrderIds.map(id => parseInt(id)), + assignTo: assignToValue + }); + + if (releaseRes.code === "SUCCESS") { + console.log("Pick orders released successfully"); + + // Get the consoCode from the response + const consoCode = (releaseRes.entity as any)?.consoCode; + + if (consoCode) { + // Create StockOutLine records for each pick order line + for (const pickOrder of selectedPickOrders) { + for (const line of pickOrder.pickOrderLines) { + try { + const stockOutLineData = { + consoCode: consoCode, + pickOrderLineId: line.id, + inventoryLotLineId: 0, // This will be set when user scans QR code + qty: line.requiredQty, + }; + + console.log("Creating stock out line:", stockOutLineData); + await createStockOutLine(stockOutLineData); + } catch (error) { + console.error("Error creating stock out line for line", line.id, error); + } + } + } + } + + fetchNewPageItems(pagingController, filterArgs); + } else { + console.error("Release failed:", releaseRes.message); + } + } catch (error) { + console.error("Error releasing pick orders:", error); + } finally { + setIsUploading(false); + } +}, [selectedPickOrderIds, filteredPickOrders, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); + + // Update search criteria to match the new data structure + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Pick Order Code"), + paramName: "code", + type: "text", + }, + { + label: t("Group Code"), + paramName: "groupName", + type: "text", + }, + { + label: t("Target Date From"), + label2: t("Target Date To"), + paramName: "targetDate", + type: "dateRange", + }, + ], + [t], + ); + + // Update search function to work with pick order data + const handleSearch = useCallback((query: Record) => { + setSearchQuery({ ...query }); + + const filtered = originalPickOrderData.filter((pickOrder) => { + const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); + + const codeMatch = !query.code || + pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); + + const groupNameMatch = !query.groupName || + pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); + + // Date range search + let dateMatch = true; + if (query.targetDate || query.targetDateTo) { + try { + if (query.targetDate && !query.targetDateTo) { + const fromDate = dayjs(query.targetDate); + dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || + pickOrderTargetDateStr.isAfter(fromDate, 'day'); + } else if (!query.targetDate && query.targetDateTo) { + const toDate = dayjs(query.targetDateTo); + dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || + pickOrderTargetDateStr.isBefore(toDate, 'day'); + } else if (query.targetDate && query.targetDateTo) { + const fromDate = dayjs(query.targetDate); + const toDate = dayjs(query.targetDateTo); + dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || + pickOrderTargetDateStr.isAfter(fromDate, 'day')) && + (pickOrderTargetDateStr.isSame(toDate, 'day') || + pickOrderTargetDateStr.isBefore(toDate, 'day')); + } + } catch (error) { + console.error("Date parsing error:", error); + dateMatch = true; + } + } + + return codeMatch && groupNameMatch && dateMatch; + }); + + setFilteredPickOrders(filtered); + }, [originalPickOrderData]); + + const handleReset = useCallback(() => { + setSearchQuery({}); + setFilteredPickOrders(originalPickOrderData); + setTimeout(() => { + setSearchQuery({}); + }, 0); + }, [originalPickOrderData]); + + // Pagination handlers + const handlePageChange = useCallback((event: unknown, newPage: number) => { + const newPagingController = { + ...pagingController, + pageNum: newPage + 1, + }; + setPagingController(newPagingController); + }, [pagingController]); + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + const newPagingController = { + pageNum: 1, + pageSize: newPageSize, + }; + setPagingController(newPagingController); + }, []); + + // Component mount effect + useEffect(() => { + fetchNewPageItems(pagingController, filterArgs || {}); + }, []); + + // Dependencies change effect + useEffect(() => { + if (pagingController && (filterArgs || {})) { + fetchNewPageItems(pagingController, filterArgs || {}); + } + }, [pagingController, filterArgs, fetchNewPageItems]); + + useEffect(() => { + const loadUsernameList = async () => { + try { + const res = await fetchNameList(); + if (res) { + setUsernameList(res); + } + } catch (error) { + console.error("Error loading username list:", error); + } + }; + loadUsernameList(); + }, []); + + // Update the table component to work with pick order data directly + const CustomPickOrderTable = () => { + // Helper function to get user name + const getUserName = useCallback((assignToId: number | null | undefined) => { + if (!assignToId) return '-'; + const user = usernameList.find(u => u.id === assignToId); + return user ? user.name : `User ${assignToId}`; + }, [usernameList]); + + return ( + <> + + + + + {t("Selected")} + {t("Pick Order Code")} + {t("Group Code")} + {t("Item Code")} + {t("Item Name")} + {t("Order Quantity")} + {t("Current Stock")} + {t("Stock Unit")} + {t("Target Date")} + {t("Assigned To")} + + + + {filteredPickOrders.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + filteredPickOrders.map((pickOrder) => ( + pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => ( + + {/* Checkbox - only show for first line of each pick order */} + + {index === 0 ? ( + handlePickOrderSelect(pickOrder.id, e.target.checked)} + disabled={!isEmpty(pickOrder.consoCode)} + /> + ) : null} + + {/* Pick Order Code - only show for first line */} + + {index === 0 ? pickOrder.code : null} + + {/* Group Name - only show for first line */} + + {index === 0 ? pickOrder.groupName : null} + + {/* Item Code */} + {line.itemCode} + {/* Item Name */} + {line.itemName} + + {/* Order Quantity */} + {line.requiredQty} + + {/* Current Stock */} + + 0 ? "success.main" : "error.main"} + sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} + > + {(line.availableQty || 0).toLocaleString()} + + + + {/* Unit */} + {line.uomDesc} + + {/* Target Date - only show for first line */} + + {index === 0 ? ( + arrayToDayjs(pickOrder.targetDate) + .add(-1, "month") + .format(OUTPUT_DATE_FORMAT) + ) : null} + + + {/* Assigned To - only show for first line */} + + {index === 0 ? ( + + {getUserName(pickOrder.assignTo)} + + ) : null} + + + )) + )) + )} + +
+
+ + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); + }; + + return ( + <> + + + + {isLoadingItems ? ( + + ) : ( + + )} + + + + + + + + + ); +}; + +export default AssignTo; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/assignTo.tsx b/src/components/FinishedGoodSearch/assignTo.tsx new file mode 100644 index 0000000..87ab821 --- /dev/null +++ b/src/components/FinishedGoodSearch/assignTo.tsx @@ -0,0 +1,511 @@ +"use client"; +import { + Autocomplete, + Box, + Button, + CircularProgress, + FormControl, + Grid, + Modal, + TextField, + Typography, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + Checkbox, + TablePagination, +} from "@mui/material"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + newassignPickOrder, + AssignPickOrderInputs, + releaseAssignedPickOrders, + fetchPickOrderWithStockClient, // Add this import +} from "@/app/api/pickOrder/actions"; +import { fetchNameList, NameList } from "@/app/api/user/actions"; +import { + FormProvider, + useForm, +} from "react-hook-form"; +import { isEmpty, upperFirst, groupBy } from "lodash"; +import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; +import useUploadContext from "../UploadProvider/useUploadContext"; +import dayjs from "dayjs"; +import arraySupport from "dayjs/plugin/arraySupport"; +import SearchBox, { Criterion } from "../SearchBox"; +import { sortBy, uniqBy } from "lodash"; +import { createStockOutLine, CreateStockOutLine, fetchPickOrderDetails } from "@/app/api/pickOrder/actions"; +dayjs.extend(arraySupport); + +interface Props { + filterArgs: Record; +} + +// Update the interface to match the new API response structure +interface PickOrderRow { + id: string; + code: string; + targetDate: string; + type: string; + status: string; + assignTo: number; + groupName: string; + consoCode?: string; + pickOrderLines: PickOrderLineRow[]; +} + +interface PickOrderLineRow { + id: number; + itemId: number; + itemCode: string; + itemName: string; + availableQty: number | null; + requiredQty: number; + uomCode: string; + uomDesc: string; + suggestedList: any[]; +} + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + width: { xs: "100%", sm: "100%", md: "100%" }, +}; + +const AssignTo: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("pickOrder"); + const { setIsUploading } = useUploadContext(); + const [isUploading, setIsUploadingLocal] = useState(false); + // Update state to use pick order data directly + const [selectedPickOrderIds, setSelectedPickOrderIds] = useState([]); + const [filteredPickOrders, setFilteredPickOrders] = useState([]); + const [isLoadingItems, setIsLoadingItems] = useState(false); + const [pagingController, setPagingController] = useState({ + pageNum: 1, + pageSize: 10, + }); + const [totalCountItems, setTotalCountItems] = useState(); + const [modalOpen, setModalOpen] = useState(false); + const [usernameList, setUsernameList] = useState([]); + const [searchQuery, setSearchQuery] = useState>({}); + const [originalPickOrderData, setOriginalPickOrderData] = useState([]); + + const formProps = useForm(); + const errors = formProps.formState.errors; + + // Update the handler functions to work with string IDs + const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => { + if (checked) { + setSelectedPickOrderIds(prev => [...prev, pickOrderId]); + } else { + setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); + } + }, []); + + const isPickOrderSelected = useCallback((pickOrderId: string) => { + return selectedPickOrderIds.includes(pickOrderId); + }, [selectedPickOrderIds]); + + // Update the fetch function to use the correct endpoint + const fetchNewPageItems = useCallback( + async (pagingController: Record, filterArgs: Record) => { + setIsLoadingItems(true); + try { + const params = { + ...pagingController, + ...filterArgs, + pageNum: (pagingController.pageNum || 1) - 1, + pageSize: pagingController.pageSize || 10, + // Filter for assigned status only + status: "assigned" + }; + + const res = await fetchPickOrderWithStockClient(params); + + if (res && res.records) { + // Convert pick order data to the expected format + const pickOrderRows: PickOrderRow[] = res.records.map((pickOrder: any) => ({ + id: pickOrder.id, + code: pickOrder.code, + targetDate: pickOrder.targetDate, + type: pickOrder.type, + status: pickOrder.status, + assignTo: pickOrder.assignTo, + groupName: pickOrder.groupName || "No Group", + consoCode: pickOrder.consoCode, + pickOrderLines: pickOrder.pickOrderLines || [] + })); + + setOriginalPickOrderData(pickOrderRows); + setFilteredPickOrders(pickOrderRows); + setTotalCountItems(res.total); + } else { + setFilteredPickOrders([]); + setTotalCountItems(0); + } + } catch (error) { + console.error("Error fetching pick orders:", error); + setFilteredPickOrders([]); + setTotalCountItems(0); + } finally { + setIsLoadingItems(false); + } + }, + [], + ); + + // Handle Release operation +// Handle Release operation +const handleRelease = useCallback(async () => { + if (selectedPickOrderIds.length === 0) return; + + setIsUploading(true); + try { + // Get the assigned user from the selected pick orders + const selectedPickOrders = filteredPickOrders.filter(pickOrder => + selectedPickOrderIds.includes(pickOrder.id) + ); + + // Check if all selected pick orders have the same assigned user + const assignedUsers = selectedPickOrders.map(po => po.assignTo).filter(Boolean); + if (assignedUsers.length === 0) { + alert("Selected pick orders are not assigned to any user."); + return; + } + + const assignToValue = assignedUsers[0]; + + // Validate that all pick orders are assigned to the same user + const allSameUser = assignedUsers.every(userId => userId === assignToValue); + if (!allSameUser) { + alert("All selected pick orders must be assigned to the same user."); + return; + } + + console.log("Using assigned user:", assignToValue); + console.log("selectedPickOrderIds:", selectedPickOrderIds); + + const releaseRes = await releaseAssignedPickOrders({ + pickOrderIds: selectedPickOrderIds.map(id => parseInt(id)), + assignTo: assignToValue + }); + + if (releaseRes.code === "SUCCESS") { + console.log("Pick orders released successfully"); + + // Get the consoCode from the response + const consoCode = (releaseRes.entity as any)?.consoCode; + + if (consoCode) { + // Create StockOutLine records for each pick order line + for (const pickOrder of selectedPickOrders) { + for (const line of pickOrder.pickOrderLines) { + try { + const stockOutLineData = { + consoCode: consoCode, + pickOrderLineId: line.id, + inventoryLotLineId: 0, // This will be set when user scans QR code + qty: line.requiredQty, + }; + + console.log("Creating stock out line:", stockOutLineData); + await createStockOutLine(stockOutLineData); + } catch (error) { + console.error("Error creating stock out line for line", line.id, error); + } + } + } + } + + fetchNewPageItems(pagingController, filterArgs); + } else { + console.error("Release failed:", releaseRes.message); + } + } catch (error) { + console.error("Error releasing pick orders:", error); + } finally { + setIsUploading(false); + } +}, [selectedPickOrderIds, filteredPickOrders, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); + + // Update search criteria to match the new data structure + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Pick Order Code"), + paramName: "code", + type: "text", + }, + { + label: t("Group Code"), + paramName: "groupName", + type: "text", + }, + { + label: t("Target Date From"), + label2: t("Target Date To"), + paramName: "targetDate", + type: "dateRange", + }, + ], + [t], + ); + + // Update search function to work with pick order data + const handleSearch = useCallback((query: Record) => { + setSearchQuery({ ...query }); + + const filtered = originalPickOrderData.filter((pickOrder) => { + const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); + + const codeMatch = !query.code || + pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); + + const groupNameMatch = !query.groupName || + pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); + + // Date range search + let dateMatch = true; + if (query.targetDate || query.targetDateTo) { + try { + if (query.targetDate && !query.targetDateTo) { + const fromDate = dayjs(query.targetDate); + dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || + pickOrderTargetDateStr.isAfter(fromDate, 'day'); + } else if (!query.targetDate && query.targetDateTo) { + const toDate = dayjs(query.targetDateTo); + dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || + pickOrderTargetDateStr.isBefore(toDate, 'day'); + } else if (query.targetDate && query.targetDateTo) { + const fromDate = dayjs(query.targetDate); + const toDate = dayjs(query.targetDateTo); + dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || + pickOrderTargetDateStr.isAfter(fromDate, 'day')) && + (pickOrderTargetDateStr.isSame(toDate, 'day') || + pickOrderTargetDateStr.isBefore(toDate, 'day')); + } + } catch (error) { + console.error("Date parsing error:", error); + dateMatch = true; + } + } + + return codeMatch && groupNameMatch && dateMatch; + }); + + setFilteredPickOrders(filtered); + }, [originalPickOrderData]); + + const handleReset = useCallback(() => { + setSearchQuery({}); + setFilteredPickOrders(originalPickOrderData); + setTimeout(() => { + setSearchQuery({}); + }, 0); + }, [originalPickOrderData]); + + // Pagination handlers + const handlePageChange = useCallback((event: unknown, newPage: number) => { + const newPagingController = { + ...pagingController, + pageNum: newPage + 1, + }; + setPagingController(newPagingController); + }, [pagingController]); + + const handlePageSizeChange = useCallback((event: React.ChangeEvent) => { + const newPageSize = parseInt(event.target.value, 10); + const newPagingController = { + pageNum: 1, + pageSize: newPageSize, + }; + setPagingController(newPagingController); + }, []); + + // Component mount effect + useEffect(() => { + fetchNewPageItems(pagingController, filterArgs || {}); + }, []); + + // Dependencies change effect + useEffect(() => { + if (pagingController && (filterArgs || {})) { + fetchNewPageItems(pagingController, filterArgs || {}); + } + }, [pagingController, filterArgs, fetchNewPageItems]); + + useEffect(() => { + const loadUsernameList = async () => { + try { + const res = await fetchNameList(); + if (res) { + setUsernameList(res); + } + } catch (error) { + console.error("Error loading username list:", error); + } + }; + loadUsernameList(); + }, []); + + // Update the table component to work with pick order data directly + const CustomPickOrderTable = () => { + // Helper function to get user name + const getUserName = useCallback((assignToId: number | null | undefined) => { + if (!assignToId) return '-'; + const user = usernameList.find(u => u.id === assignToId); + return user ? user.name : `User ${assignToId}`; + }, [usernameList]); + + return ( + <> + + + + + {t("Selected")} + {t("Pick Order Code")} + {t("Group Code")} + {t("Item Code")} + {t("Item Name")} + {t("Order Quantity")} + {t("Current Stock")} + {t("Stock Unit")} + {t("Target Date")} + {t("Assigned To")} + + + + {filteredPickOrders.length === 0 ? ( + + + + {t("No data available")} + + + + ) : ( + filteredPickOrders.map((pickOrder) => ( + pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => ( + + {/* Checkbox - only show for first line of each pick order */} + + {index === 0 ? ( + handlePickOrderSelect(pickOrder.id, e.target.checked)} + disabled={!isEmpty(pickOrder.consoCode)} + /> + ) : null} + + {/* Pick Order Code - only show for first line */} + + {index === 0 ? pickOrder.code : null} + + {/* Group Name - only show for first line */} + + {index === 0 ? pickOrder.groupName : null} + + {/* Item Code */} + {line.itemCode} + {/* Item Name */} + {line.itemName} + + {/* Order Quantity */} + {line.requiredQty} + + {/* Current Stock */} + + 0 ? "success.main" : "error.main"} + sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} + > + {(line.availableQty || 0).toLocaleString()} + + + + {/* Unit */} + {line.uomDesc} + + {/* Target Date - only show for first line */} + + {index === 0 ? ( + arrayToDayjs(pickOrder.targetDate) + .add(-1, "month") + .format(OUTPUT_DATE_FORMAT) + ) : null} + + + {/* Assigned To - only show for first line */} + + {index === 0 ? ( + + {getUserName(pickOrder.assignTo)} + + ) : null} + + + )) + )) + )} + +
+
+ + + `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` + } + /> + + ); + }; + + return ( + <> + + + + {isLoadingItems ? ( + + ) : ( + + )} + + + + + + + + + ); +}; + +export default AssignTo; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/dummyQcTemplate.tsx b/src/components/FinishedGoodSearch/dummyQcTemplate.tsx new file mode 100644 index 0000000..fa5ff5d --- /dev/null +++ b/src/components/FinishedGoodSearch/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/FinishedGoodSearch/index.ts b/src/components/FinishedGoodSearch/index.ts new file mode 100644 index 0000000..513ba22 --- /dev/null +++ b/src/components/FinishedGoodSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./FinishedGoodSearchWrapper"; diff --git a/src/components/FinishedGoodSearch/newcreatitem copy.tsx b/src/components/FinishedGoodSearch/newcreatitem copy.tsx new file mode 100644 index 0000000..4d876fa --- /dev/null +++ b/src/components/FinishedGoodSearch/newcreatitem copy.tsx @@ -0,0 +1,2234 @@ +"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"; +import VerticalSearchBox from "./VerticalSearchBox"; +import SearchResultsTable from './SearchResultsTable'; +import CreatedItemsTable from './CreatedItemsTable'; +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 NewCreateItem: 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("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]); + + // 1. First, add the checkAndAutoAddItem function (place this BEFORE the other functions) + const checkAndAutoAddItem = useCallback((itemId: number) => { + const item = secondSearchResults.find(i => i.id === itemId); + if (!item) return; + + // Check if item has both group and quantity + const hasGroup = item.groupId !== undefined && item.groupId !== null; + const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0; + + if (hasGroup && hasQty && !isItemInCreated(item.id)) { + // Auto-add to created items + 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]); + + // Remove from search results since it's now in created items + setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); + setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); + } + }, [secondSearchResults, isItemInCreated, targetDate]); + + // 2. Replace the existing handleSecondSearchQtyChange function (around line 999) + const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { + setSecondSearchResults(prev => + prev.map(item => + item.id === itemId ? { ...item, qty: newQty } : item + ) + ); + + // Check if this item should be auto-added + setTimeout(() => { + checkAndAutoAddItem(itemId); + }, 0); +}, [checkAndAutoAddItem]); + + // 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 || "")} + value={c.options?.find(option => option.value === inputs[c.paramName]) || null} + 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); + }, []); + + // 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("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]); + + // 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); + }, []); + + // 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}`}` + } + /> + + ); + }; + + // 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}) + + + {selectedSecondSearchItemIds.length > 0 && ( + + + {t("Selected items will join above created group")} + + + )} + + {isLoadingSecondSearch ? ( + {t("Loading...")} + ) : secondSearchResults.length === 0 ? ( + {t("No results found")} + ) : ( + + )} + +)} + + {/* 创建项目区域 - 修改Group列为可选择的 */} + {createdItems.length > 0 && ( + + + {t("Created Items")} ({createdItems.length}) + + + + +)} + + {/* 操作按钮 */} + + + + + + + ); +}; + +export default NewCreateItem; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/newcreatitem.tsx b/src/components/FinishedGoodSearch/newcreatitem.tsx new file mode 100644 index 0000000..4e6b58c --- /dev/null +++ b/src/components/FinishedGoodSearch/newcreatitem.tsx @@ -0,0 +1,2054 @@ +"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,fetchItemsWithDetails } 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"; +import VerticalSearchBox from "./VerticalSearchBox"; +import SearchResultsTable from './SearchResultsTable'; +import CreatedItemsTable from './CreatedItemsTable'; + +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 ; +} +// Move the counter outside the component to persist across re-renders +let checkboxChangeCallCount = 0; +let processingItems = new Set(); + +const NewCreateItem: 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 { + // Change from fetchAllItemsInClient to fetchItemsWithDetails + const itemsData = await fetchItemsWithDetails(); + console.log("Loaded items:", itemsData); + + // Transform the data to match the expected ItemCombo format + // Fix: Access the records property correctly + const transformedItems: ItemCombo[] = (itemsData as any).records?.map((item: any) => ({ + id: item.id, + label: `${item.code} - ${item.name}`, + uomId: item.uomId, + uom: item.uom, + uomDesc: item.uomDesc, + currentStockBalance: item.currentStockBalance + })) || []; + + setItems(transformedItems); + 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, + currentStockBalance: 0, // Add default value + })); + + 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], + ); + + // Remove the duplicate handleSearch function and keep only this one + + + // 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))); + setSelectedGroup(prev => { + if (prev && prev.id === groupId) { + return { ...prev, targetDate: newTargetDate }; + } + return prev; + }); + // Update selected items that belong to this group + setSecondSearchResults(prev => prev.map(item => + item.groupId === groupId + ? { + ...item, + targetDate: newTargetDate + } + : item + )); + setCreatedItems(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: "" + }; + + 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]); + + const checkAndAutoAddItem = useCallback((itemId: number) => { + const item = secondSearchResults.find(i => i.id === itemId); + if (!item) return; + + // Check if item has ALL 3 conditions: + // 1. Item is selected (checkbox checked) + const isSelected = selectedSecondSearchItemIds.includes(itemId); + // 2. Group is assigned + const hasGroup = item.groupId !== undefined && item.groupId !== null; + // 3. Quantity is entered + const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0; + + if (isSelected && hasGroup && hasQty && !isItemInCreated(item.id)) { + // Auto-add to created items + 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]); + + // Remove from search results since it's now in created items + setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); + setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); + } + }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); + // Add this function after checkAndAutoAddItem + // Add this function after checkAndAutoAddItem +const handleQtyBlur = useCallback((itemId: number) => { + // Only auto-add if item is already selected (scenario 1: select first, then enter quantity) + setTimeout(() => { + const currentItem = secondSearchResults.find(i => i.id === itemId); + if (!currentItem) return; + + const isSelected = selectedSecondSearchItemIds.includes(itemId); + const hasGroup = currentItem.groupId !== undefined && currentItem.groupId !== null; + const hasQty = currentItem.qty !== null && currentItem.qty !== undefined && currentItem.qty > 0; + + // Only auto-add if item is already selected (scenario 1: select first, then enter quantity) + if (isSelected && hasGroup && hasQty && !isItemInCreated(currentItem.id)) { + const newCreatedItem: CreatedItem = { + itemId: currentItem.id, + itemName: currentItem.label, + itemCode: currentItem.label, + qty: currentItem.qty || 1, + uom: currentItem.uom || "", + uomId: currentItem.uomId || 0, + uomDesc: currentItem.uomDesc || "", + isSelected: true, + currentStockBalance: currentItem.currentStockBalance, + targetDate: currentItem.targetDate || targetDate, + groupId: currentItem.groupId || undefined, + }; + setCreatedItems(prev => [...prev, newCreatedItem]); + + setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); + setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); + } + }, 0); +}, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); + const handleSearchItemGroupChange = useCallback((itemId: number, groupId: string) => { + const gid = groupId ? Number(groupId) : undefined; + const group = groups.find(g => g.id === gid); + + setSecondSearchResults(prev => prev.map(item => + item.id === itemId + ? { + ...item, + groupId: gid, + targetDate: group?.targetDate || undefined + } + : item + )); + + // Check auto-add after group assignment + setTimeout(() => { + checkAndAutoAddItem(itemId); + }, 0); + }, [groups, checkAndAutoAddItem]); + // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group) + const handleSearch = useCallback(() => { + 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 }); + + // Use the new API with search parameters + const searchParams: Record = {}; + + if (searchCode && searchCode.trim()) { + searchParams.code = searchCode.trim(); + } + + if (searchName && searchName.trim()) { + searchParams.name = searchName.trim(); + } + + if (type && type.trim()) { + searchParams.type = type.trim(); + } + + // Add pagination parameters + searchParams.pageSize = 100; + searchParams.pageNum = 1; + + // Fetch items using the new API + fetchItemsWithDetails(searchParams) + .then(response => { + try { + // Fix: Handle the response type correctly and safely + let itemsToTransform: any[] = []; + + // Safely check and extract data from response + if (response && typeof response === 'object') { + if ('records' in response && Array.isArray((response as any).records)) { + itemsToTransform = (response as any).records; + } else if (Array.isArray(response)) { + itemsToTransform = response; + } + } + + const transformedItems: SearchItemWithQty[] = itemsToTransform.map((item: any) => ({ + id: item.id, + label: `${item.code} - ${item.name}`, + uomId: item.uomId, + uom: item.uom, + uomDesc: item.uomDesc, + currentStockBalance: item.currentStockBalance, + qty: null, + targetDate: targetDate, + })); + + console.log("Search results:", transformedItems.length); + setFilteredItems(transformedItems); + } catch (error) { + console.error("Error processing response:", error); + setFilteredItems([]); + } finally { + setIsLoading(false); + } + }) + .catch(error => { + console.error("Error searching items:", error); + setFilteredItems([]); + setIsLoading(false); + }); + }, [type, searchCode, searchName, 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; + } + + // ✅ 修复:自动填充 type 为 "Consumable",不再强制用户选择 + // if (!data.type) { + // alert(t("Please select product type")); + // 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; + } + } + + // ✅ 修复:自动使用 "Consumable" 作为默认 type + const pickOrderData: SavePickOrderRequest = { + type: data.type || "Consumable", // 如果用户选择了 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&& 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?.toLocaleString() } + + ); + }, + }, + { + 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("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 + ) + ); + + // Don't auto-add here - only on blur event + }, []); + + // 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)) { + handleSearchItemSelect(item.id, true); + } + }); + } else { + // 部分选择:只处理当前页面的选择 + secondSearchResults.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 = 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)) { + handleSearchItemSelect(id as number, true); + } + }); + + newlyDeselected.forEach(id => { + setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); + }); + } + }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]); + + // 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?.toLocaleString() } + + + ); + }, + }, + { + 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) { + 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); + } + + // Build search parameters for the new API + const searchParams: Record = {}; + + if (query.code && query.code.trim()) { + searchParams.code = query.code.trim(); + } + + if (query.name && query.name.trim()) { + searchParams.name = query.name.trim(); + } + + if (query.type && query.type !== "All") { + searchParams.type = query.type; + } + + // Add pagination parameters + searchParams.pageSize = 100; + searchParams.pageNum = 1; + + // Use the new API + fetchItemsWithDetails(searchParams) + .then(response => { + try { + // Fix: Handle the response type correctly and safely + let itemsToTransform: any[] = []; + + // Safely check and extract data from response + if (response && typeof response === 'object') { + if ('records' in response && Array.isArray((response as any).records)) { + itemsToTransform = (response as any).records; + } else if (Array.isArray(response)) { + itemsToTransform = response; + } + } + + const transformedItems: SearchItemWithQty[] = itemsToTransform.map((item: any) => ({ + id: item.id, + label: `${item.code} - ${item.name}`, + uomId: item.uomId, + uom: item.uom, + uomDesc: item.uomDesc, + currentStockBalance: item.currentStockBalance, + qty: null, + targetDate: undefined, + groupId: undefined, + })); + + setSecondSearchResults(transformedItems); + setHasSearchedSecond(true); + } catch (error) { + console.error("Error processing response:", error); + setSecondSearchResults([]); + } finally { + setIsLoadingSecondSearch(false); + } + }) + .catch(error => { + console.error("Error in second search:", error); + setSecondSearchResults([]); + setIsLoadingSecondSearch(false); + }); + }, [formProps, t]); +/* + // 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 || "")} + value={c.options?.find(option => option.value === inputs[c.paramName]) || null} + 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) => { + checkboxChangeCallCount++; + + if (checked) { + // Add to selected IDs + setSelectedSecondSearchItemIds(prev => [...prev, itemId]); + + // Set the item's group and targetDate to current group when selected + setSecondSearchResults(prev => { + const updatedResults = prev.map(item => + item.id === itemId + ? { + ...item, + groupId: selectedGroup?.id || undefined, + targetDate: selectedGroup?.targetDate !== undefined && selectedGroup?.targetDate !== "" ? selectedGroup.targetDate : undefined + } + : item + ); + + // Check if should auto-add after state update + setTimeout(() => { + // Check if we're already processing this item + if (processingItems.has(itemId)) { + //alert(`Item ${itemId} is already being processed, skipping duplicate auto-add`); + return; + } + + const updatedItem = updatedResults.find(i => i.id === itemId); + if (updatedItem) { + const isSelected = true; // We just selected it + const hasGroup = updatedItem.groupId !== undefined && updatedItem.groupId !== null; + const hasQty = updatedItem.qty !== null && updatedItem.qty !== undefined && updatedItem.qty > 0; + + // Only auto-add if item has quantity (scenario 2: enter quantity first, then select) + if (isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)) { + // Mark this item as being processed + processingItems.add(itemId); + + const newCreatedItem: CreatedItem = { + itemId: updatedItem.id, + itemName: updatedItem.label, + itemCode: updatedItem.label, + qty: updatedItem.qty || 1, + uom: updatedItem.uom || "", + uomId: updatedItem.uomId || 0, + uomDesc: updatedItem.uomDesc || "", + isSelected: true, + currentStockBalance: updatedItem.currentStockBalance, + targetDate: updatedItem.targetDate || targetDate, + groupId: updatedItem.groupId || undefined, + }; + setCreatedItems(prev => [...prev, newCreatedItem]); + + setSecondSearchResults(current => current.filter(searchItem => searchItem.id !== itemId)); + setSelectedSecondSearchItemIds(current => current.filter(id => id !== itemId)); + + // Remove from processing set after a short delay + setTimeout(() => { + processingItems.delete(itemId); + }, 100); + } + + // Show final debug info in one alert + /* + alert(`FINAL DEBUG INFO for item ${itemId}: +Function called ${checkboxChangeCallCount} times +Is Selected: ${isSelected} +Has Group: ${hasGroup} +Has Quantity: ${hasQty} +Quantity: ${updatedItem.qty} +Group ID: ${updatedItem.groupId} +Is Item In Created: ${isItemInCreated(updatedItem.id)} +Auto-add triggered: ${isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)} +Processing items: ${Array.from(processingItems).join(', ')}`); + */ + } + }, 0); + + return updatedResults; + }); + } else { + // Remove from selected IDs + 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, isItemInCreated, targetDate]); + +// 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 && selectedGroup.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.label.split(' - ')[1] || item.label} + + + {item.label.split(' - ')[0] || ''} + + + + + + + + {(() => { + if (item.groupId) { + const group = groups.find(g => g.id === item.groupId); + return group?.name || "-"; + } + return "-"; // Show "-" for unselected items + })()} + + + + + + 0 ? "success.main" : "error.main"} + sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }} + > + {item.currentStockBalance || 0} + + + + + + + {item.uomDesc || "-"} + + + + + { + const value = e.target.value; + // Only allow numbers + if (value === "" || /^\d+$/.test(value)) { + const numValue = value === "" ? null : Number(value); + handleSecondSearchQtyChange(item.id, numValue); + } + }} + onBlur={() => { + // Trigger auto-add check when user finishes input + handleQtyBlur(item.id); + }} + inputProps={{ + 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}`}` + } + /> + + ); +}; +*/ + + // 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}) + + + {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 */} +{/* + + {hasSearchedSecond && secondSearchResults.length > 0 && selectedSecondSearchItemIds.length > 0 && ( + + + + + {selectedSecondSearchItemIds.length > 0 && !areSelectedItemsValid() && ( + + {getValidationMessage()} + + )} + + + + )} + */} + + {/* 创建项目区域 - 修改Group列为可选择的 */} + {createdItems.length > 0 && ( + + + {t("Created Items")} ({createdItems.length}) + + + + +)} + + {/* 操作按钮 */} + + + + + + + ); +}; + +export default NewCreateItem; \ No newline at end of file diff --git a/src/components/FinishedGoodSearch/pickorderModelVer2.tsx b/src/components/FinishedGoodSearch/pickorderModelVer2.tsx new file mode 100644 index 0000000..1294f1d --- /dev/null +++ b/src/components/FinishedGoodSearch/pickorderModelVer2.tsx @@ -0,0 +1,380 @@ +"use client"; +// 修改为 PickOrder 相关的导入 +import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { PurchaseQcResult } from "@/app/api/po/actions"; +import { + Box, + Button, + Grid, + Modal, + ModalProps, + Stack, + Typography, +} from "@mui/material"; +import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; +import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import StockInFormVer2 from "./StockInFormVer2"; +import QcFormVer2 from "./QcFormVer2"; +import PutawayForm from "./PutawayForm"; +import { dummyPutawayLine, dummyQCData, QcData } from "./dummyQcTemplate"; +import { useGridApiRef } from "@mui/x-data-grid"; +import {submitDialogWithWarning} from "../Swal/CustomAlerts"; + +const style = { + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + bgcolor: "background.paper", + pt: 5, + px: 5, + pb: 10, + display: "block", + width: { xs: "60%", sm: "60%", md: "60%" }, + // height: { xs: "60%", sm: "60%", md: "60%" }, +}; +// 修改接口定义 +interface CommonProps extends Omit { + itemDetail: GetPickOrderLineInfo & { + pickOrderCode: string; + qcResult?: PurchaseQcResult[] + }; + setItemDetail: Dispatch< + SetStateAction< + | (GetPickOrderLineInfo & { + pickOrderCode: string; + warehouseId?: number; + }) + | undefined + > + >; + qc?: QcItemWithChecks[]; + warehouse?: any[]; +} + +interface Props extends CommonProps { + itemDetail: GetPickOrderLineInfo & { + pickOrderCode: string; + qcResult?: PurchaseQcResult[] + }; +} + +// 修改组件名称 +const PickQcStockInModalVer2: React.FC = ({ + open, + onClose, + itemDetail, + setItemDetail, + qc, + warehouse, +}) => { + console.log(warehouse); + // 修改翻译键 + const { + t, + i18n: { language }, + } = useTranslation("pickOrder"); +const [qcItems, setQcItems] = useState(dummyQCData) + const formProps = useForm({ + defaultValues: { + ...itemDetail, + putAwayLine: dummyPutawayLine, + // receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT), + // warehouseId: itemDetail.defaultWarehouseId || 0 + }, + }); + const closeHandler = useCallback>( + (...args) => { + onClose?.(...args); + // reset(); + }, + [onClose], + ); + const [openPutaway, setOpenPutaway] = useState(false); + const onOpenPutaway = useCallback(() => { + setOpenPutaway(true); + }, []); + const onClosePutaway = useCallback(() => { + setOpenPutaway(false); + }, []); + // Stock In submission handler + const onSubmitStockIn = useCallback>( + async (data, event) => { + console.log("Stock In Submission:", event!.nativeEvent); + // Extract only stock-in related fields + const stockInData = { + // quantity: data.quantity, + // receiptDate: data.receiptDate, + // batchNumber: data.batchNumber, + // expiryDate: data.expiryDate, + // warehouseId: data.warehouseId, + // location: data.location, + // unitCost: data.unitCost, + data: data, + // Add other stock-in specific fields from your form + }; + console.log("Stock In Data:", stockInData); + // Handle stock-in submission logic here + // e.g., call API, update state, etc. + }, + [], + ); + // QC submission handler + const onSubmitQc = useCallback>( + async (data, event) => { + console.log("QC Submission:", event!.nativeEvent); + + // Get QC data from the shared form context + const qcAccept = data.qcAccept; + const acceptQty = data.acceptQty; + + // Validate QC data + const validationErrors : string[] = []; + // Check if all QC items have results + const itemsWithoutResult = qcItems.filter(item => item.isPassed === undefined); + if (itemsWithoutResult.length > 0) { + validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.qcItem).join(', ')}`); + } + + // Check if failed items have failed quantity + const failedItemsWithoutQty = qcItems.filter(item => + item.isPassed === false && (!item.failedQty || item.failedQty <= 0) + ); + if (failedItemsWithoutQty.length > 0) { + validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`); + } + + // Check if QC accept decision is made + // if (qcAccept === undefined) { + // validationErrors.push("QC accept/reject decision is required"); + // } + + // Check if accept quantity is valid + if (acceptQty === undefined || acceptQty <= 0) { + validationErrors.push("Accept quantity must be greater than 0"); + } + + if (validationErrors.length > 0) { + console.error("QC Validation failed:", validationErrors); + alert(`未完成品檢: ${validationErrors}`); + return; + } + + const qcData = { + qcAccept: qcAccept, + acceptQty: acceptQty, + qcItems: qcItems.map(item => ({ + id: item.id, + qcItem: item.qcItem, + qcDescription: item.qcDescription, + isPassed: item.isPassed, + failedQty: (item.failedQty && !item.isPassed) || 0, + remarks: item.remarks || '' + })) + }; + // const qcData = data; + + console.log("QC Data for submission:", qcData); + // await submitQcData(qcData); + + if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { + submitDialogWithWarning(onOpenPutaway, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); + return; + } + + if (qcData.qcAccept) { + onOpenPutaway(); + } else { + onClose(); + } + }, + [onOpenPutaway, qcItems], + ); + // Email supplier handler + const onSubmitEmailSupplier = useCallback>( + async (data, event) => { + console.log("Email Supplier Submission:", event!.nativeEvent); + // Extract only email supplier related fields + const emailData = { + // supplierEmail: data.supplierEmail, + // issueDescription: data.issueDescription, + // qcComments: data.qcComments, + // defectNotes: data.defectNotes, + // attachments: data.attachments, + // escalationReason: data.escalationReason, + data: data, + + // Add other email-specific fields + }; + console.log("Email Supplier Data:", emailData); + // Handle email supplier logic here + // e.g., send email to supplier, log escalation, etc. + }, + [], + ); + // Putaway submission handler + const onSubmitPutaway = useCallback>( + async (data, event) => { + console.log("Putaway Submission:", event!.nativeEvent); + // Extract only putaway related fields + const putawayData = { + // putawayLine: data.putawayLine, + // putawayLocation: data.putawayLocation, + // binLocation: data.binLocation, + // putawayQuantity: data.putawayQuantity, + // putawayNotes: data.putawayNotes, + data: data, + + // Add other putaway specific fields + }; + console.log("Putaway Data:", putawayData); + // Handle putaway submission logic here + // Close modal after successful putaway + closeHandler({}, "backdropClick"); + }, + [closeHandler], + ); + // Print handler + const onPrint = useCallback(() => { + console.log("Print putaway documents"); + // Handle print logic here + window.print(); + }, []); + const acceptQty = formProps.watch("acceptedQty") + + const checkQcIsPassed = useCallback((qcItems: QcData[]) => { + const isPassed = qcItems.every((qc) => qc.isPassed); + console.log(isPassed) + if (isPassed) { + formProps.setValue("passingQty", acceptQty) + } else { + formProps.setValue("passingQty", 0) + } + return isPassed + }, [acceptQty, formProps]) + + useEffect(() => { + // maybe check if submitted before + console.log(qcItems) + checkQcIsPassed(qcItems) + }, [qcItems, checkQcIsPassed]) + + return ( + <> + + + + {openPutaway ? ( + + + + + + + + ) : ( + <> + + + + {t("qc processing")} + + + + + + + + + + + + + + + + + + )} + + + + + ); +}; +export default PickQcStockInModalVer2; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index d4b216e..90d17a1 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -84,6 +84,11 @@ const NavigationContent: React.FC = () => { label: "Put Away Scan", path: "/putAway", }, + { + icon: , + label: "Finished Good", + path: "/finishedGood", + }, ], }, // { diff --git a/src/components/PickOrderSearch/PickExecution.tsx b/src/components/PickOrderSearch/PickExecution.tsx index 3b982cf..0e15fb5 100644 --- a/src/components/PickOrderSearch/PickExecution.tsx +++ b/src/components/PickOrderSearch/PickExecution.tsx @@ -70,6 +70,8 @@ import { dummyQCData } from "../PoDetail/dummyQcTemplate"; import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; import LotTable from './LotTable'; import { updateInventoryLotLineStatus, updateInventoryStatus, updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; +import { useSession } from "next-auth/react"; // ✅ Add session import +import { SessionWithTokens } from "@/config/authConfig"; // ✅ Add custom session type interface Props { filterArgs: Record; @@ -101,6 +103,11 @@ interface PickQtyData { const PickExecution: React.FC = ({ filterArgs }) => { const { t } = useTranslation("pickOrder"); const router = useRouter(); + const { data: session } = useSession() as { data: SessionWithTokens | null }; // ✅ Add session + + // ✅ Get current user ID from session with proper typing + const currentUserId = session?.id ? parseInt(session.id) : undefined; + const [filteredPickOrders, setFilteredPickOrders] = useState( [] as ConsoPickOrderResult[], ); @@ -252,10 +259,11 @@ const PickExecution: React.FC = ({ filterArgs }) => { const handleFetchAllPickOrderDetails = useCallback(async () => { setDetailLoading(true); try { - const data = await fetchAllPickOrderDetails(); + // ✅ Use current user ID for filtering + const data = await fetchAllPickOrderDetails(currentUserId); setPickOrderDetails(data); setOriginalPickOrderData(data); // Store original data for filtering - console.log("All Pick Order Details:", data); + console.log("All Pick Order Details for user:", currentUserId, data); const initialPickQtyData: PickQtyData = {}; data.pickOrders.forEach((pickOrder: any) => { @@ -270,7 +278,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { } finally { setDetailLoading(false); } - }, []); + }, [currentUserId]); // ✅ Add currentUserId as dependency useEffect(() => { handleFetchAllPickOrderDetails(); diff --git a/src/i18n/zh/pickOrder.json b/src/i18n/zh/pickOrder.json index 8329f98..b2d82c3 100644 --- a/src/i18n/zh/pickOrder.json +++ b/src/i18n/zh/pickOrder.json @@ -183,5 +183,9 @@ "Item lot to be Pick:": "批次貨品提料:", "Report and Pick another lot": "上報並需重新選擇批號", "Accept Stock Out": "接受出庫", - "Pick Another Lot": "重新選擇批號" + "Pick Another Lot": "重新選擇批號", + "Lot No": "批號", + "Expiry Date": "到期日", + "Location": "位置" + } \ No newline at end of file