diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 9e5ca11..7395a53 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -81,6 +81,93 @@ export interface PickOrderApprovalInput { rejectQty: number; status: string; } + + +export interface GetPickOrderInfoResponse { + pickOrders: GetPickOrderInfo[]; + items: CurrentInventoryItemInfo[]; +} + +export interface GetPickOrderInfo { + id: number; + code: string; + targetDate: string; + type: string; + status: string; + pickOrderLines: GetPickOrderLineInfo[]; +} + +export interface GetPickOrderLineInfo { + id: number; + itemId: number; + itemCode: string; + itemName: string; + availableQty: number; + requiredQty: number; + uomCode: string; + uomDesc: string; + suggestedList: any[]; +} + +export interface CurrentInventoryItemInfo { + id: number; + code: string; + name: string; + uomDesc: string; + availableQty: number; + requiredQty: number; +} + + +export const fetchPickOrderDetails = cache(async (ids: string) => { + return serverFetchJson( + `${BASE_API_URL}/pickOrder/detail/${ids}`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); +}); +export interface PickOrderLotDetailResponse { + lotId: number; + lotNo: string; + expiryDate: string; + location: string; + stockUnit: string; + availableQty: number; + requiredQty: number; + actualPickQty: number; + suggestedPickLotId: number; + lotStatus: string; + lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; +} + + +export const fetchAllPickOrderDetails = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/pickOrder/detail`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); +}); + +export const fetchPickOrderLineLotDetails = cache(async (pickOrderLineId: number) => { + return serverFetchJson( + `${BASE_API_URL}/pickOrder/lot-details/${pickOrderLineId}`, + { + method: "GET", + next: { tags: ["pickorder"] }, + }, + ); +}); + + + + + + export const createPickOrder = async (data: SavePickOrderRequest) => { console.log(data); const po = await serverFetchJson( diff --git a/src/components/PickOrderSearch/EscalationComponent.tsx b/src/components/PickOrderSearch/EscalationComponent.tsx new file mode 100644 index 0000000..53761a8 --- /dev/null +++ b/src/components/PickOrderSearch/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/PickOrderSearch/PickExecution.tsx b/src/components/PickOrderSearch/PickExecution.tsx new file mode 100644 index 0000000..60c92dc --- /dev/null +++ b/src/components/PickOrderSearch/PickExecution.tsx @@ -0,0 +1,795 @@ +"use client"; + +import { + Autocomplete, + Box, + Button, + CircularProgress, + FormControl, + Grid, + Paper, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, + Checkbox, + FormControlLabel, + Select, + MenuItem, + InputLabel, +} from "@mui/material"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ByItemsSummary, + ConsoPickOrderResult, + PickOrderLine, + PickOrderResult, +} from "@/app/api/pickOrder"; +import { useRouter } from "next/navigation"; +import { GridInputRowSelectionModel } from "@mui/x-data-grid"; +import { + fetchConsoDetail, + fetchConsoPickOrderClient, + releasePickOrder, + ReleasePickOrderInputs, + fetchPickOrderDetails, + fetchAllPickOrderDetails, + GetPickOrderInfoResponse, + GetPickOrderLineInfo, +} from "@/app/api/pickOrder/actions"; +import { EditNote } from "@mui/icons-material"; +import { fetchNameList, NameList } from "@/app/api/user/actions"; +import { + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { pickOrderStatusMap } from "@/app/utils/formatUtil"; +import { QcItemWithChecks } from "@/app/api/qc"; +import { fetchQcItemCheck, fetchPickOrderQcResult } from "@/app/api/qc/actions"; + +import { PurchaseQcResult } from "@/app/api/po/actions"; +import PickQcStockInModalVer2 from "./PickQcStockInModalVer3"; +import { fetchPickOrderLineLotDetails, PickOrderLotDetailResponse } from "@/app/api/pickOrder/actions"; +import SearchResults, { Column } from "../SearchResults/SearchResults"; +import { defaultPagingController } from "../SearchResults/SearchResults"; +interface Props { + filterArgs: Record; +} + + +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'; +} + +interface PickQtyData { + [lineId: number]: { + [lotId: number]: number; + }; +} + +const PickExecution: React.FC = ({ filterArgs }) => { + const { t } = useTranslation("pickOrder"); + const router = useRouter(); + const [filteredPickOrders, setFilteredPickOrders] = useState( + [] as ConsoPickOrderResult[], + ); + const [isLoading, setIsLoading] = useState(false); + const [selectedConsoCode, setSelectedConsoCode] = useState(); + 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 [selectedRowId, setSelectedRowId] = useState(null); + + + const [pickOrderDetails, setPickOrderDetails] = useState(null); + const [detailLoading, setDetailLoading] = useState(false); + + + const [pickQtyData, setPickQtyData] = useState({}); + const [lotData, setLotData] = useState([]); + + + const [qcItems, setQcItems] = useState([]); + const [qcModalOpen, setQcModalOpen] = useState(false); + const [selectedItemForQc, setSelectedItemForQc] = useState(null); + + const formProps = useForm(); + const errors = formProps.formState.errors; + + const onDetailClick = useCallback( + (pickOrder: any) => { + console.log(pickOrder); + const status = pickOrder.status; + if (pickOrderStatusMap[status] >= 3) { + router.push(`/pickOrder/detail?consoCode=${pickOrder.consoCode}`); + } else { + setSelectedConsoCode(pickOrder.consoCode); + } + }, + [router], + ); + + 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({ limit: 10, offset: 0 }, filterArgs); + }, [fetchNewPageConsoPickOrder, 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); + setByItemsRows(res.items); + setDisableRelease(isReleasable(res.items)); + } else { + console.log("error"); + console.log(res); + } + if (nameListRes) { + console.log(nameListRes); + setUsernameList(nameListRes); + } + }, + [isReleasable], + ); + + + const handleFetchAllPickOrderDetails = useCallback(async () => { + setDetailLoading(true); + try { + const data = await fetchAllPickOrderDetails(); + setPickOrderDetails(data); + console.log("All Pick Order Details:", data); + + + const initialPickQtyData: PickQtyData = {}; + data.pickOrders.forEach((pickOrder: any) => { + pickOrder.pickOrderLines.forEach((line: any) => { + initialPickQtyData[line.id] = {}; + }); + }); + setPickQtyData(initialPickQtyData); + + } catch (error) { + console.error("Error fetching all pick order details:", error); + } finally { + setDetailLoading(false); + } + }, []); + + + useEffect(() => { + handleFetchAllPickOrderDetails(); + }, [handleFetchAllPickOrderDetails]); + + 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 (selectedConsoCode) { + fetchConso(selectedConsoCode); + formProps.setValue("consoCode", selectedConsoCode); + } + }, [selectedConsoCode, fetchConso, formProps]); + + + const handlePickQtyChange = useCallback((lineId: number, lotId: number, value: number) => { + setPickQtyData(prev => ({ + ...prev, + [lineId]: { + ...prev[lineId], + [lotId]: value + } + })); + }, []); + + const handleSubmitPickQty = useCallback((lineId: number, lotId: number) => { + const qty = pickQtyData[lineId]?.[lotId] || 0; + console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`); + + }, [pickQtyData]); + + + const getTotalPickedQty = useCallback((lineId: number) => { + const lineData = pickQtyData[lineId]; + if (!lineData) return 0; + return Object.values(lineData).reduce((sum, qty) => sum + qty, 0); + }, [pickQtyData]); + + + const handleInsufficientStock = useCallback(() => { + console.log("Insufficient stock - need to pick another lot"); + alert("Insufficient stock - need to pick another lot"); + }, []); + + + const handleQcCheck = useCallback(async (line: GetPickOrderLineInfo, pickOrderCode: string) => { + console.log("QC Check clicked for:", line, pickOrderCode); + try { + + const qcItemsData = await fetchQcItemCheck(line.itemId); + setQcItems(qcItemsData); + console.log("QC Items:", qcItemsData); + + + let qcResult: PurchaseQcResult[] = []; + try { + qcResult = await fetchPickOrderQcResult(line.id); + console.log("QC Result:", qcResult); + } catch (error) { + console.log("No existing QC result found"); + } + + setSelectedItemForQc({ + ...line, + pickOrderCode, + qcResult + }); + setQcModalOpen(true); + console.log("QC Modal should open now"); + } catch (error) { + console.error("Error fetching QC data:", error); + } + }, []); + + + const handleCloseQcModal = useCallback(() => { + console.log("Closing QC modal"); + setQcModalOpen(false); + setSelectedItemForQc(null); + }, []); + + + const handleSetItemDetail = useCallback((item: any) => { + setSelectedItemForQc(item); + }, []); + + + const renderMainTableRow = useCallback((line: GetPickOrderLineInfo, pickOrderCode: string) => { + const handleRowSelect = async (event: React.ChangeEvent) => { + if (event.target.checked) { + setSelectedRowId(line.id); + + try { + const lotDetails = await fetchPickOrderLineLotDetails(line.id); + console.log("Lot details from API:", lotDetails); + + + const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ + id: lot.lotId, // Add id here + lotId: lot.lotId, + lotNo: lot.lotNo, + expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', + location: lot.location, + stockUnit: lot.stockUnit, + availableQty: lot.availableQty, + requiredQty: lot.requiredQty, + actualPickQty: lot.actualPickQty || 0, + lotStatus: lot.lotStatus, + lotAvailability: lot.lotAvailability + })); + + setLotData(realLotData); + } catch (error) { + console.error("Error fetching lot details:", error); + + setLotData([]); + } + } else { + setSelectedRowId(null); + setLotData([]); + } + }; + + // Calculate Balance to Pick (availableQty - requiredQty) + const balanceToPick = line.availableQty - line.requiredQty; + const totalPickedQty = getTotalPickedQty(line.id); + + return ( + *": { borderBottom: "unset" }, + color: "black", + backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit", + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }} + > + + e.stopPropagation()} + /> + + {pickOrderCode} + {line.itemCode} + {line.itemName} + {line.requiredQty} + = 0 ? 'success.main' : 'error.main', + //fontWeight: 'bold' + }}> + {balanceToPick} + + {totalPickedQty} + + ); + }, [selectedRowId, getTotalPickedQty]); + + + const selectedRow = useMemo(() => { + if (!selectedRowId || !pickOrderDetails) return null; + + // 在所有 pick order lines 中查找选中的行 + for (const pickOrder of pickOrderDetails.pickOrders) { + const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); + if (foundLine) { + return { ...foundLine, pickOrderCode: pickOrder.code }; + } + } + return null; + }, [selectedRowId, pickOrderDetails]); + + // Add these state variables after the existing useState declarations + const [area4PagingController, setArea4PagingController] = useState(defaultPagingController); + const [area5PagingController, setArea5PagingController] = useState(defaultPagingController); + const [selectedLotIds, setSelectedLotIds] = useState([]); + + // Add these helper functions + const getPaginatedData = useCallback((data: any[], pagingController: any) => { + const startIndex = pagingController.pageNum * pagingController.pageSize; + const endIndex = startIndex + pagingController.pageSize; + return data.slice(startIndex, endIndex); + }, []); + + const getTotalPages = useCallback((totalCount: number, pageSize: number) => { + return Math.ceil(totalCount / pageSize); + }, []); + + // Add this useEffect to reset pagination when data changes + useEffect(() => { + setArea4PagingController({ pageNum: 0, pageSize: 10 }); + }, [pickOrderDetails]); + + useEffect(() => { + setArea5PagingController({ pageNum: 0, pageSize: 10 }); + }, [lotData]); + + // Add this function to handle row selection + const handleRowSelect = useCallback(async (lineId: number) => { + setSelectedRowId(lineId); + + // Get real lot data + try { + const lotDetails = await fetchPickOrderLineLotDetails(lineId); + console.log("Lot details from API:", lotDetails); + + const realLotData: LotPickData[] = lotDetails.map((lot: any) => ({ + id: lot.lotId, // Add this line + lotId: lot.lotId, + lotNo: lot.lotNo, + expiryDate: lot.expiryDate ? new Date(lot.expiryDate).toLocaleDateString() : 'N/A', + location: lot.location, + stockUnit: lot.stockUnit, + availableQty: lot.availableQty, + requiredQty: lot.requiredQty, + actualPickQty: lot.actualPickQty || 0, + lotStatus: lot.lotStatus, + lotAvailability: lot.lotAvailability + })); + + setLotData(realLotData); + } catch (error) { + console.error("Error fetching lot details:", error); + setLotData([]); + } + }, []); + + const prepareArea4Data = useMemo(() => { + if (!pickOrderDetails) return []; + + return pickOrderDetails.pickOrders.flatMap((pickOrder) => + pickOrder.pickOrderLines.map((line) => ({ + ...line, + pickOrderCode: pickOrder.code, + balanceToPick: line.availableQty - line.requiredQty, + })) + ); + }, [pickOrderDetails]); + + const prepareArea5Data = useMemo(() => { + return lotData.map((lot) => ({ + ...lot, + id: lot.lotId, // Add id field for SearchResults + })); + }, [lotData]); + + const area4Columns = useMemo[]>( + () => [ + { + name: "select", + label: "", + type: "checkbox", + disabled: () => false, // Allow all rows to be selectable + }, + { + name: "pickOrderCode", + label: t("Pick Order#"), + }, + { + name: "itemCode", + label: t("Item Code"), + }, + { + name: "itemName", + label: t("Item Description"), + }, + { + name: "requiredQty", + label: t("Required Qty"), + + }, + { + name: "balanceToPick", + label: t("Balance to Pick"), + + renderCell: (params) => { + const balanceToPick = params.availableQty - params.requiredQty; + return ( + = 0 ? 'success.main' : 'error.main', + //fontWeight: 'bold' + }} + > + {balanceToPick} + + ); + }, + }, + { + name: "qtyAlreadyPicked", + label: t("Qty Already Picked"), + + renderCell: (params) => { + return getTotalPickedQty(params.id); + }, + }, + ], + [t, getTotalPickedQty], + ); + + const area5Columns = useMemo[]>( + () => [ + { + name: "id", // Use "id" instead of "select" + label: "", + type: "checkbox", + disabled: () => false, + }, + { + name: "lotNo", + label: t("Lot#"), + renderCell: (params) => ( + + {params.lotNo} + {params.lotAvailability !== 'available' && ( + + ({params.lotAvailability === 'expired' ? 'Expired' : + params.lotAvailability === 'insufficient_stock' ? 'Insufficient' : + 'Unavailable'}) + + )} + + ), + }, + { + name: "expiryDate", + label: t("Lot ExpiryDate"), + }, + { + name: "location", + label: t("Lot Location"), + }, + { + name: "stockUnit", + label: t("Stock Unit"), + }, + { + name: "availableQty", + label: t("Available Lot"), + align: "right", + }, + { + name: "requiredQty", + label: t("Lot Required Pick Qty"), + align: "right", + }, + { + name: "actualPickQty", + label: t("Lot Actual Pick Qty"), + align: "right", + renderCell: (params) => ( + { + if (selectedRowId) { + handlePickQtyChange( + selectedRowId, + params.lotId, + parseInt(e.target.value) || 0 + ); + } + }} + inputProps={{ min: 0, max: params.availableQty }} + disabled={params.lotAvailability !== 'available'} + sx={{ width: '80px' }} + /> + ), + }, + { + name: "lotId", + label: t("Submit"), + align: "center", + renderCell: (params) => ( + + ), + }, + ], + [t, selectedRowId, pickQtyData, handlePickQtyChange, handleSubmitPickQty], + ); + + return ( + + + {/* Area 4 & 5: Main Table and Detail Side by Side */} + + {/* Area 4: Main Table */} + + + {t("Pick Order Details")} + + {detailLoading ? ( + + + + ) : pickOrderDetails ? ( + <> + + + items={prepareArea4Data} + columns={area4Columns} + pagingController={area4PagingController} + setPagingController={setArea4PagingController} + totalCount={prepareArea4Data.length} + checkboxIds={selectedRowId ? [selectedRowId] : []} + setCheckboxIds={(ids) => { + const newSelectedId = ids[0] as number || null; + setSelectedRowId(newSelectedId); + + // Handle lot data fetching when selection changes + if (newSelectedId) { + handleRowSelect(newSelectedId); + } else { + setLotData([]); + } + }} + /> + + + + ) : ( + + + 正在載入數據... + + + )} + + + {/* Area 5: Item lot to be Pick */} + + {selectedRow && lotData.length > 0 ? ( + <> + + Item lot to be Pick: {selectedRow.pickOrderCode} - {selectedRow.itemName} + + + items={prepareArea5Data} + columns={area5Columns} + pagingController={area5PagingController} + setPagingController={setArea5PagingController} + totalCount={prepareArea5Data.length} + checkboxIds={selectedLotIds} + setCheckboxIds={(ids) => { + setSelectedLotIds(Array.isArray(ids) ? ids as number[] : []); + }} + /> + + {/* Action buttons below the table */} + + + + + + + + ) : ( + + + Please select an pick order lot to view lot information + + + )} + + + + {/* Area 6: Action Buttons */} + {selectedRow && ( + + + + + + + )} + + {/* QC Modal */} + {selectedItemForQc && qcModalOpen && ( + + )} + + + ); +}; + +export default PickExecution; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PickOrderSearch.tsx b/src/components/PickOrderSearch/PickOrderSearch.tsx index 8dc3bb5..92991dc 100644 --- a/src/components/PickOrderSearch/PickOrderSearch.tsx +++ b/src/components/PickOrderSearch/PickOrderSearch.tsx @@ -18,7 +18,9 @@ import { import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; import PickOrders from "./PickOrders"; import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; +import PickExecution from "./PickExecution"; import CreatePickOrderModal from "./CreatePickOrderModal"; +import NewCreateItem from "./newcreatitem"; import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; @@ -39,6 +41,7 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { 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(); @@ -48,6 +51,7 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { }, [], ); + const openCreateModal = useCallback(async () => { console.log("testing") const res = await fetchAllItemsInClient() @@ -60,69 +64,123 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { 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( - () => [ - { label: t("Code"), paramName: "code", type: "text" }, - { - label: t("Target Date From"), - label2: t("Target Date To"), - paramName: "targetDate", - type: "dateRange", - }, - { - label: t("Type"), - paramName: "type", - type: "autocomplete", - options: sortBy( - uniqBy( - pickOrders.map((po) => ({ - value: po.type, - label: t(upperCase(po.type)), - })), - "value", - ), - "label", - ), - }, - { - label: t("Status"), - paramName: "status", - type: "autocomplete", - options: sortBy( - uniqBy( - pickOrders.map((po) => ({ - value: po.status, - label: t(upperFirst(po.status)), - })), - "value", - ), - "label", - ), - }, - { - label: t("Items"), - paramName: "items", - type: "autocomplete", // multiple: true, - options: uniqBy( - flatten( - sortBy( - pickOrders.map((po) => - po.items - ? po.items.map((item) => ({ - value: item.name, - label: item.name, - // group: item.type - })) - : [], + () => { + 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", ), - "label", + }, + { + 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", + ), + }, + ]; + + if (tabIndex === 3) { + + baseCriteria.splice(1, 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", + }); + } + + if (tabIndex !== 3) { + baseCriteria.splice(4, 0, { + label: t("Status"), + paramName: "status", + type: "autocomplete", + options: sortBy( + uniqBy( + pickOrders.map((po) => ({ + value: po.status, + label: t(upperFirst(po.status)), + })), + "value", ), + "label", ), - "value", - ), - }, - ], - [pickOrders, t], + }); + } + + return baseCriteria; + }, + [pickOrders, t, tabIndex, items], ); const fetchNewPagePickOrder = useCallback( @@ -186,40 +244,66 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { { - setFilterArgs({ ...query }); // modify later - setFilteredPickOrders( - pickOrders.filter((po) => { - const poTargetDateStr = arrayToDayjs(po.targetDate); - - // console.log(intersectionWith(po.items?.map(item => item.name), query.items)) - return ( - po.code.toLowerCase().includes(query.code.toLowerCase()) && - (isEmpty(query.targetDate) || + console.log("SearchBox search triggered with query:", query); + setSearchQuery({ ...query }); + + // when tabIndex === 3, do not execute any search logic, only store query conditions + if (tabIndex !== 3) { + // only execute search logic when other tabs + setFilterArgs({ ...query }); + setFilteredPickOrders( + pickOrders.filter((po) => { + const poTargetDateStr = arrayToDayjs(po.targetDate); + + // safely check search conditions + const codeMatch = !query.code || + po.code.toLowerCase().includes((query.code || "").toLowerCase()); + + const dateMatch = !query.targetDate || poTargetDateStr.isSame(query.targetDate) || - poTargetDateStr.isAfter(query.targetDate)) && - (isEmpty(query.targetDateTo) || + poTargetDateStr.isAfter(query.targetDate); + + const dateToMatch = !query.targetDateTo || poTargetDateStr.isSame(query.targetDateTo) || - poTargetDateStr.isBefore(query.targetDateTo)) && - (intersectionWith(["All"], query.items).length > 0 || - intersectionWith( - po.items?.map((item) => item.name), - query.items, - ).length > 0) && - (query.status.toLowerCase() == "all" || - po.status - .toLowerCase() - .includes(query.status.toLowerCase())) && - (query.type.toLowerCase() == "all" || - po.type.toLowerCase().includes(query.type.toLowerCase())) - ); - }), - ); + poTargetDateStr.isBefore(query.targetDateTo); + + const itemsMatch = !query.items || + Array.isArray(query.items) && ( + intersectionWith(["All"], query.items).length > 0 || + intersectionWith( + po.items?.map((item) => item.name) || [], + query.items, + ).length > 0 + ); + + const statusMatch = !query.status || + query.status.toLowerCase() === "all" || + po.status.toLowerCase().includes((query.status || "").toLowerCase()); + + const typeMatch = !query.type || + query.type.toLowerCase() === "all" || + po.type.toLowerCase().includes((query.type || "").toLowerCase()); + + return codeMatch && dateMatch && dateToMatch && itemsMatch && statusMatch && typeMatch; + }), + ); + } + // when tabIndex === 3, SearchBox's search will not trigger any filtering, only pass data to NewCreateItem + }} + onReset={() => { + console.log("SearchBox reset triggered"); + setSearchQuery({}); + if (tabIndex !== 3) { + onReset(); + } }} - onReset={onReset} /> + + + {tabIndex === 0 && ( = ({ pickOrders }) => { /> )} {tabIndex === 1 && } + {tabIndex === 2 && } + {tabIndex === 3 && } ); }; diff --git a/src/components/PickOrderSearch/PickQcStockInModalVer2.tsx b/src/components/PickOrderSearch/PickQcStockInModalVer2.tsx new file mode 100644 index 0000000..7cacf6e --- /dev/null +++ b/src/components/PickOrderSearch/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/PickOrderSearch/PickQcStockInModalVer3.tsx b/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx new file mode 100644 index 0000000..039f498 --- /dev/null +++ b/src/components/PickOrderSearch/PickQcStockInModalVer3.tsx @@ -0,0 +1,335 @@ +"use client"; + +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, + TextField, + Radio, + RadioGroup, + FormControlLabel, + FormControl, +} 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(`QC failed: ${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 table - same as QcFormVer2 */} + + + + + + QC模板代號 + 檢查項目 + QC RESULT + FAILED QTY + REMARKS + + + + {qcItems.map((item, index) => ( + + {item.id} + + {item.qcDescription} + + + {/* same as QcFormVer2 */} + + { + const value = e.target.value; + handleQcItemChange(index, 'isPassed', value === "true"); + }} + name={`isPassed-${item.id}`} + > + } + label="合格" + sx={{ + color: item.isPassed === true ? "green" : "inherit", + "& .Mui-checked": {color: "green"} + }} + /> + } + label="不合格" + sx={{ + color: item.isPassed === false ? "red" : "inherit", + "& .Mui-checked": {color: "red"} + }} + /> + + + + + { + const v = e.target.value; + const next = v === '' ? undefined : Number(v); + if (Number.isNaN(next)) return; + handleQcItemChange(index, 'failedQty', next); + }} + inputProps={{ min: 0 }} + sx={{ width: '60px' }} + /> + + + { + const remarks = e.target.value; + handleQcItemChange(index, 'remarks', remarks); + }} + sx={{ width: '280px' }} + /> + + + ))} + +
+
+
+ + {/* buttons */} + + + + + + + +
+
+
+
+ + ); +}; + +export default PickQcStockInModalVer2; \ No newline at end of file diff --git a/src/components/PickOrderSearch/PutawayForm.tsx b/src/components/PickOrderSearch/PutawayForm.tsx new file mode 100644 index 0000000..75459a7 --- /dev/null +++ b/src/components/PickOrderSearch/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("putawayLine")) + // 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={"putawayLine"} + columns={columns} + validateRow={validation} + needAdd={true} + showRemoveBtn={false} + /> + + + {/* + + */} + + + + + {t("Please scan warehouse qr code.")} + + {/* */} + + + + ); +}; +export default PutawayForm; diff --git a/src/components/PickOrderSearch/QCDatagrid.tsx b/src/components/PickOrderSearch/QCDatagrid.tsx new file mode 100644 index 0000000..b9947db --- /dev/null +++ b/src/components/PickOrderSearch/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/PickOrderSearch/QcFormVer2.tsx b/src/components/PickOrderSearch/QcFormVer2.tsx new file mode 100644 index 0000000..9c615a2 --- /dev/null +++ b/src/components/PickOrderSearch/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/dashboard/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/PickOrderSearch/QcSelect.tsx b/src/components/PickOrderSearch/QcSelect.tsx new file mode 100644 index 0000000..b42732b --- /dev/null +++ b/src/components/PickOrderSearch/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/PickOrderSearch/StockInFormVer2.tsx b/src/components/PickOrderSearch/StockInFormVer2.tsx new file mode 100644 index 0000000..32b9169 --- /dev/null +++ b/src/components/PickOrderSearch/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/PickOrderSearch/TwoLineCell.tsx b/src/components/PickOrderSearch/TwoLineCell.tsx new file mode 100644 index 0000000..f32e56a --- /dev/null +++ b/src/components/PickOrderSearch/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/PickOrderSearch/dummyQcTemplate.tsx b/src/components/PickOrderSearch/dummyQcTemplate.tsx new file mode 100644 index 0000000..8f9ab89 --- /dev/null +++ b/src/components/PickOrderSearch/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/PickOrderSearch/newcreatitem.tsx b/src/components/PickOrderSearch/newcreatitem.tsx new file mode 100644 index 0000000..760a437 --- /dev/null +++ b/src/components/PickOrderSearch/newcreatitem.tsx @@ -0,0 +1,635 @@ +"use client"; + +import { createPickOrder, SavePickOrderRequest, SavePickOrderLineRequest } from "@/app/api/pickOrder/actions"; +import { + Autocomplete, + Box, + Button, + FormControl, + Grid, + Stack, + TextField, + Typography, + Checkbox, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, +} 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 } 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"; + +type Props = { + filterArgs?: Record; + searchQuery?: Record; +}; + +// 扩展表单类型以包含搜索字段 +interface SearchFormData extends SavePickOrderRequest { + searchCode?: string; + searchName?: string; +} + +interface CreatedItem { + itemId: number; + itemName: string; + itemCode: string; + qty: number; + uom: string; + uomId: number; + isSelected: boolean; +} + +const NewCreateItem: React.FC = ({ filterArgs, searchQuery }) => { + 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); + + // Add state for selected item IDs in search results + const [selectedSearchItemIds, setSelectedSearchItemIds] = 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"); + + // 加载项目数据 + 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(); + }, []); + + // 根据搜索查询过滤项目 + useEffect(() => { + if (searchQuery && items.length > 0) { + // 检查是否有有效的搜索条件 + const hasValidSearch = ( + (searchQuery.items && searchQuery.items.trim && searchQuery.items.trim() !== "") || + (searchQuery.code && searchQuery.code.trim && searchQuery.code.trim() !== "") || + (searchQuery.type && searchQuery.type !== "All") + ); + + if (hasValidSearch) { + let filtered = items; + + // 处理项目名称搜索 - 确保 searchQuery.items 是数组 + if (searchQuery.items) { + const itemsToSearch = Array.isArray(searchQuery.items) + ? searchQuery.items + : [searchQuery.items]; + + if (itemsToSearch.length > 0 && !itemsToSearch.includes("All")) { + filtered = filtered.filter(item => + itemsToSearch.some((searchItem: string) => + item.label.toLowerCase().includes(searchItem.toLowerCase()) + ) + ); + } + } + + // 处理项目代码搜索 + if (searchQuery.code) { + filtered = filtered.filter(item => + item.label.toLowerCase().includes(searchQuery.code.toLowerCase()) + ); + } + + // 处理类型搜索 + if (searchQuery.type && searchQuery.type !== "All") { + // 这里可以根据实际需求调整类型过滤逻辑 + // 目前先注释掉,因为项目数据可能没有类型字段 + // filtered = filtered.filter(item => item.type === searchQuery.type); + } + + filtered = filtered.slice(0, 10); + setFilteredItems(filtered); + setHasSearched(true); + } else { + // 如果没有有效的搜索条件,清空结果 + setFilteredItems([]); + setHasSearched(false); + } + } else { + // 如果没有搜索查询,清空结果并重置搜索状态 + setFilteredItems([]); + setHasSearched(false); + } + }, [searchQuery, items]); + + // 新增:同步 SearchBox 的数据到表单 + 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, 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); + } + + filtered = filtered.slice(0, 100); + console.log("Final filtered results:", filtered.length); + setFilteredItems(filtered); + setIsLoading(false); + }, 500); + }, [type, searchCode, searchName, items, t]); + + // 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; + } + + const newCreatedItem: CreatedItem = { + itemId: item.id, + itemName: item.label, + itemCode: item.label, + qty: 1, + uom: item.uom || "", + uomId: item.uomId || 0, + isSelected: true + }; + setCreatedItems(prev => [...prev, newCreatedItem]); + } + }, [filteredItems, createdItems, t]); + + // 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]); + + 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; + } + + + let formattedTargetDate = data.targetDate; + if (data.targetDate && typeof data.targetDate === 'string') { + try { + const date = dayjs(data.targetDate); + formattedTargetDate = date.format('YYYY-MM-DD'); + } catch (error) { + console.error("Invalid date format:", data.targetDate); + alert(t("Invalid date format")); + return; + } + } + + + const pickOrderData: SavePickOrderRequest = { + type: data.type || "Consumable", + targetDate: formattedTargetDate, + pickOrderLine: selectedCreatedItems.map(item => ({ + itemId: item.itemId, + qty: item.qty, + uomId: item.uomId + } as SavePickOrderLineRequest)) + }; + + console.log("Submitting pick order:", pickOrderData); + + try { + const res = await createPickOrder(pickOrderData); + if (res.id) { + console.log("Pick order created successfully:", res); + + setCreatedItems(prev => prev.filter(item => !item.isSelected)); + formProps.reset(); + setHasSearched(false); + setFilteredItems([]); + alert(t("Pick order created successfully")); + } + } catch (error) { + console.error("Error creating pick order:", error); + alert(t("Failed to create pick order")); + } + }, + [createdItems, t, formProps] + ); + + const handleReset = useCallback(() => { + formProps.reset(); + setCreatedItems([]); + setHasSearched(false); + setFilteredItems([]); + }, [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, + // }); + // } + }; + + // 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) => ( + + {item.label} + + ID: {item.id} + + + ), + }, + { + name: "id", // Use id as placeholder for quantity + label: t("Order Quantity"), + renderCell: () => "-", + }, + { + name: "uom", + label: t("Unit"), + renderCell: (item) => item.uom || "-", + }, + ], [t, isItemInCreated]); + + // Handle checkbox selection from SearchResults + const handleSearchCheckboxChange = useCallback((ids: (string | number)[]) => { + setSelectedSearchItemIds(ids); + + // Process newly selected items + ids.forEach(id => { + if (!isItemInCreated(id as number)) { + handleSearchItemSelect(id as number, true); + } + }); + }, [isItemInCreated, handleSearchItemSelect]); + + return ( + + + + + + {t("Pick Order Detail")} + + + + {/* 隐藏搜索条件区域 */} + {/* + + + option.type} + options={typeList} + onChange={handleTypeChange} + renderInput={(params) => } + /> + + + + + ( + + )} + /> + + + + ( + + )} + /> + + + + ( + + { + if (!date) return; + formProps.setValue("targetDate", date.format(INPUT_DATE_FORMAT)); + }} + inputRef={field.ref} + slotProps={{ + textField: { + error: Boolean(errors.targetDate?.message), + helperText: errors.targetDate?.message, + }, + }} + /> + + )} + /> + + + + + + */} + + {/* 创建项目区域 */} + {createdItems.length > 0 && ( + + + {t("Created Items")} ({createdItems.length}) + + + + + + + + {t("Select")} + + + {t("Item")} + + + {t("Order Quantity")} + + + {t("Unit")} + + + + + {createdItems.map((item) => ( + + + handleCreatedItemSelect(item.itemId, e.target.checked)} + /> + + + {item.itemName} + + {item.itemCode} + + + + handleQtyChange(item.itemId, Number(e.target.value))} + inputProps={{ min: 1 }} + sx={{ width: '80px' }} + /> + + + {item.uom} + + + ))} + +
+
+
+ )} + {/* Search Results with SearchResults component */} + {hasSearched && filteredItems.length > 0 && ( + + + {t("Search Results")} ({filteredItems.length}) + {filteredItems.length >= 100 && ( + + {t("Showing first 100 results")} + + )} + + + + items={filteredItems} + columns={searchItemColumns} + totalCount={filteredItems.length} + checkboxIds={selectedSearchItemIds} + setCheckboxIds={handleSearchCheckboxChange} + /> + + )} + + + + {/* 操作按钮 */} + + + + +
+
+ ); +}; + +export default NewCreateItem; \ No newline at end of file diff --git a/src/components/PickOrderSearch/pickorderModelVer2.tsx b/src/components/PickOrderSearch/pickorderModelVer2.tsx new file mode 100644 index 0000000..b95a819 --- /dev/null +++ b/src/components/PickOrderSearch/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;