| @@ -81,6 +81,93 @@ export interface PickOrderApprovalInput { | |||||
| rejectQty: number; | rejectQty: number; | ||||
| status: string; | 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<GetPickOrderInfoResponse>( | |||||
| `${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<GetPickOrderInfoResponse>( | |||||
| `${BASE_API_URL}/pickOrder/detail`, | |||||
| { | |||||
| method: "GET", | |||||
| next: { tags: ["pickorder"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| export const fetchPickOrderLineLotDetails = cache(async (pickOrderLineId: number) => { | |||||
| return serverFetchJson<PickOrderLotDetailResponse[]>( | |||||
| `${BASE_API_URL}/pickOrder/lot-details/${pickOrderLineId}`, | |||||
| { | |||||
| method: "GET", | |||||
| next: { tags: ["pickorder"] }, | |||||
| }, | |||||
| ); | |||||
| }); | |||||
| export const createPickOrder = async (data: SavePickOrderRequest) => { | export const createPickOrder = async (data: SavePickOrderRequest) => { | ||||
| console.log(data); | console.log(data); | ||||
| const po = await serverFetchJson<PostPickOrderResponse>( | const po = await serverFetchJson<PostPickOrderResponse>( | ||||
| @@ -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<React.SetStateAction<boolean>> | |||||
| } | |||||
| const EscalationComponent: React.FC<Props> = ({ | |||||
| forSupervisor, | |||||
| isCollapsed, | |||||
| setIsCollapsed | |||||
| }) => { | |||||
| const { t } = useTranslation("purchaseOrder"); | |||||
| const [formData, setFormData] = useState<FormData>({ | |||||
| 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<HTMLInputElement | HTMLTextAreaElement> | SelectChangeEvent<string> | |||||
| ): void => { | |||||
| const { name, value } = event.target; | |||||
| setFormData((prev) => ({ | |||||
| ...prev, | |||||
| [name]: value, | |||||
| })); | |||||
| }; | |||||
| const handleSubmit = (e: FormEvent<HTMLFormElement>): void => { | |||||
| e.preventDefault(); | |||||
| console.log('表單已提交:', formData); | |||||
| // 處理表單提交 | |||||
| }; | |||||
| const handleCollapseToggle = (e: ChangeEvent<HTMLInputElement>): void => { | |||||
| setIsCollapsed(e.target.checked); | |||||
| }; | |||||
| return ( | |||||
| // <Paper elevation={3} sx={{ maxWidth: 400, mx: 'auto', p: 3 }}> | |||||
| <> | |||||
| <Paper> | |||||
| {/* <Paper elevation={3} sx={{ mx: 'auto', p: 3 }}> */} | |||||
| <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> | |||||
| <FormControlLabel | |||||
| control={ | |||||
| <Checkbox | |||||
| checked={isCollapsed} | |||||
| onChange={handleCollapseToggle} | |||||
| color="primary" | |||||
| /> | |||||
| } | |||||
| label={ | |||||
| <Box sx={{ display: 'flex', alignItems: 'center' }}> | |||||
| <Typography variant="body1">上報結果</Typography> | |||||
| {isCollapsed ? ( | |||||
| <ExpandLessIcon sx={{ ml: 1 }} /> | |||||
| ) : ( | |||||
| <ExpandMoreIcon sx={{ ml: 1 }} /> | |||||
| )} | |||||
| </Box> | |||||
| } | |||||
| /> | |||||
| </Box> | |||||
| <Collapse in={isCollapsed}> | |||||
| <Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> | |||||
| {forSupervisor ? ( | |||||
| <FormControl> | |||||
| <RadioGroup | |||||
| row | |||||
| aria-labelledby="demo-radio-buttons-group-label" | |||||
| defaultValue="pass" | |||||
| name="radio-buttons-group" | |||||
| > | |||||
| <FormControlLabel value="pass" control={<Radio />} label="合格" /> | |||||
| <FormControlLabel value="fail" control={<Radio />} label="不合格" /> | |||||
| </RadioGroup> | |||||
| </FormControl> | |||||
| ): undefined} | |||||
| <FormControl fullWidth> | |||||
| <select | |||||
| id="name" | |||||
| name="name" | |||||
| value={formData.name} | |||||
| onChange={handleInputChange} | |||||
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" | |||||
| > | |||||
| {nameOptions.map((option: NameOption) => ( | |||||
| <option key={option.value} value={option.value}> | |||||
| {option.label} | |||||
| </option> | |||||
| ))} | |||||
| </select> | |||||
| </FormControl> | |||||
| <TextField | |||||
| fullWidth | |||||
| id="quantity" | |||||
| name="quantity" | |||||
| label="數量" | |||||
| type="number" | |||||
| value={formData.quantity} | |||||
| onChange={handleInputChange} | |||||
| InputProps={{ inputProps: { min: 1 } }} | |||||
| placeholder="請輸入數量" | |||||
| /> | |||||
| <TextField | |||||
| fullWidth | |||||
| id="message" | |||||
| name="message" | |||||
| label="備註" | |||||
| multiline | |||||
| rows={4} | |||||
| value={formData.message} | |||||
| onChange={handleInputChange} | |||||
| placeholder="請輸入您的備註" | |||||
| /> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| type="submit" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| > | |||||
| {t("update qc info")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Box> | |||||
| </Collapse> | |||||
| </Paper> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default EscalationComponent; | |||||
| @@ -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<string, any>; | |||||
| } | |||||
| 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<Props> = ({ filterArgs }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| const router = useRouter(); | |||||
| const [filteredPickOrders, setFilteredPickOrders] = useState( | |||||
| [] as ConsoPickOrderResult[], | |||||
| ); | |||||
| const [isLoading, setIsLoading] = useState(false); | |||||
| const [selectedConsoCode, setSelectedConsoCode] = useState<string | undefined>(); | |||||
| const [revertIds, setRevertIds] = useState<GridInputRowSelectionModel>([]); | |||||
| const [totalCount, setTotalCount] = useState<number>(); | |||||
| const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||||
| const [byPickOrderRows, setByPickOrderRows] = useState< | |||||
| Omit<PickOrderResult, "items">[] | undefined | |||||
| >(undefined); | |||||
| const [byItemsRows, setByItemsRows] = useState<ByItemsSummary[] | undefined>( | |||||
| undefined, | |||||
| ); | |||||
| const [disableRelease, setDisableRelease] = useState<boolean>(true); | |||||
| const [selectedRowId, setSelectedRowId] = useState<number | null>(null); | |||||
| const [pickOrderDetails, setPickOrderDetails] = useState<GetPickOrderInfoResponse | null>(null); | |||||
| const [detailLoading, setDetailLoading] = useState(false); | |||||
| const [pickQtyData, setPickQtyData] = useState<PickQtyData>({}); | |||||
| const [lotData, setLotData] = useState<LotPickData[]>([]); | |||||
| const [qcItems, setQcItems] = useState<QcItemWithChecks[]>([]); | |||||
| const [qcModalOpen, setQcModalOpen] = useState(false); | |||||
| const [selectedItemForQc, setSelectedItemForQc] = useState<GetPickOrderLineInfo & { | |||||
| pickOrderCode: string; | |||||
| qcResult?: PurchaseQcResult[]; | |||||
| } | null>(null); | |||||
| const formProps = useForm<ReleasePickOrderInputs>(); | |||||
| 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<string, number>, | |||||
| filterArgs: Record<string, number>, | |||||
| ) => { | |||||
| 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<SubmitHandler<ReleasePickOrderInputs>>( | |||||
| 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<SubmitErrorHandler<ReleasePickOrderInputs>>( | |||||
| (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<HTMLInputElement>) => { | |||||
| 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 ( | |||||
| <TableRow | |||||
| key={line.id} | |||||
| sx={{ | |||||
| "& > *": { borderBottom: "unset" }, | |||||
| color: "black", | |||||
| backgroundColor: selectedRowId === line.id ? "action.selected" : "inherit", | |||||
| cursor: "pointer", | |||||
| "&:hover": { | |||||
| backgroundColor: "action.hover", | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <TableCell align="center" sx={{ width: "60px" }}> | |||||
| <Checkbox | |||||
| checked={selectedRowId === line.id} | |||||
| onChange={handleRowSelect} | |||||
| onClick={(e) => e.stopPropagation()} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell align="left">{pickOrderCode}</TableCell> | |||||
| <TableCell align="left">{line.itemCode}</TableCell> | |||||
| <TableCell align="left">{line.itemName}</TableCell> | |||||
| <TableCell align="right">{line.requiredQty}</TableCell> | |||||
| <TableCell align="right" sx={{ | |||||
| //color: balanceToPick >= 0 ? 'success.main' : 'error.main', | |||||
| //fontWeight: 'bold' | |||||
| }}> | |||||
| {balanceToPick} | |||||
| </TableCell> | |||||
| <TableCell align="right">{totalPickedQty}</TableCell> | |||||
| </TableRow> | |||||
| ); | |||||
| }, [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<number[]>([]); | |||||
| // 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<Column<GetPickOrderLineInfo & { pickOrderCode: string }>[]>( | |||||
| () => [ | |||||
| { | |||||
| 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 ( | |||||
| <Typography | |||||
| sx={{ | |||||
| color: balanceToPick >= 0 ? 'success.main' : 'error.main', | |||||
| //fontWeight: 'bold' | |||||
| }} | |||||
| > | |||||
| {balanceToPick} | |||||
| </Typography> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| name: "qtyAlreadyPicked", | |||||
| label: t("Qty Already Picked"), | |||||
| renderCell: (params) => { | |||||
| return getTotalPickedQty(params.id); | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| [t, getTotalPickedQty], | |||||
| ); | |||||
| const area5Columns = useMemo<Column<LotPickData>[]>( | |||||
| () => [ | |||||
| { | |||||
| name: "id", // Use "id" instead of "select" | |||||
| label: "", | |||||
| type: "checkbox", | |||||
| disabled: () => false, | |||||
| }, | |||||
| { | |||||
| name: "lotNo", | |||||
| label: t("Lot#"), | |||||
| renderCell: (params) => ( | |||||
| <Box> | |||||
| <Typography>{params.lotNo}</Typography> | |||||
| {params.lotAvailability !== 'available' && ( | |||||
| <Typography variant="caption" color="error" display="block"> | |||||
| ({params.lotAvailability === 'expired' ? 'Expired' : | |||||
| params.lotAvailability === 'insufficient_stock' ? 'Insufficient' : | |||||
| 'Unavailable'}) | |||||
| </Typography> | |||||
| )} | |||||
| </Box> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| 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) => ( | |||||
| <TextField | |||||
| type="number" | |||||
| value={selectedRowId ? (pickQtyData[selectedRowId]?.[params.lotId] || 0) : 0} | |||||
| onChange={(e) => { | |||||
| 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) => ( | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| if (selectedRowId) { | |||||
| handleSubmitPickQty(selectedRowId, params.lotId); | |||||
| } | |||||
| }} | |||||
| disabled={params.lotAvailability !== 'available' || !pickQtyData[selectedRowId!]?.[params.lotId]} | |||||
| sx={{ | |||||
| fontSize: '0.75rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px' | |||||
| }} | |||||
| > | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| ), | |||||
| }, | |||||
| ], | |||||
| [t, selectedRowId, pickQtyData, handlePickQtyChange, handleSubmitPickQty], | |||||
| ); | |||||
| return ( | |||||
| <FormProvider {...formProps}> | |||||
| <Stack spacing={2}> | |||||
| {/* Area 4 & 5: Main Table and Detail Side by Side */} | |||||
| <Grid container spacing={2} sx={{ height: '100%', flex: 1 }}> | |||||
| {/* Area 4: Main Table */} | |||||
| <Grid item xs={6} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | |||||
| <Typography variant="h6" gutterBottom> | |||||
| {t("Pick Order Details")} | |||||
| </Typography> | |||||
| {detailLoading ? ( | |||||
| <Box display="flex" justifyContent="center" alignItems="center" flex={1}> | |||||
| <CircularProgress size={40} /> | |||||
| </Box> | |||||
| ) : pickOrderDetails ? ( | |||||
| <> | |||||
| <SearchResults<GetPickOrderLineInfo & { pickOrderCode: string }> | |||||
| 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([]); | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </> | |||||
| ) : ( | |||||
| <Box display="flex" justifyContent="center" alignItems="center" flex={1}> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| 正在載入數據... | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| </Grid> | |||||
| {/* Area 5: Item lot to be Pick */} | |||||
| <Grid item xs={6} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | |||||
| {selectedRow && lotData.length > 0 ? ( | |||||
| <> | |||||
| <Typography variant="h6" gutterBottom> | |||||
| Item lot to be Pick: {selectedRow.pickOrderCode} - {selectedRow.itemName} | |||||
| </Typography> | |||||
| <SearchResults<LotPickData> | |||||
| 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 */} | |||||
| <Box sx={{ mt: 2 }}> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| if (selectedRowId && selectedRow) { | |||||
| handleQcCheck(selectedRow, selectedRow.pickOrderCode); | |||||
| } | |||||
| }} | |||||
| sx={{ whiteSpace: 'nowrap' }} | |||||
| > | |||||
| {t("Qc Check")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => handleInsufficientStock()} | |||||
| sx={{ whiteSpace: 'nowrap' }} | |||||
| > | |||||
| {t("Insufficient Stock & Pick Another Lot")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Box> | |||||
| </> | |||||
| ) : ( | |||||
| <Box display="flex" justifyContent="center" alignItems="center" flex={1}> | |||||
| <Typography variant="h6" color="text.secondary"> | |||||
| Please select an pick order lot to view lot information | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| </Grid> | |||||
| </Grid> | |||||
| {/* Area 6: Action Buttons */} | |||||
| {selectedRow && ( | |||||
| <Grid container> | |||||
| <Grid item xs={12} display="flex" justifyContent="end" alignItems="end"> | |||||
| <Button | |||||
| disabled={(revertIds as number[]).length < 1} | |||||
| variant="outlined" | |||||
| onClick={handleConsolidate_revert} | |||||
| sx={{ mr: 1 }} | |||||
| > | |||||
| {t("remove")} | |||||
| </Button> | |||||
| <Button | |||||
| disabled={disableRelease} | |||||
| variant="contained" | |||||
| onClick={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
| > | |||||
| {t("release")} | |||||
| </Button> | |||||
| </Grid> | |||||
| </Grid> | |||||
| )} | |||||
| {/* QC Modal */} | |||||
| {selectedItemForQc && qcModalOpen && ( | |||||
| <PickQcStockInModalVer2 | |||||
| open={qcModalOpen} | |||||
| onClose={handleCloseQcModal} | |||||
| itemDetail={selectedItemForQc} | |||||
| setItemDetail={handleSetItemDetail} | |||||
| qc={qcItems} | |||||
| warehouse={[]} | |||||
| /> | |||||
| )} | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| ); | |||||
| }; | |||||
| export default PickExecution; | |||||
| @@ -18,7 +18,9 @@ import { | |||||
| import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | import { Button, Grid, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; | ||||
| import PickOrders from "./PickOrders"; | import PickOrders from "./PickOrders"; | ||||
| import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; | import ConsolidatedPickOrders from "./ConsolidatedPickOrders"; | ||||
| import PickExecution from "./PickExecution"; | |||||
| import CreatePickOrderModal from "./CreatePickOrderModal"; | import CreatePickOrderModal from "./CreatePickOrderModal"; | ||||
| import NewCreateItem from "./newcreatitem"; | |||||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | ||||
| import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | ||||
| @@ -39,6 +41,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| const [items, setItems] = useState<ItemCombo[]>([]) | const [items, setItems] = useState<ItemCombo[]>([]) | ||||
| const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); | const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); | ||||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | ||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| const [totalCount, setTotalCount] = useState<number>(); | const [totalCount, setTotalCount] = useState<number>(); | ||||
| @@ -48,6 +51,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| }, | }, | ||||
| [], | [], | ||||
| ); | ); | ||||
| const openCreateModal = useCallback(async () => { | const openCreateModal = useCallback(async () => { | ||||
| console.log("testing") | console.log("testing") | ||||
| const res = await fetchAllItemsInClient() | const res = await fetchAllItemsInClient() | ||||
| @@ -60,69 +64,123 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| setIsOpenCreateModal(false) | 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<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = 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<SearchParamNames>[] = [ | |||||
| { | |||||
| 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( | const fetchNewPagePickOrder = useCallback( | ||||
| @@ -186,40 +244,66 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| 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.isSame(query.targetDate) || | ||||
| poTargetDateStr.isAfter(query.targetDate)) && | |||||
| (isEmpty(query.targetDateTo) || | |||||
| poTargetDateStr.isAfter(query.targetDate); | |||||
| const dateToMatch = !query.targetDateTo || | |||||
| poTargetDateStr.isSame(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} | |||||
| /> | /> | ||||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | ||||
| <Tab label={t("Pick Orders")} iconPosition="end" /> | <Tab label={t("Pick Orders")} iconPosition="end" /> | ||||
| <Tab label={t("Consolidated Pick Orders")} iconPosition="end" /> | <Tab label={t("Consolidated Pick Orders")} iconPosition="end" /> | ||||
| <Tab label={t("Pick Execution")} iconPosition="end" /> | |||||
| <Tab label={t("Create Item")} iconPosition="end" /> | |||||
| </Tabs> | </Tabs> | ||||
| {tabIndex === 0 && ( | {tabIndex === 0 && ( | ||||
| <PickOrders | <PickOrders | ||||
| @@ -228,6 +312,8 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| /> | /> | ||||
| )} | )} | ||||
| {tabIndex === 1 && <ConsolidatedPickOrders filterArgs={filterArgs} />} | {tabIndex === 1 && <ConsolidatedPickOrders filterArgs={filterArgs} />} | ||||
| {tabIndex === 2 && <PickExecution filterArgs={filterArgs} />} | |||||
| {tabIndex === 3 && <NewCreateItem filterArgs={filterArgs} searchQuery={searchQuery} />} | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -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<ModalProps, "children"> { | |||||
| 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<Props> = ({ | |||||
| open, | |||||
| onClose, | |||||
| itemDetail, | |||||
| setItemDetail, | |||||
| qc, | |||||
| warehouse, | |||||
| }) => { | |||||
| console.log(warehouse); | |||||
| // 修改翻译键 | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation("pickOrder"); | |||||
| const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| const formProps = useForm<any>({ | |||||
| defaultValues: { | |||||
| ...itemDetail, | |||||
| }, | |||||
| }); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (...args) => { | |||||
| onClose?.(...args); | |||||
| }, | |||||
| [onClose], | |||||
| ); | |||||
| // QC submission handler | |||||
| const onSubmitQc = useCallback<SubmitHandler<any>>( | |||||
| 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 ( | |||||
| <> | |||||
| <FormProvider {...formProps}> | |||||
| <Modal open={open} onClose={closeHandler}> | |||||
| <Box | |||||
| sx={{ | |||||
| ...style, | |||||
| padding: 2, | |||||
| maxHeight: "90vh", | |||||
| overflowY: "auto", | |||||
| marginLeft: 3, | |||||
| marginRight: 3, | |||||
| }} | |||||
| > | |||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| GroupA - {itemDetail.pickOrderCode} | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary" marginBlockEnd={2}> | |||||
| 記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標 | |||||
| </Typography> | |||||
| </Grid> | |||||
| {/* QC 表格 */} | |||||
| <Grid item xs={12}> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>QC模板代號</TableCell> | |||||
| <TableCell>檢查項目</TableCell> | |||||
| <TableCell>QC Result</TableCell> | |||||
| <TableCell>Failed Qty</TableCell> | |||||
| <TableCell>Remarks</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {qcItems.map((item, index) => ( | |||||
| <TableRow key={item.id}> | |||||
| <TableCell>{item.id}</TableCell> | |||||
| <TableCell>{item.qcDescription}</TableCell> | |||||
| <TableCell> | |||||
| <select | |||||
| value={item.isPassed === undefined ? '' : item.isPassed ? 'pass' : 'fail'} | |||||
| onChange={(e) => handleQcItemChange(index, 'isPassed', e.target.value === 'pass')} | |||||
| > | |||||
| <option value="">Select</option> | |||||
| <option value="pass">Pass</option> | |||||
| <option value="fail">Fail</option> | |||||
| </select> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <input | |||||
| type="number" | |||||
| value={item.failedQty || 0} | |||||
| onChange={(e) => handleQcItemChange(index, 'failedQty', parseInt(e.target.value) || 0)} | |||||
| disabled={item.isPassed !== false} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <input | |||||
| type="text" | |||||
| value={item.remarks || ''} | |||||
| onChange={(e) => handleQcItemChange(index, 'remarks', e.target.value)} | |||||
| /> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ))} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Grid> | |||||
| {/* 按钮 */} | |||||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="success" | |||||
| onClick={formProps.handleSubmit(onSubmitQc)} | |||||
| > | |||||
| QC Accept | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="warning" | |||||
| onClick={() => { | |||||
| console.log("Sort to accept"); | |||||
| onClose(); | |||||
| }} | |||||
| > | |||||
| Sort to Accept | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="error" | |||||
| onClick={() => { | |||||
| console.log("Reject and pick another lot"); | |||||
| onClose(); | |||||
| }} | |||||
| > | |||||
| Reject and Pick Another Lot | |||||
| </Button> | |||||
| </Stack> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </Modal> | |||||
| </FormProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default PickQcStockInModalVer2; | |||||
| @@ -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<ModalProps, "children"> { | |||||
| 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<Props> = ({ | |||||
| open, | |||||
| onClose, | |||||
| itemDetail, | |||||
| setItemDetail, | |||||
| qc, | |||||
| warehouse, | |||||
| }) => { | |||||
| console.log(warehouse); | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation("pickOrder"); | |||||
| const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| const formProps = useForm<any>({ | |||||
| defaultValues: { | |||||
| ...itemDetail, | |||||
| }, | |||||
| }); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (...args) => { | |||||
| onClose?.(...args); | |||||
| }, | |||||
| [onClose], | |||||
| ); | |||||
| // QC submission handler | |||||
| const onSubmitQc = useCallback<SubmitHandler<any>>( | |||||
| 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 ( | |||||
| <> | |||||
| <FormProvider {...formProps}> | |||||
| <Modal open={open} onClose={closeHandler}> | |||||
| <Box | |||||
| sx={{ | |||||
| ...style, | |||||
| padding: 2, | |||||
| maxHeight: "90vh", | |||||
| overflowY: "auto", | |||||
| marginLeft: 3, | |||||
| marginRight: 3, | |||||
| }} | |||||
| > | |||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| GroupA - {itemDetail.pickOrderCode} | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary" marginBlockEnd={2}> | |||||
| 記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標 | |||||
| </Typography> | |||||
| </Grid> | |||||
| {/* QC table - same as QcFormVer2 */} | |||||
| <Grid item xs={12}> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell sx={{ width: '80px' }}>QC模板代號</TableCell> | |||||
| <TableCell sx={{ width: '300px' }}>檢查項目</TableCell> | |||||
| <TableCell sx={{ width: '120px' }}>QC RESULT</TableCell> | |||||
| <TableCell sx={{ width: '80px' }}>FAILED QTY</TableCell> | |||||
| <TableCell sx={{ width: '300px' }}>REMARKS</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {qcItems.map((item, index) => ( | |||||
| <TableRow key={item.id}> | |||||
| <TableCell>{item.id}</TableCell> | |||||
| <TableCell sx={{ | |||||
| maxWidth: '300px', | |||||
| wordWrap: 'break-word', | |||||
| whiteSpace: 'normal' | |||||
| }}> | |||||
| {item.qcDescription} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {/* same as QcFormVer2 */} | |||||
| <FormControl> | |||||
| <RadioGroup | |||||
| row | |||||
| aria-labelledby="demo-radio-buttons-group-label" | |||||
| value={item.isPassed === undefined ? "" : (item.isPassed ? "true" : "false")} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value; | |||||
| handleQcItemChange(index, 'isPassed', value === "true"); | |||||
| }} | |||||
| name={`isPassed-${item.id}`} | |||||
| > | |||||
| <FormControlLabel | |||||
| value="true" | |||||
| control={<Radio size="small" />} | |||||
| label="合格" | |||||
| sx={{ | |||||
| color: item.isPassed === true ? "green" : "inherit", | |||||
| "& .Mui-checked": {color: "green"} | |||||
| }} | |||||
| /> | |||||
| <FormControlLabel | |||||
| value="false" | |||||
| control={<Radio size="small" />} | |||||
| label="不合格" | |||||
| sx={{ | |||||
| color: item.isPassed === false ? "red" : "inherit", | |||||
| "& .Mui-checked": {color: "red"} | |||||
| }} | |||||
| /> | |||||
| </RadioGroup> | |||||
| </FormControl> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={!item.isPassed ? (item.failedQty ?? 0) : 0} | |||||
| disabled={item.isPassed} | |||||
| onChange={(e) => { | |||||
| 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' }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| size="small" | |||||
| value={item.remarks ?? ''} | |||||
| onChange={(e) => { | |||||
| const remarks = e.target.value; | |||||
| handleQcItemChange(index, 'remarks', remarks); | |||||
| }} | |||||
| sx={{ width: '280px' }} | |||||
| /> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ))} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Grid> | |||||
| {/* buttons */} | |||||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={formProps.handleSubmit(onSubmitQc)} | |||||
| > | |||||
| QC Accept | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| console.log("Sort to accept"); | |||||
| onClose?.(); | |||||
| }} | |||||
| > | |||||
| Sort to Accept | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| console.log("Reject and pick another lot"); | |||||
| onClose?.(); | |||||
| }} | |||||
| > | |||||
| Reject and Pick Another Lot | |||||
| </Button> | |||||
| </Stack> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| </Modal> | |||||
| </FormProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default PickQcStockInModalVer2; | |||||
| @@ -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<Partial<PutawayLine>, 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<Props> = ({ 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<PutawayInput>(); | |||||
| 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<NonNullable<ModalProps["onClose"]>>( | |||||
| (...args) => { | |||||
| setOpenScanner(false); | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const onOpenScanner = useCallback(() => { | |||||
| setOpenScanner(true); | |||||
| }, []); | |||||
| const onCloseScanner = useCallback(() => { | |||||
| setOpenScanner(false); | |||||
| }, []); | |||||
| const scannerConfig = useMemo<ScannerConfig>( | |||||
| () => ({ | |||||
| 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<GridColDef[]>( | |||||
| () => [ | |||||
| { | |||||
| 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<PutawayRow>): EntryError => { | |||||
| const error: EntryError = {}; | |||||
| const { qty, warehouseId, printQty } = newRow; | |||||
| return Object.keys(error).length > 0 ? error : undefined; | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| return ( | |||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("Putaway Detail")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| spacing={2} | |||||
| sx={{ mt: 0.5 }} | |||||
| > | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| label={t("LotNo")} | |||||
| fullWidth | |||||
| value={itemDetail.lotNo} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Supplier")} | |||||
| fullWidth | |||||
| value={itemDetail.supplier} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Po Code")} | |||||
| fullWidth | |||||
| value={itemDetail.poCode} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("itemName")} | |||||
| fullWidth | |||||
| value={itemDetail.itemName} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("itemNo")} | |||||
| fullWidth | |||||
| value={itemDetail.itemNo} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("qty")} | |||||
| fullWidth | |||||
| value={itemDetail.acceptedQty} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("productionDate")} | |||||
| fullWidth | |||||
| value={ | |||||
| // dayjs(itemDetail.productionDate) | |||||
| dayjs() | |||||
| // .add(-1, "month") | |||||
| .format(OUTPUT_DATE_FORMAT)} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("expiryDate")} | |||||
| fullWidth | |||||
| value={ | |||||
| // dayjs(itemDetail.expiryDate) | |||||
| dayjs() | |||||
| .add(20, "day") | |||||
| .format(OUTPUT_DATE_FORMAT)} | |||||
| disabled | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <FormControl fullWidth> | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Warehouse")} | |||||
| disableClearable | |||||
| disabled | |||||
| fullWidth | |||||
| defaultValue={options[0]} /// modify this later | |||||
| // onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => ( | |||||
| <TextField {...params} label={t("Default Warehouse")} /> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| {/* <Grid item xs={5.5}> | |||||
| <TextField | |||||
| label={t("acceptedQty")} | |||||
| fullWidth | |||||
| {...register("acceptedQty", { | |||||
| required: "acceptedQty required!", | |||||
| min: 1, | |||||
| max: itemDetail.acceptedQty, | |||||
| valueAsNumber: true, | |||||
| })} | |||||
| // defaultValue={itemDetail.acceptedQty} | |||||
| disabled={disabled} | |||||
| error={Boolean(errors.acceptedQty)} | |||||
| helperText={errors.acceptedQty?.message} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={1}> | |||||
| <Button disabled={disabled} onClick={onOpenScanner}> | |||||
| {t("bind")} | |||||
| </Button> | |||||
| </Grid> */} | |||||
| {/* <Grid item xs={5.5}> | |||||
| <Controller | |||||
| control={control} | |||||
| name="warehouseId" | |||||
| render={({ field }) => { | |||||
| console.log(field); | |||||
| return ( | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Warehouse")} | |||||
| disableClearable | |||||
| fullWidth | |||||
| value={options.find((o) => o.value == field.value)} | |||||
| onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={"Select warehouse"} | |||||
| error={Boolean(errors.warehouseId?.message)} | |||||
| helperText={warehouseHelperText} | |||||
| // helperText={errors.warehouseId?.message} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| <FormControl fullWidth> | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Warehouse")} | |||||
| disableClearable | |||||
| fullWidth | |||||
| // value={warehouseId > 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) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| // label={"Select warehouse"} | |||||
| disabled={disabled} | |||||
| error={Boolean(errors.warehouseId?.message)} | |||||
| helperText={ | |||||
| errors.warehouseId?.message ?? getWarningTextHardcode() | |||||
| } | |||||
| // helperText={warehouseHelperText} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> */} | |||||
| <Grid | |||||
| item | |||||
| xs={12} | |||||
| style={{ display: "flex", justifyContent: "center" }} | |||||
| > | |||||
| {/* <QrCode content={qrContent} sx={{ width: 200, height: 200 }} /> */} | |||||
| <InputDataGrid<PutawayInput, PutawayLine, EntryError> | |||||
| apiRef={apiRef} | |||||
| checkboxSelection={false} | |||||
| _formKey={"putawayLine"} | |||||
| columns={columns} | |||||
| validateRow={validation} | |||||
| needAdd={true} | |||||
| showRemoveBtn={false} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| {/* <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| spacing={2} | |||||
| sx={{ mt: 0.5 }} | |||||
| > | |||||
| <Button onClick={onOpenScanner}>bind</Button> | |||||
| </Grid> */} | |||||
| <Modal open={isOpenScanner} onClose={closeHandler}> | |||||
| <Box sx={style}> | |||||
| <Typography variant="h4"> | |||||
| {t("Please scan warehouse qr code.")} | |||||
| </Typography> | |||||
| {/* <ReactQrCodeScanner scannerConfig={scannerConfig} /> */} | |||||
| </Box> | |||||
| </Modal> | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default PutawayForm; | |||||
| @@ -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<E> { | |||||
| _isNew: boolean; | |||||
| _error: E; | |||||
| } | |||||
| interface SelectionResult<E> { | |||||
| active: boolean; | |||||
| _isNew: boolean; | |||||
| _error: E; | |||||
| } | |||||
| type Result<E> = DefaultResult<E> | SelectionResult<E>; | |||||
| export type TableRow<V, E> = Partial< | |||||
| V & { | |||||
| isActive: boolean | undefined; | |||||
| _isNew: boolean; | |||||
| _error: E; | |||||
| } & ResultWithId | |||||
| >; | |||||
| export interface InputDataGridProps<T, V, E> { | |||||
| apiRef: MutableRefObject<GridApiCommunity>; | |||||
| // checkboxSelection: false | undefined; | |||||
| _formKey: keyof T; | |||||
| columns: GridColDef[]; | |||||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | |||||
| needAdd?: boolean; | |||||
| } | |||||
| export interface SelectionInputDataGridProps<T, V, E> { | |||||
| // thinking how do | |||||
| apiRef: MutableRefObject<GridApiCommunity>; | |||||
| // checkboxSelection: true; | |||||
| _formKey: keyof T; | |||||
| columns: GridColDef[]; | |||||
| validateRow: (newRow: GridRowModel<TableRow<V, E>>) => E; | |||||
| } | |||||
| export type Props<T, V, E> = | |||||
| | InputDataGridProps<T, V, E> | |||||
| | SelectionInputDataGridProps<T, V, E>; | |||||
| export class ProcessRowUpdateError<T, E> 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<T, V, E>({ | |||||
| apiRef, | |||||
| // checkboxSelection = false, | |||||
| _formKey, | |||||
| columns, | |||||
| validateRow, | |||||
| }: Props<T, V, E>) { | |||||
| const { | |||||
| t, | |||||
| // i18n: { language }, | |||||
| } = useTranslation("purchaseOrder"); | |||||
| const formKey = _formKey.toString(); | |||||
| const { setValue, getValues } = useFormContext(); | |||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||||
| // const apiRef = useGridApiRef(); | |||||
| const getRowId = useCallback<GridRowIdGetter<TableRow<V, E>>>( | |||||
| (row) => row.id! as number, | |||||
| [], | |||||
| ); | |||||
| const formValue = getValues(formKey) | |||||
| const list: TableRow<V, E>[] = !formValue || formValue.length == 0 ? dummyQCData : getValues(formKey); | |||||
| console.log(list) | |||||
| const [rows, setRows] = useState<TableRow<V, E>[]>(() => { | |||||
| // const list: TableRow<V, E>[] = 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<GridRowSelectionModel>(() => { | |||||
| // 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<T, E>) => { | |||||
| const errors = updateError.errors; | |||||
| const row = updateError.row; | |||||
| console.log(errors); | |||||
| apiRef.current.updateRows([{ ...row, _error: errors }]); | |||||
| }, | |||||
| [apiRef], | |||||
| ); | |||||
| const processRowUpdate = useCallback( | |||||
| ( | |||||
| newRow: GridRowModel<TableRow<V, E>>, | |||||
| originalRow: GridRowModel<TableRow<V, E>>, | |||||
| ) => { | |||||
| ///////////////// | |||||
| // 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<V, E>; /// 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<V, E>; | |||||
| 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<GridColDef[]>( | |||||
| () => [ | |||||
| ...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 [ | |||||
| <GridActionsCellItem | |||||
| icon={<SaveIcon />} | |||||
| label="Save" | |||||
| key="edit" | |||||
| sx={{ | |||||
| color: "primary.main", | |||||
| }} | |||||
| onClick={handleSave(id)} | |||||
| />, | |||||
| <GridActionsCellItem | |||||
| icon={<CancelIcon />} | |||||
| label="Cancel" | |||||
| key="edit" | |||||
| onClick={handleCancel(id)} | |||||
| />, | |||||
| ]; | |||||
| } | |||||
| return [ | |||||
| <GridActionsCellItem | |||||
| icon={<DeleteIcon />} | |||||
| 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 = ( | |||||
| <Box display="flex" gap={2} alignItems="center"> | |||||
| <Button | |||||
| disableRipple | |||||
| variant="outlined" | |||||
| startIcon={<Add />} | |||||
| onClick={addRow} | |||||
| size="small" | |||||
| > | |||||
| 新增 | |||||
| {/* {t("Add Record")} */} | |||||
| </Button> | |||||
| <Button | |||||
| disableRipple | |||||
| variant="outlined" | |||||
| startIcon={<Add />} | |||||
| onClick={reset} | |||||
| size="small" | |||||
| > | |||||
| {/* {t("Clean Record")} */} | |||||
| 清除 | |||||
| </Button> | |||||
| </Box> | |||||
| ); | |||||
| // const handleRowEditStop: GridEventListener<'rowEditStop'> = (params, event) => { | |||||
| // if (params.reason === GridRowEditStopReasons.rowFocusOut) { | |||||
| // event.defaultMuiPrevented = true; | |||||
| // } | |||||
| // }; | |||||
| return ( | |||||
| <StyledDataGrid | |||||
| // {...props} | |||||
| // getRowId={getRowId as GridRowIdGetter<GridValidRowModel>} | |||||
| 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<TableRow<T, E>>) => { | |||||
| let classname = ""; | |||||
| if (params.row._error) { | |||||
| classname = "hasError"; | |||||
| } | |||||
| return classname; | |||||
| }} | |||||
| slots={{ | |||||
| // footer: FooterToolbar, | |||||
| noRowsOverlay: NoRowsOverlay, | |||||
| }} | |||||
| // slotProps={{ | |||||
| // footer: { child: footer }, | |||||
| // } | |||||
| // } | |||||
| /> | |||||
| ); | |||||
| } | |||||
| const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => { | |||||
| return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>; | |||||
| }; | |||||
| const NoRowsOverlay: React.FC = () => { | |||||
| const { t } = useTranslation("home"); | |||||
| return ( | |||||
| <Box | |||||
| display="flex" | |||||
| justifyContent="center" | |||||
| alignItems="center" | |||||
| height="100%" | |||||
| > | |||||
| <Typography variant="caption">{t("Add some entries!")}</Typography> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default InputDataGrid; | |||||
| @@ -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<SetStateAction<QcData[]>> | |||||
| } | |||||
| type EntryError = | |||||
| | { | |||||
| [field in keyof QcData]?: string; | |||||
| } | |||||
| | undefined; | |||||
| type QcRow = TableRow<Partial<QcData>, EntryError>; | |||||
| // fetchQcItemCheck | |||||
| const QcFormVer2: React.FC<Props> = ({ 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<PurchaseQCInput>(); | |||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>(); | |||||
| 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<GridColDef[]>( | |||||
| () => [ | |||||
| { | |||||
| field: "escalation", | |||||
| headerName: t("escalation"), | |||||
| flex: 1, | |||||
| }, | |||||
| { | |||||
| field: "supervisor", | |||||
| headerName: t("supervisor"), | |||||
| flex: 1, | |||||
| }, | |||||
| ], [] | |||||
| ) | |||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
| (_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<GridColDef[]>( | |||||
| () => [ | |||||
| { | |||||
| field: "escalation", | |||||
| headerName: t("escalation"), | |||||
| flex: 1, | |||||
| }, | |||||
| { | |||||
| field: "supervisor", | |||||
| headerName: t("supervisor"), | |||||
| flex: 1, | |||||
| }, | |||||
| ], | |||||
| [], | |||||
| ); | |||||
| /// validate datagrid | |||||
| const validation = useCallback( | |||||
| (newRow: GridRowModel<QcRow>): 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<HTMLInputElement>) => { | |||||
| apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); | |||||
| apiRef.current.stopCellEditMode({ id, field }); // commit immediately | |||||
| }; | |||||
| return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | |||||
| } | |||||
| const qcColumns: GridColDef[] = [ | |||||
| { | |||||
| field: "qcItem", | |||||
| headerName: t("qcItem"), | |||||
| flex: 2, | |||||
| renderCell: (params) => ( | |||||
| <Box> | |||||
| <b>{params.value}</b><br/> | |||||
| {params.row.qcDescription}<br/> | |||||
| </Box> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| field: 'isPassed', | |||||
| headerName: t("qcResult"), | |||||
| flex: 1.5, | |||||
| renderCell: (params) => { | |||||
| const currentValue = params.value; | |||||
| return ( | |||||
| <FormControl> | |||||
| <RadioGroup | |||||
| row | |||||
| aria-labelledby="demo-radio-buttons-group-label" | |||||
| value={currentValue === undefined ? "" : (currentValue ? "true" : "false")} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value; | |||||
| setQcItems((prev) => | |||||
| prev.map((r): QcData => (r.id === params.id ? { ...r, isPassed: value === "true" } : r)) | |||||
| ); | |||||
| }} | |||||
| name={`isPassed-${params.id}`} | |||||
| > | |||||
| <FormControlLabel | |||||
| value="true" | |||||
| control={<Radio />} | |||||
| label="合格" | |||||
| sx={{ | |||||
| color: currentValue === true ? "green" : "inherit", | |||||
| "& .Mui-checked": {color: "green"} | |||||
| }} | |||||
| /> | |||||
| <FormControlLabel | |||||
| value="false" | |||||
| control={<Radio />} | |||||
| label="不合格" | |||||
| sx={{ | |||||
| color: currentValue === false ? "red" : "inherit", | |||||
| "& .Mui-checked": {color: "red"} | |||||
| }} | |||||
| /> | |||||
| </RadioGroup> | |||||
| </FormControl> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "failedQty", | |||||
| headerName: t("failedQty"), | |||||
| flex: 1, | |||||
| // editable: true, | |||||
| renderCell: (params) => ( | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={!params.row.isPassed? (params.value ?? '') : '0'} | |||||
| disabled={params.row.isPassed} | |||||
| onChange={(e) => { | |||||
| 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) => ( | |||||
| <TextField | |||||
| size="small" | |||||
| value={params.value ?? ''} | |||||
| onChange={(e) => { | |||||
| 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<boolean>(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<HTMLInputElement>) => { | |||||
| // 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 ( | |||||
| <> | |||||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||||
| <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| spacing={2} | |||||
| sx={{ mt: 0.5 }} | |||||
| > | |||||
| <Grid item xs={12}> | |||||
| <Tabs | |||||
| value={tabIndex} | |||||
| onChange={handleTabChange} | |||||
| variant="scrollable" | |||||
| > | |||||
| <Tab label={t("QC Info")} iconPosition="end" /> | |||||
| <Tab label={t("Escalation History")} iconPosition="end" /> | |||||
| </Tabs> | |||||
| </Grid> | |||||
| {tabIndex == 0 && ( | |||||
| <> | |||||
| <Grid item xs={12}> | |||||
| {/* <QcDataGrid<ModalFormInput, QcData, EntryError> | |||||
| apiRef={apiRef} | |||||
| columns={qcColumns} | |||||
| _formKey="qcResult" | |||||
| validateRow={validation} | |||||
| /> */} | |||||
| <StyledDataGrid | |||||
| columns={qcColumns} | |||||
| rows={qcItems} | |||||
| autoHeight | |||||
| /> | |||||
| </Grid> | |||||
| {/* <Grid item xs={12}> | |||||
| <EscalationComponent | |||||
| forSupervisor={false} | |||||
| isCollapsed={isCollapsed} | |||||
| setIsCollapsed={setIsCollapsed} | |||||
| /> | |||||
| </Grid> */} | |||||
| </> | |||||
| )} | |||||
| {tabIndex == 1 && ( | |||||
| <> | |||||
| {/* <Grid item xs={12}> | |||||
| <StockInFormVer2 | |||||
| itemDetail={itemDetail} | |||||
| disabled={false} | |||||
| /> | |||||
| </Grid> */} | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("Escalation Info")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <StyledDataGrid | |||||
| rows={escalationHistory} | |||||
| columns={columns} | |||||
| onRowSelectionModelChange={(newRowSelectionModel) => { | |||||
| setRowSelectionModel(newRowSelectionModel); | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| </> | |||||
| )} | |||||
| <Grid item xs={12}> | |||||
| <FormControl> | |||||
| <Controller | |||||
| name="qcAccept" | |||||
| control={control} | |||||
| defaultValue={true} | |||||
| render={({ field }) => ( | |||||
| <RadioGroup | |||||
| row | |||||
| aria-labelledby="demo-radio-buttons-group-label" | |||||
| {...field} | |||||
| value={field.value?.toString() || "true"} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value === 'true'; | |||||
| if (!value && Boolean(errors.acceptQty)) { | |||||
| setValue("acceptQty", itemDetail.acceptedQty); | |||||
| } | |||||
| field.onChange(value); | |||||
| }} | |||||
| > | |||||
| <FormControlLabel value="true" control={<Radio />} label="接受" /> | |||||
| <Box sx={{mr:2}}> | |||||
| <TextField | |||||
| type="number" | |||||
| label={t("acceptQty")} | |||||
| sx={{ width: '150px' }} | |||||
| defaultValue={accQty} | |||||
| disabled={!qcAccept} | |||||
| {...register("acceptQty", { | |||||
| required: "acceptQty required!", | |||||
| })} | |||||
| error={Boolean(errors.acceptQty)} | |||||
| helperText={errors.acceptQty?.message} | |||||
| /> | |||||
| </Box> | |||||
| <FormControlLabel value="false" control={<Radio />} label="不接受及上報" /> | |||||
| </RadioGroup> | |||||
| )} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| {/* <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("Escalation Result")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <EscalationComponent | |||||
| forSupervisor={true} | |||||
| isCollapsed={isCollapsed} | |||||
| setIsCollapsed={setIsCollapsed} | |||||
| /> | |||||
| </Grid> */} | |||||
| </Grid> | |||||
| </Grid> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default QcFormVer2; | |||||
| @@ -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<void>; | |||||
| // multiple: false; | |||||
| } | |||||
| type Props = SingleAutocompleteProps; | |||||
| const QcSelect: React.FC<Props> = ({ 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 ( | |||||
| <Autocomplete | |||||
| noOptionsText={t("No Qc")} | |||||
| disableClearable | |||||
| fullWidth | |||||
| value={currentValue} | |||||
| onChange={onChange} | |||||
| getOptionLabel={(option) => option.label} | |||||
| options={options} | |||||
| renderInput={(params) => <TextField {...params} error={error} />} | |||||
| /> | |||||
| ); | |||||
| }; | |||||
| export default QcSelect; | |||||
| @@ -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<Partial<PurchaseQcResult>, EntryError>; | |||||
| const StockInFormVer2: React.FC<Props> = ({ | |||||
| // 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<StockInInput>(); | |||||
| // 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 ( | |||||
| <Grid container spacing={2}> | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("stock in information")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("itemNo")} | |||||
| fullWidth | |||||
| {...register("itemNo", { | |||||
| required: "itemNo required!", | |||||
| })} | |||||
| value={getItemDisplayValue()} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("itemName")} | |||||
| fullWidth | |||||
| {...register("itemName", { | |||||
| required: "itemName required!", | |||||
| })} | |||||
| value={getItemNameDisplayValue()} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <Controller | |||||
| name="productionDate" | |||||
| control={control} | |||||
| rules={{ | |||||
| required: "productionDate required!", | |||||
| }} | |||||
| render={({ field }) => { | |||||
| return ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale={`${language}-hk`} | |||||
| > | |||||
| <DatePicker | |||||
| {...field} | |||||
| sx={{ width: "100%" }} | |||||
| label={t("productionDate")} | |||||
| value={productionDate ? dayjs(productionDate) : undefined} | |||||
| disabled={disabled} | |||||
| onChange={(date) => { | |||||
| 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, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <Controller | |||||
| name="expiryDate" | |||||
| control={control} | |||||
| rules={{ | |||||
| required: "expiryDate required!", | |||||
| }} | |||||
| render={({ field }) => { | |||||
| return ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale={`${language}-hk`} | |||||
| > | |||||
| <DatePicker | |||||
| {...field} | |||||
| sx={{ width: "100%" }} | |||||
| label={t("expiryDate")} | |||||
| value={expiryDate ? dayjs(expiryDate) : undefined} | |||||
| disabled={disabled} | |||||
| onChange={(date) => { | |||||
| 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, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | |||||
| ); | |||||
| }} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("receivedQty")} | |||||
| fullWidth | |||||
| {...register("receivedQty", { | |||||
| required: "receivedQty required!", | |||||
| })} | |||||
| value={getQuantityDisplayValue()} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("uom")} | |||||
| fullWidth | |||||
| {...register("uom", { | |||||
| required: "uom required!", | |||||
| })} | |||||
| value={getUomDisplayValue()} | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("acceptedQty")} | |||||
| fullWidth | |||||
| {...register("acceptedQty", { | |||||
| required: "acceptedQty required!", | |||||
| })} | |||||
| value={getQuantityDisplayValue()} | |||||
| disabled={true} | |||||
| // disabled={disabled} | |||||
| // error={Boolean(errors.acceptedQty)} | |||||
| // helperText={errors.acceptedQty?.message} | |||||
| /> | |||||
| </Grid> | |||||
| {/* <Grid item xs={4}> | |||||
| <TextField | |||||
| label={t("acceptedWeight")} | |||||
| fullWidth | |||||
| // {...register("acceptedWeight", { | |||||
| // required: "acceptedWeight required!", | |||||
| // })} | |||||
| disabled={disabled} | |||||
| error={Boolean(errors.acceptedWeight)} | |||||
| helperText={errors.acceptedWeight?.message} | |||||
| /> | |||||
| </Grid> */} | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default StockInFormVer2; | |||||
| @@ -0,0 +1,24 @@ | |||||
| import { Box, Tooltip } from "@mui/material"; | |||||
| import React from "react"; | |||||
| const TwoLineCell: React.FC<{ children: React.ReactNode }> = ({ children }) => { | |||||
| return ( | |||||
| <Tooltip title={children}> | |||||
| <Box | |||||
| sx={{ | |||||
| whiteSpace: "normal", | |||||
| overflow: "hidden", | |||||
| textOverflow: "ellipsis", | |||||
| display: "-webkit-box", | |||||
| WebkitLineClamp: 2, | |||||
| WebkitBoxOrient: "vertical", | |||||
| lineHeight: "22px", | |||||
| }} | |||||
| > | |||||
| {children} | |||||
| </Box> | |||||
| </Tooltip> | |||||
| ); | |||||
| }; | |||||
| export default TwoLineCell; | |||||
| @@ -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 | |||||
| } | |||||
| ] | |||||
| @@ -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<string, any>; | |||||
| searchQuery?: Record<string, any>; | |||||
| }; | |||||
| // 扩展表单类型以包含搜索字段 | |||||
| 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<Props> = ({ filterArgs, searchQuery }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| const [items, setItems] = useState<ItemCombo[]>([]); | |||||
| const [filteredItems, setFilteredItems] = useState<ItemCombo[]>([]); | |||||
| const [createdItems, setCreatedItems] = useState<CreatedItem[]>([]); | |||||
| 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<SearchFormData>(); | |||||
| 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<SubmitHandler<SearchFormData>>( | |||||
| 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<HTMLInputElement>, | |||||
| ) => { | |||||
| 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<ItemCombo>[] = useMemo(() => [ | |||||
| { | |||||
| name: "id", | |||||
| label: "", | |||||
| type: "checkbox", | |||||
| disabled: (item) => isItemInCreated(item.id), // Disable if already in created items | |||||
| }, | |||||
| { | |||||
| name: "label", | |||||
| label: t("Item"), | |||||
| renderCell: (item) => ( | |||||
| <Box> | |||||
| <Typography variant="body2">{item.label}</Typography> | |||||
| <Typography variant="caption" color="textSecondary"> | |||||
| ID: {item.id} | |||||
| </Typography> | |||||
| </Box> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| 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 ( | |||||
| <FormProvider {...formProps}> | |||||
| <Box | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit)} | |||||
| > | |||||
| <Grid container spacing={2}> | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("Pick Order Detail")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| {/* 隐藏搜索条件区域 */} | |||||
| {/* | |||||
| <Grid item xs={2}> | |||||
| <FormControl fullWidth> | |||||
| <Autocomplete | |||||
| disableClearable | |||||
| fullWidth | |||||
| getOptionLabel={(option) => option.type} | |||||
| options={typeList} | |||||
| onChange={handleTypeChange} | |||||
| renderInput={(params) => <TextField {...params} label={t("type")} required/>} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| <Grid item xs={2}> | |||||
| <Controller | |||||
| control={formProps.control} | |||||
| name="searchCode" | |||||
| render={({ field }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| fullWidth | |||||
| label={t("code")} | |||||
| placeholder={t("Enter item code")} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={2}> | |||||
| <Controller | |||||
| control={formProps.control} | |||||
| name="searchName" | |||||
| render={({ field }) => ( | |||||
| <TextField | |||||
| {...field} | |||||
| fullWidth | |||||
| label={t("name")} | |||||
| placeholder={t("Enter item name")} | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <Controller | |||||
| control={formProps.control} | |||||
| name="targetDate" | |||||
| render={({ field }) => ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale="zh-hk" | |||||
| > | |||||
| <DatePicker | |||||
| {...field} | |||||
| sx={{ width: "100%" }} | |||||
| label={t("targetDate")} | |||||
| value={targetDate ? dayjs(targetDate) : undefined} | |||||
| onChange={(date) => { | |||||
| 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, | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </LocalizationProvider> | |||||
| )} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={3}> | |||||
| <Button | |||||
| variant="contained" | |||||
| startIcon={<Search />} | |||||
| onClick={handleSearch} | |||||
| disabled={!type || (!searchCode && !searchName) || isLoading} | |||||
| fullWidth | |||||
| sx={{ height: '56px' }} | |||||
| > | |||||
| {isLoading ? t("Searching...") : t("Search")} | |||||
| </Button> | |||||
| </Grid> | |||||
| */} | |||||
| </Grid> | |||||
| {/* 创建项目区域 */} | |||||
| {createdItems.length > 0 && ( | |||||
| <Box sx={{ mt: 3 }}> | |||||
| <Typography variant="h6" marginBlockEnd={2}> | |||||
| {t("Created Items")} ({createdItems.length}) | |||||
| </Typography> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell padding="checkbox"> | |||||
| <Typography variant="subtitle2">{t("Select")}</Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="subtitle2">{t("Item")}</Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="subtitle2">{t("Order Quantity")}</Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="subtitle2">{t("Unit")}</Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {createdItems.map((item) => ( | |||||
| <TableRow key={item.itemId}> | |||||
| <TableCell padding="checkbox"> | |||||
| <Checkbox | |||||
| checked={item.isSelected} | |||||
| onChange={(e) => handleCreatedItemSelect(item.itemId, e.target.checked)} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body2">{item.itemName}</Typography> | |||||
| <Typography variant="caption" color="textSecondary"> | |||||
| {item.itemCode} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={item.qty} | |||||
| onChange={(e) => handleQtyChange(item.itemId, Number(e.target.value))} | |||||
| inputProps={{ min: 1 }} | |||||
| sx={{ width: '80px' }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body2">{item.uom}</Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ))} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| </Box> | |||||
| )} | |||||
| {/* Search Results with SearchResults component */} | |||||
| {hasSearched && filteredItems.length > 0 && ( | |||||
| <Box sx={{ mt: 3 }}> | |||||
| <Typography variant="h6" marginBlockEnd={2}> | |||||
| {t("Search Results")} ({filteredItems.length}) | |||||
| {filteredItems.length >= 100 && ( | |||||
| <Typography variant="caption" color="textSecondary" sx={{ ml: 2 }}> | |||||
| {t("Showing first 100 results")} | |||||
| </Typography> | |||||
| )} | |||||
| </Typography> | |||||
| <SearchResults<ItemCombo> | |||||
| items={filteredItems} | |||||
| columns={searchItemColumns} | |||||
| totalCount={filteredItems.length} | |||||
| checkboxIds={selectedSearchItemIds} | |||||
| setCheckboxIds={handleSearchCheckboxChange} | |||||
| /> | |||||
| </Box> | |||||
| )} | |||||
| {/* 操作按钮 */} | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1} sx={{ mt: 3 }}> | |||||
| <Button | |||||
| name="submit" | |||||
| variant="contained" | |||||
| startIcon={<Check />} | |||||
| type="submit" | |||||
| disabled={createdItems.filter(item => item.isSelected).length === 0} | |||||
| > | |||||
| {t("submit")} | |||||
| </Button> | |||||
| <Button | |||||
| name="reset" | |||||
| variant="outlined" | |||||
| onClick={handleReset} | |||||
| > | |||||
| {t("reset")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Box> | |||||
| </FormProvider> | |||||
| ); | |||||
| }; | |||||
| export default NewCreateItem; | |||||
| @@ -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<ModalProps, "children"> { | |||||
| 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<Props> = ({ | |||||
| open, | |||||
| onClose, | |||||
| itemDetail, | |||||
| setItemDetail, | |||||
| qc, | |||||
| warehouse, | |||||
| }) => { | |||||
| console.log(warehouse); | |||||
| // 修改翻译键 | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation("pickOrder"); | |||||
| const [qcItems, setQcItems] = useState(dummyQCData) | |||||
| const formProps = useForm<any>({ | |||||
| defaultValues: { | |||||
| ...itemDetail, | |||||
| putawayLine: dummyPutawayLine, | |||||
| // receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT), | |||||
| // warehouseId: itemDetail.defaultWarehouseId || 0 | |||||
| }, | |||||
| }); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||||
| (...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<SubmitHandler<any>>( | |||||
| 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<SubmitHandler<any>>( | |||||
| 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<SubmitHandler<any>>( | |||||
| 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<SubmitHandler<any>>( | |||||
| 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 ( | |||||
| <> | |||||
| <FormProvider {...formProps}> | |||||
| <Modal open={open} onClose={closeHandler}> | |||||
| <Box | |||||
| sx={{ | |||||
| ...style, | |||||
| padding: 2, | |||||
| maxHeight: "90vh", | |||||
| overflowY: "auto", | |||||
| marginLeft: 3, | |||||
| marginRight: 3, | |||||
| }} | |||||
| > | |||||
| {openPutaway ? ( | |||||
| <Box | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmitPutaway)} | |||||
| > | |||||
| <PutawayForm | |||||
| itemDetail={itemDetail} | |||||
| warehouse={warehouse!} | |||||
| disabled={false} | |||||
| /> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| id="printButton" | |||||
| type="button" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| sx={{ mt: 1 }} | |||||
| onClick={onPrint} | |||||
| > | |||||
| {t("print")} | |||||
| </Button> | |||||
| <Button | |||||
| id="putawaySubmit" | |||||
| type="submit" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| sx={{ mt: 1 }} | |||||
| > | |||||
| {t("confirm putaway")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Box> | |||||
| ) : ( | |||||
| <> | |||||
| <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| > | |||||
| <Grid item xs={12}> | |||||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||||
| {t("qc processing")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <StockInFormVer2 itemDetail={itemDetail} disabled={false} /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| id="stockInSubmit" | |||||
| type="button" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| onClick={formProps.handleSubmit(onSubmitStockIn)} | |||||
| > | |||||
| {t("submitStockIn")} | |||||
| </Button> | |||||
| </Stack> | |||||
| <Grid | |||||
| container | |||||
| justifyContent="flex-start" | |||||
| alignItems="flex-start" | |||||
| > | |||||
| <QcFormVer2 | |||||
| qc={qc!} | |||||
| itemDetail={itemDetail} | |||||
| disabled={false} | |||||
| qcItems={qcItems} | |||||
| setQcItems={setQcItems} | |||||
| /> | |||||
| </Grid> | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| id="emailSupplier" | |||||
| type="button" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| sx={{ mt: 1 }} | |||||
| onClick={formProps.handleSubmit(onSubmitEmailSupplier)} | |||||
| > | |||||
| {t("email supplier")} | |||||
| </Button> | |||||
| <Button | |||||
| id="qcSubmit" | |||||
| type="button" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| sx={{ mt: 1 }} | |||||
| onClick={formProps.handleSubmit(onSubmitQc)} | |||||
| > | |||||
| {t("confirm putaway")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </> | |||||
| )} | |||||
| </Box> | |||||
| </Modal> | |||||
| </FormProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default PickQcStockInModalVer2; | |||||