| @@ -76,7 +76,6 @@ import { Controller, FormProvider, useForm } from "react-hook-form"; | |||||
| import dayjs, { Dayjs } from "dayjs"; | import dayjs, { Dayjs } from "dayjs"; | ||||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
| import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers"; | import { DatePicker, LocalizationProvider, zhHK } from "@mui/x-date-pickers"; | ||||
| import { debounce } from "lodash"; | |||||
| import LoadingComponent from "../General/LoadingComponent"; | import LoadingComponent from "../General/LoadingComponent"; | ||||
| import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions"; | import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions"; | ||||
| import { PrinterCombo } from "@/app/api/settings/printer"; | import { PrinterCombo } from "@/app/api/settings/printer"; | ||||
| @@ -233,7 +232,7 @@ const PoSearchList: React.FC<{ | |||||
| interface PolInputResult { | interface PolInputResult { | ||||
| lotNo: string, | lotNo: string, | ||||
| dnQty: number, | |||||
| dnQty: string, | |||||
| } | } | ||||
| const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | ||||
| @@ -250,15 +249,19 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| const [rows, setRows] = useState<PurchaseOrderLine[]>( | const [rows, setRows] = useState<PurchaseOrderLine[]>( | ||||
| purchaseOrder.pol || [], | purchaseOrder.pol || [], | ||||
| ); | ); | ||||
| const [polInputList, setPolInputList] = useState<PolInputResult[]>([]) | |||||
| const [polInputList, setPolInputList] = useState<Record<number, PolInputResult>>({}) | |||||
| const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; | const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; | ||||
| useEffect(() => { | useEffect(() => { | ||||
| setPolInputList( | |||||
| (purchaseOrder.pol ?? []).map(() => ({ | |||||
| lotNo: "", | |||||
| dnQty: 0, | |||||
| } as PolInputResult)) | |||||
| ); | |||||
| setPolInputList((prev) => { | |||||
| const next: Record<number, PolInputResult> = {}; | |||||
| (purchaseOrder.pol ?? []).forEach((pol) => { | |||||
| next[pol.id] = prev[pol.id] ?? { | |||||
| lotNo: "", | |||||
| dnQty: "", | |||||
| }; | |||||
| }); | |||||
| return next; | |||||
| }); | |||||
| }, [purchaseOrder.pol]); | }, [purchaseOrder.pol]); | ||||
| useEffect(() => { | useEffect(() => { | ||||
| try { | try { | ||||
| @@ -335,7 +338,7 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| }, [selectedIdsParam]); | }, [selectedIdsParam]); | ||||
| const fetchPoDetail = useCallback(async (poId: string, preserveDnNo: boolean = false) => { | |||||
| const fetchPoDetail = useCallback(async (poId: string, preserveDnNo: boolean = false, preferredPolId?: number) => { | |||||
| try { | try { | ||||
| const result = await fetchPoInClient(parseInt(poId)); | const result = await fetchPoInClient(parseInt(poId)); | ||||
| if (result) { | if (result) { | ||||
| @@ -348,16 +351,12 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| }); | }); | ||||
| setRows(result.pol || []); | setRows(result.pol || []); | ||||
| if (result.pol && result.pol.length > 0) { | if (result.pol && result.pol.length > 0) { | ||||
| if (result.id === selectedPoId && selectedRow?.id) { | |||||
| const polIndex = result.pol.findIndex((p) => p.id === selectedRow?.id) | |||||
| // setSelectedRow(result.pol[polIndex]); | |||||
| setStockInLine(result.pol[polIndex].stockInLine); | |||||
| setProcessedQty(result.pol[polIndex].processed); | |||||
| } else { | |||||
| // setSelectedRow(result.pol[0]); | |||||
| setStockInLine(result.pol[0].stockInLine); | |||||
| setProcessedQty(result.pol[0].processed); | |||||
| } | |||||
| const targetPolId = preferredPolId ?? selectedRow?.id; | |||||
| const targetPol = | |||||
| result.pol.find((p) => p.id === targetPolId) ?? result.pol[0]; | |||||
| setSelectedRow(targetPol); | |||||
| setStockInLine(targetPol.stockInLine); | |||||
| setProcessedQty(targetPol.processed); | |||||
| } | } | ||||
| // if (focusField) {console.log(focusField);focusField.focus();} | // if (focusField) {console.log(focusField);focusField.focus();} | ||||
| } | } | ||||
| @@ -446,6 +445,8 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| // const [open, setOpen] = useState(false); | // const [open, setOpen] = useState(false); | ||||
| const [processedQty, setProcessedQty] = useState(row.processed); | const [processedQty, setProcessedQty] = useState(row.processed); | ||||
| const [currStatus, setCurrStatus] = useState(row.status); | const [currStatus, setCurrStatus] = useState(row.status); | ||||
| const [lotNoInput, setLotNoInput] = useState(polInputList[row.id]?.lotNo ?? ""); | |||||
| const [dnQtyInput, setDnQtyInput] = useState(polInputList[row.id]?.dnQty ?? ""); | |||||
| // const [stockInLine, setStockInLine] = useState(row.stockInLine); | // const [stockInLine, setStockInLine] = useState(row.stockInLine); | ||||
| const totalWeight = useMemo( | const totalWeight = useMemo( | ||||
| () => calculateWeight(row.qty, row.uom), | () => calculateWeight(row.qty, row.uom), | ||||
| @@ -455,10 +456,6 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| () => returnWeightUnit(row.uom), | () => returnWeightUnit(row.uom), | ||||
| [row.uom], | [row.uom], | ||||
| ); | ); | ||||
| const rowIndex = useMemo(() => { | |||||
| return rows.findIndex((r) => r.id === row.id) | |||||
| }, []) | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const polId = searchParams.get("polId") != null ? parseInt(searchParams.get("polId")!) : null | const polId = searchParams.get("polId") != null ? parseInt(searchParams.get("polId")!) : null | ||||
| if (polId) { | if (polId) { | ||||
| @@ -479,6 +476,11 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| } | } | ||||
| }, [processedQty, row.qty, row.stockUom?.stockQty]); | }, [processedQty, row.qty, row.stockUom?.stockQty]); | ||||
| useEffect(() => { | |||||
| setLotNoInput(polInputList[row.id]?.lotNo ?? ""); | |||||
| setDnQtyInput(polInputList[row.id]?.dnQty ?? ""); | |||||
| }, [polInputList, row.id]); | |||||
| const handleRowSelect = () => { | const handleRowSelect = () => { | ||||
| // setSelectedRowId(row.id); | // setSelectedRowId(row.id); | ||||
| setSelectedRow(row); | setSelectedRow(row); | ||||
| @@ -501,7 +503,7 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| const handleStart = useCallback( | const handleStart = useCallback( | ||||
| () => { | () => { | ||||
| const orderQty = Number(row?.qty) ?? 0; | const orderQty = Number(row?.qty) ?? 0; | ||||
| const acceptedQty = Number(polInputList[rowIndex].dnQty); | |||||
| const acceptedQty = Number(dnQtyInput.trim()); | |||||
| if (isNaN(acceptedQty) || acceptedQty <= 0) { | if (isNaN(acceptedQty) || acceptedQty <= 0) { | ||||
| alert("來貨數量必須大於0!"); | alert("來貨數量必須大於0!"); | ||||
| @@ -518,11 +520,18 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| itemName: row.itemName, | itemName: row.itemName, | ||||
| purchaseOrderLineId: row.id, | purchaseOrderLineId: row.id, | ||||
| acceptedQty: acceptedQty, | acceptedQty: acceptedQty, | ||||
| productLotNo: polInputList[rowIndex].lotNo || '', | |||||
| productLotNo: lotNoInput || "", | |||||
| }; | }; | ||||
| const res = await createStockInLine(postData); | const res = await createStockInLine(postData); | ||||
| if (res) { | if (res) { | ||||
| fetchPoDetail(selectedPoId.toString(), true); | |||||
| setLotNoInput(""); | |||||
| setDnQtyInput(""); | |||||
| setPolInputList((prev) => ({ | |||||
| ...prev, | |||||
| [row.id]: { lotNo: "", dnQty: "" }, | |||||
| })); | |||||
| setSelectedRow(row); | |||||
| fetchPoDetail(selectedPoId.toString(), true, row.id); | |||||
| } | } | ||||
| console.log(res); | console.log(res); | ||||
| }, 200); | }, 200); | ||||
| @@ -539,57 +548,24 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| doSubmit(); | doSubmit(); | ||||
| } | } | ||||
| }, | }, | ||||
| [polInputList, row, rowIndex, dnFormProps, selectedPoId, fetchPoDetail, t], | |||||
| [dnQtyInput, row, dnFormProps, selectedPoId, fetchPoDetail, t, lotNoInput], | |||||
| ); | ); | ||||
| const handleChange = useCallback(debounce((e: React.ChangeEvent<HTMLInputElement>) => { | |||||
| const raw = e.target.value; | |||||
| const id = e.target.id | |||||
| // const temp = polInputList | |||||
| switch (id) { | |||||
| case "lotNo": | |||||
| if (raw.trim() === '') { | |||||
| polInputList[rowIndex].lotNo = ''; | |||||
| break; | |||||
| } | |||||
| polInputList[rowIndex].lotNo = raw.trim(); | |||||
| break; | |||||
| case "dnQty": | |||||
| // Allow empty input | |||||
| const trimmed = raw.trim(); | |||||
| if (trimmed === "") { | |||||
| polInputList[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; | |||||
| } | |||||
| */ | |||||
| const parsed = Number(trimmed); | |||||
| // 不是合法數字(例如 "abc") | |||||
| if (!Number.isFinite(parsed)) { | |||||
| polInputList[rowIndex].dnQty = 0; // 或保留舊值,看你需求 | |||||
| break; | |||||
| } | |||||
| polInputList[rowIndex].dnQty = parsed; // 這裡允許小數,先存起來 | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| // setPolInputList(() => temp) | |||||
| }, 300), [rowIndex]); | |||||
| const syncRowInputToParent = useCallback((lotNo: string, dnQty: string) => { | |||||
| setPolInputList((prev) => { | |||||
| const current = prev[row.id] ?? { lotNo: "", dnQty: "" }; | |||||
| if (current.lotNo === lotNo && current.dnQty === dnQty) return prev; | |||||
| return { | |||||
| ...prev, | |||||
| [row.id]: { lotNo, dnQty }, | |||||
| }; | |||||
| }); | |||||
| }, [row.id]); | |||||
| // const [focusField, setFocusField] = useState<HTMLInputElement>(); | // const [focusField, setFocusField] = useState<HTMLInputElement>(); | ||||
| // 本批收貨數量(訂單單位): 使用者在該行輸入的 dnQty | // 本批收貨數量(訂單單位): 使用者在該行輸入的 dnQty | ||||
| const batchPurchaseQty = polInputList[rowIndex]?.dnQty ?? 0; | |||||
| const batchPurchaseQty = Number(dnQtyInput.trim()) || 0; | |||||
| // 已來貨總數(庫存單位): 同一 POL 底下所有 stock_in_line.acceptedQty 的合計 | // 已來貨總數(庫存單位): 同一 POL 底下所有 stock_in_line.acceptedQty 的合計 | ||||
| const totalStockReceived = row.stockInLine | const totalStockReceived = row.stockInLine | ||||
| @@ -686,8 +662,9 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| label="輸入貨品批號" | label="輸入貨品批號" | ||||
| type="text" // Use type="text" to allow validation in the change handler | type="text" // Use type="text" to allow validation in the change handler | ||||
| variant="outlined" | variant="outlined" | ||||
| defaultValue={polInputList[rowIndex]?.lotNo ?? ''} | |||||
| onChange={handleChange} | |||||
| value={lotNoInput} | |||||
| onChange={(e) => setLotNoInput(e.target.value)} | |||||
| onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)} | |||||
| // onFocus={(e) => {setFocusField(e.target as HTMLInputElement);}} | // onFocus={(e) => {setFocusField(e.target as HTMLInputElement);}} | ||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| @@ -697,8 +674,9 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| label="此批送貨數量" | label="此批送貨數量" | ||||
| type="text" // Use type="text" to allow validation in the change handler | type="text" // Use type="text" to allow validation in the change handler | ||||
| variant="outlined" | variant="outlined" | ||||
| defaultValue={polInputList[rowIndex]?.dnQty ?? ''} | |||||
| onChange={handleChange} | |||||
| value={dnQtyInput} | |||||
| onChange={(e) => setDnQtyInput(e.target.value)} | |||||
| onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)} | |||||
| InputProps={{ | InputProps={{ | ||||
| inputProps: { | inputProps: { | ||||
| min: 0, // Optional: set a minimum value | min: 0, // Optional: set a minimum value | ||||