| @@ -76,7 +76,6 @@ 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 LoadingComponent from "../General/LoadingComponent"; | |||
| import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| @@ -233,7 +232,7 @@ const PoSearchList: React.FC<{ | |||
| interface PolInputResult { | |||
| lotNo: string, | |||
| dnQty: number, | |||
| dnQty: string, | |||
| } | |||
| 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[]>( | |||
| purchaseOrder.pol || [], | |||
| ); | |||
| const [polInputList, setPolInputList] = useState<PolInputResult[]>([]) | |||
| const [polInputList, setPolInputList] = useState<Record<number, PolInputResult>>({}) | |||
| const PO_DETAIL_SELECTION_KEY = "po-detail-selection"; | |||
| 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]); | |||
| useEffect(() => { | |||
| try { | |||
| @@ -335,7 +338,7 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| }, [selectedIdsParam]); | |||
| const fetchPoDetail = useCallback(async (poId: string, preserveDnNo: boolean = false) => { | |||
| const fetchPoDetail = useCallback(async (poId: string, preserveDnNo: boolean = false, preferredPolId?: number) => { | |||
| try { | |||
| const result = await fetchPoInClient(parseInt(poId)); | |||
| if (result) { | |||
| @@ -348,16 +351,12 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| }); | |||
| setRows(result.pol || []); | |||
| 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();} | |||
| } | |||
| @@ -446,6 +445,8 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| // const [open, setOpen] = useState(false); | |||
| const [processedQty, setProcessedQty] = useState(row.processed); | |||
| 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 totalWeight = useMemo( | |||
| () => calculateWeight(row.qty, row.uom), | |||
| @@ -455,10 +456,6 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| () => 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 | |||
| if (polId) { | |||
| @@ -479,6 +476,11 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| } | |||
| }, [processedQty, row.qty, row.stockUom?.stockQty]); | |||
| useEffect(() => { | |||
| setLotNoInput(polInputList[row.id]?.lotNo ?? ""); | |||
| setDnQtyInput(polInputList[row.id]?.dnQty ?? ""); | |||
| }, [polInputList, row.id]); | |||
| const handleRowSelect = () => { | |||
| // setSelectedRowId(row.id); | |||
| setSelectedRow(row); | |||
| @@ -501,7 +503,7 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| const handleStart = useCallback( | |||
| () => { | |||
| const orderQty = Number(row?.qty) ?? 0; | |||
| const acceptedQty = Number(polInputList[rowIndex].dnQty); | |||
| const acceptedQty = Number(dnQtyInput.trim()); | |||
| if (isNaN(acceptedQty) || acceptedQty <= 0) { | |||
| alert("來貨數量必須大於0!"); | |||
| @@ -518,11 +520,18 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| itemName: row.itemName, | |||
| purchaseOrderLineId: row.id, | |||
| acceptedQty: acceptedQty, | |||
| productLotNo: polInputList[rowIndex].lotNo || '', | |||
| productLotNo: lotNoInput || "", | |||
| }; | |||
| const res = await createStockInLine(postData); | |||
| 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); | |||
| }, 200); | |||
| @@ -539,57 +548,24 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| 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>(); | |||
| // 本批收貨數量(訂單單位): 使用者在該行輸入的 dnQty | |||
| const batchPurchaseQty = polInputList[rowIndex]?.dnQty ?? 0; | |||
| const batchPurchaseQty = Number(dnQtyInput.trim()) || 0; | |||
| // 已來貨總數(庫存單位): 同一 POL 底下所有 stock_in_line.acceptedQty 的合計 | |||
| const totalStockReceived = row.stockInLine | |||
| @@ -686,8 +662,9 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| label="輸入貨品批號" | |||
| type="text" // Use type="text" to allow validation in the change handler | |||
| 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);}} | |||
| /> | |||
| </TableCell> | |||
| @@ -697,8 +674,9 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| label="此批送貨數量" | |||
| type="text" // Use type="text" to allow validation in the change handler | |||
| variant="outlined" | |||
| defaultValue={polInputList[rowIndex]?.dnQty ?? ''} | |||
| onChange={handleChange} | |||
| value={dnQtyInput} | |||
| onChange={(e) => setDnQtyInput(e.target.value)} | |||
| onBlur={() => syncRowInputToParent(lotNoInput, dnQtyInput)} | |||
| InputProps={{ | |||
| inputProps: { | |||
| min: 0, // Optional: set a minimum value | |||