diff --git a/src/app/api/dashboard/actions.ts b/src/app/api/dashboard/actions.ts index f3c4e6a..df54e1c 100644 --- a/src/app/api/dashboard/actions.ts +++ b/src/app/api/dashboard/actions.ts @@ -30,6 +30,9 @@ export interface StockInLineEntry { acceptedQty: number; status?: string; expiryDate?: string; + productLotNo?: string; + receiptDate?: string; + dnDate?: string; } export interface PurchaseQcResult { @@ -87,6 +90,7 @@ export const fetchStockInLineInfo = cache(async (stockInLineId: number) => { }); export const createStockInLine = async (data: StockInLineEntry) => { + console.log(data) const stockInLine = await serverFetchJson< PostStockInLiineResponse >(`${BASE_API_URL}/stockInLine/create`, { diff --git a/src/app/api/po/index.ts b/src/app/api/po/index.ts index 07e010b..1a0f944 100644 --- a/src/app/api/po/index.ts +++ b/src/app/api/po/index.ts @@ -60,6 +60,8 @@ export interface StockInLine { poCode: string; uom: Uom; defaultWarehouseId: number; // id for now + dnNo: string; + dnDate: number[]; } export const fetchPoList = cache(async (queryParams?: Record) => { diff --git a/src/app/api/settings/item/actions.ts b/src/app/api/settings/item/actions.ts index 8012d3f..35ce6d1 100644 --- a/src/app/api/settings/item/actions.ts +++ b/src/app/api/settings/item/actions.ts @@ -56,6 +56,7 @@ export interface ItemCombo { label: string, uomId: number, uom: string, + group?: string, } export const fetchAllItemsInClient = cache(async () => { diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 31142c9..21e61f0 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -76,6 +76,10 @@ export const dayjsToInputDateTimeString = (date: Dayjs) => { return date.format(`${INPUT_DATE_FORMAT}T${OUTPUT_TIME_FORMAT}`); }; +export const outputDateStringToInputDateString = (date: string) => { + return dayjsToInputDateString(dateStringToDayjs(date)) +} + export const stockInLineStatusMap: { [status: string]: number } = { draft: 0, pending: 1, diff --git a/src/components/PoDetail/EscalationComponent.tsx b/src/components/PoDetail/EscalationComponent.tsx index 53761a8..12e0b81 100644 --- a/src/components/PoDetail/EscalationComponent.tsx +++ b/src/components/PoDetail/EscalationComponent.tsx @@ -61,7 +61,7 @@ const EscalationComponent: React.FC = ({ ]; const handleInputChange = ( - event: ChangeEvent | SelectChangeEvent + event: ChangeEvent | SelectChangeEvent ): void => { const { name, value } = event.target; setFormData((prev) => ({ diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index 1e67f5b..0380b91 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -28,6 +28,9 @@ import { Typography, Checkbox, FormControlLabel, + Card, + CardContent, + Radio, } from "@mui/material"; import { useTranslation } from "react-i18next"; // import InputDataGrid, { TableRow } from "../InputDataGrid/InputDataGrid"; @@ -57,7 +60,7 @@ import PoInputGrid from "./PoInputGrid"; import { QcItemWithChecks } from "@/app/api/qc"; import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { WarehouseResult } from "@/app/api/warehouse"; -import { calculateWeight, returnWeightUnit } from "@/app/utils/formatUtil"; +import { calculateWeight, dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, returnWeightUnit } from "@/app/utils/formatUtil"; import { CameraContext } from "../Cameras/CameraProvider"; import PoQcStockInModal from "./PoQcStockInModal"; import QrModal from "./QrModal"; @@ -69,6 +72,11 @@ import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; import { fetchPoListClient } from "@/app/api/po/actions"; import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material"; import { createStockInLine } from "@/app/api/dashboard/actions"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import dayjs, { Dayjs } from "dayjs"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers"; +import { debounce } from "lodash"; //import { useRouter } from "next/navigation"; @@ -169,6 +177,11 @@ const PoSearchList: React.FC<{ ); }; +interface PolInputResult { + lotNo: string, + dnQty: number, +} + const PoDetail: React.FC = ({ po, qc, warehouse }) => { const cameras = useContext(CameraContext); console.log(cameras); @@ -178,10 +191,20 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { const [rows, setRows] = useState( purchaseOrder.pol || [], ); + const [polInputList, setPolInputList] = useState([]) + useEffect(() => { + setPolInputList( + (purchaseOrder.pol ?? []).map(() => ({ + lotNo: "", + dnQty: 0, + } as PolInputResult)) + ); + }, [purchaseOrder.pol]); + const pathname = usePathname() const searchParams = useSearchParams(); - const [row, setRow] = useState(rows[0]); + const [selectedRow, setSelectedRow] = useState(null); const [stockInLine, setStockInLine] = useState([]); const [processedQty, setProcessedQty] = useState(0); @@ -191,8 +214,13 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { const [selectedPoId, setSelectedPoId] = useState(po.id); const currentPoId = searchParams.get('id'); const selectedIdsParam = searchParams.get('selectedIds'); - const [selectedRowId, setSelectedRowId] = useState(null); - + // const [selectedRowId, setSelectedRowId] = useState(null); + const dnFormProps = useForm({ + defaultValues: { + dnNo: '', + dnDate: dayjsToDateString(dayjs()) + } + }) const fetchPoList = useCallback(async () => { try { if (selectedIdsParam) { @@ -217,11 +245,12 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { const fetchPoDetail = useCallback(async (poId: string) => { try { const result = await fetchPoInClient(parseInt(poId)); + console.log(result) if (result) { setPurchaseOrder(result); setRows(result.pol || []); if (result.pol && result.pol.length > 0) { - setRow(result.pol[0]); + setSelectedRow(result.pol[0]); setStockInLine(result.pol[0].stockInLine); setProcessedQty(result.pol[0].processed); } @@ -233,10 +262,15 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { const handlePoSelect = useCallback( async (selectedPo: PoResult) => { + if (selectedPo.id === selectedPoId) return; setSelectedPoId(selectedPo.id); await fetchPoDetail(selectedPo.id.toString()); const newSelectedIds = selectedIdsParam || selectedPo.id.toString(); - router.push(`/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`, { scroll: false }); + // router.push(`/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`, { scroll: false }); + const newUrl = `/po/edit?id=${selectedPo.id}&start=true&selectedIds=${newSelectedIds}`; + if (pathname + searchParams.toString() !== newUrl) { + router.replace(newUrl, { scroll: false }); + } }, [router, selectedIdsParam, fetchPoDetail] ); @@ -246,7 +280,7 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { setSelectedPoId(parseInt(currentPoId)); fetchPoDetail(currentPoId); } - }, [currentPoId, selectedPoId, fetchPoDetail]); + }, [currentPoId, fetchPoDetail]); useEffect(() => { fetchPoList(); @@ -257,13 +291,6 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { setSelectedPoId(parseInt(currentPoId)); } }, [currentPoId]); - - - - - - - const removeParam = (paramToRemove: string) => { const newParams = new URLSearchParams(searchParams.toString()); @@ -296,7 +323,7 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { function Row(props: { row: PurchaseOrderLine }) { const { row } = props; // const [firstReceiveQty, setFirstReceiveQty] = useState() - const [secondReceiveQty, setSecondReceiveQty] = useState() + // const [secondReceiveQty, setSecondReceiveQty] = useState() // const [open, setOpen] = useState(false); const [processedQty, setProcessedQty] = useState(row.processed); const [currStatus, setCurrStatus] = useState(row.status); @@ -309,6 +336,9 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { () => returnWeightUnit(row.uom), [row.uom], ); + const rowIndex = useMemo(() => { + return rows.findIndex((r) => r.id === row.id) + }, []) useEffect(() => { const polId = searchParams.get("polId") != null ? parseInt(searchParams.get("polId")!) : null @@ -328,21 +358,18 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { }, [processedQty, row.qty]); const handleRowSelect = () => { - setSelectedRowId(row.id); - setRow(row); + // setSelectedRowId(row.id); + setSelectedRow(row); setStockInLine(row.stockInLine); setProcessedQty(row.processed); }; const changeStockInLines = useCallback( (id: number) => { - console.log(id) //rows = purchaseOrderLine - console.log(rows) const target = rows.find((r) => r.id === id) const stockInLine = target!.stockInLine - console.log(stockInLine) setStockInLine(stockInLine) - setRow(target!) + setSelectedRow(target!) // console.log(pathname) // router.replace(`/po/edit?id=${item.poId}&polId=${item.polId}&stockInLineId=${item.stockInLineId}`); }, @@ -355,42 +382,64 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { // post stock in line const oldId = row.id; const postData = { + dnNo: dnFormProps.watch("dnNo"), + dnDate: outputDateStringToInputDateString(dnFormProps.watch("dnDate")), itemId: row.itemId, itemNo: row.itemNo, itemName: row.itemName, purchaseOrderId: row.purchaseOrderId, purchaseOrderLineId: row.id, - acceptedQty: secondReceiveQty || 0, + acceptedQty: polInputList[rowIndex].dnQty || 0, + productLotNo: polInputList[rowIndex].lotNo || '', + // acceptedQty: secondReceiveQty || 0, // acceptedQty: row.acceptedQty, }; - if (secondReceiveQty === 0) return + // if (secondReceiveQty === 0) return const res = await createStockInLine(postData); + if (res) { + fetchPoDetail(selectedPoId.toString()); + } console.log(res); }, 200); }, - [], + [polInputList, row, dnFormProps], ); - const handleChange = (e: React.ChangeEvent) => { - const raw = e.target.value; - - // Allow empty input - if (raw.trim() === '') { - setSecondReceiveQty(undefined); - return; - } - - // Keep digits only - const cleaned = raw.replace(/[^\d]/g, ''); - if (cleaned === '') { - // If the user typed only non-digits, keep previous value - return; - } - - // Parse and clamp to non-negative integer - const next = Math.max(0, Math.floor(Number(cleaned))); - setSecondReceiveQty(next); -}; + const handleChange = useCallback(debounce((e: React.ChangeEvent) => { + const raw = e.target.value; + const id = e.target.id + const temp = [...polInputList] + switch (id) { + case "lotNo": + if (raw.trim() === '') { + temp[rowIndex].lotNo = ''; + break; + } + temp[rowIndex].lotNo = raw.trim(); + break; + case "dnQty": + // Allow empty input + if (raw.trim() === '') { + temp[rowIndex].dnQty = 0; + break; + } + + // Keep digits only + const cleaned = raw.replace(/[^\d]/g, ''); + if (cleaned === '') { + // If the user typed only non-digits, keep previous value + break; + } + + // Parse and clamp to non-negative integer + const next = Math.max(0, Math.floor(Number(cleaned))); + temp[rowIndex].dnQty = next; + break; + default: + break; + } + setPolInputList(() => temp) + }, 300), [rowIndex]); return ( <> = ({ po, qc, warehouse }) => { */} - e.stopPropagation()} + e.stopPropagation()} /> {row.itemNo} @@ -425,16 +474,27 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { {decimalFormatter.format(totalWeight)} {weightUnit} */} {/* {weightUnit} */} - {decimalFormatter.format(row.price)} + {/* {decimalFormatter.format(row.price)} */} {/* {row.expiryDate} */} {t(`${currStatus.toLowerCase()}`)} {integerFormatter.format(row.receivedQty)} + + + = ({ po, qc, warehouse }) => { } }, [searchParams]) + const handleDatePickerChange = useCallback((value: Dayjs | null, onChange: (...event: any[]) => void) => { + if (value != null) { + const updatedValue = dayjsToDateString(value) + onChange(updatedValue) + } else { + onChange(value) + } +}, []) + return ( <> @@ -614,39 +683,75 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { {/* right side po info */} - - {true ? ( - - - - - - ) : undefined} + + + + + + {true ? ( + + + + + + + + + + + ( + + { + handleDatePickerChange(newValue, field.onChange) + }} + slotProps={{ textField: { fullWidth: true }}} + /> + )} + /> + + + {/* */} + + {/* */} + + + + + ) : undefined} + + @@ -666,9 +771,10 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { {t("processed")} {t("uom")} {/* {t("total weight")} */} - {`${t("price")} (HKD)`} + {/* {`${t("price")} (HKD)`} */} {t("status")} {renderFieldCondition(FIRST_IN_FIELD) ? {t("receivedQty")} : undefined} + {t("productLotNo")} {renderFieldCondition(SECOND_IN_FIELD) ? {t("dnQty")}(以訂單單位計算) : undefined} @@ -687,11 +793,11 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { - 已選擇: {selectedRowId ? row.itemNo : '無'} - {selectedRowId ? row.itemName : '無'} + {selectedRow ? `已選擇: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}}` : "未選擇貨品"} - {selectedRowId && ( + {selectedRow && ( @@ -704,7 +810,7 @@ const PoDetail: React.FC = ({ po, qc, warehouse }) => { stockInLine={stockInLine} setStockInLine={setStockInLine} setProcessedQty={setProcessedQty} - itemDetail={row} + itemDetail={selectedRow} warehouse={warehouse} /> diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index a903269..cc4482b 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -39,6 +39,7 @@ import { returnWeightUnit, calculateWeight, stockInLineStatusMap, + arrayToDateString, } from "@/app/utils/formatUtil"; // import PoQcStockInModal from "./PoQcStockInModal"; import NotificationImportantIcon from "@mui/icons-material/NotificationImportant"; @@ -443,17 +444,19 @@ const closeNewModal = useCallback(() => { field: "dnNo", headerName: t("dnNo"), width: 125, - renderCell: () => { - return <>DN0000001 - } + // renderCell: () => { + // return <>DN0000001 + // } // flex: 0.4, }, { field: "dnDate", headerName: t("dnDate"), width: 125, - renderCell: () => { - return <>07/08/2025 + renderCell: (params) => { + console.log(params.row) + // return <>07/08/2025 + return arrayToDateString(params.value) } // flex: 0.4, },