"use client"; import { PurchaseOrderLine } from "@/app/api/po"; import { Box, Button, Radio, TableCell, TableRow, TextField, alpha, } from "@mui/material"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; import { submitDialogWithWarning } from "../Swal/CustomAlerts"; import { createStockInLine } from "@/app/api/stockIn/actions"; import { previewPoBatchStockQty } from "./stockQtyRound"; const PURCHASE_STOCK_IN_ALERT_STATUSES = new Set(["pending", "receiving"]); function totalPutAwayStockQtyForPol(row: PurchaseOrderLine): number { return row.stockInLine .filter((sil) => sil.purchaseOrderLineId === row.id) .reduce((acc, sil) => { const lineSum = sil.putAwayLines?.reduce( (s, p) => s + Number(p.stockQty ?? p.qty ?? 0), 0, ) ?? 0; return acc + lineSum; }, 0); } function polOrderStockQty(row: PurchaseOrderLine): number { return Number(row.stockUom?.stockQty ?? row.qty ?? 0); } function purchaseOrderLineHasIncompleteStockIn(row: PurchaseOrderLine): boolean { const orderStock = polOrderStockQty(row); const putAway = totalPutAwayStockQtyForPol(row); if (orderStock > 0 && putAway >= orderStock) { return false; } return row.stockInLine .filter((sil) => sil.purchaseOrderLineId === row.id) .some((sil) => PURCHASE_STOCK_IN_ALERT_STATUSES.has((sil.status ?? "").toLowerCase().trim()), ); } export type PoDetailRowDnValues = { dnNo?: string; receiptDate?: string; }; type Props = { row: PurchaseOrderLine; selected: boolean; canSeeStockInReminders: boolean; showDnQty: boolean; savedLotNo: string; savedDnQty: string; onSelect: (row: PurchaseOrderLine) => void; onInputBlur: (rowId: number, lotNo: string, dnQty: string) => void; getDnValues: () => PoDetailRowDnValues; formatReceiptDate: (receiptDate?: string) => string | undefined; onSubmitted: (row: PurchaseOrderLine) => void; }; /** FP-MTMS Version Checklist | Functions Ref. No. 56 | v1.0.4 | 2026-09-10 */ export const PoDetailRow = memo(function PoDetailRow({ row, selected, canSeeStockInReminders, showDnQty, savedLotNo, savedDnQty, onSelect, onInputBlur, getDnValues, formatReceiptDate, onSubmitted, }: Props) { const { t } = useTranslation("purchaseOrder"); const [lotNoInput, setLotNoInput] = useState(savedLotNo); const [dnQtyInput, setDnQtyInput] = useState(savedDnQty); const submitInFlightRef = useRef(false); const [isStarting, setIsStarting] = useState(false); useEffect(() => { setLotNoInput(savedLotNo); setDnQtyInput(savedDnQty.replace(/[^\d]/g, "")); }, [savedLotNo, savedDnQty]); const handleStart = useCallback( () => { if (submitInFlightRef.current || isStarting) return; const orderQty = Number(row?.qty) ?? 0; const acceptedQty = Number(dnQtyInput.trim()); if (!Number.isInteger(acceptedQty) || acceptedQty <= 0) { alert("來貨數量必須為大於0的整數!"); return; } const previewStockQty = previewPoBatchStockQty( orderQty, Number(row.stockUom?.stockQty ?? 0), acceptedQty, ); const doSubmit = () => { if (submitInFlightRef.current) return; submitInFlightRef.current = true; setIsStarting(true); void (async () => { try { const dn = getDnValues(); const postData = { dnNo: dn.dnNo, receiptDate: formatReceiptDate(dn.receiptDate), itemId: row.itemId, itemNo: row.itemNo, itemName: row.itemName, purchaseOrderLineId: row.id, acceptedQty: acceptedQty, productLotNo: lotNoInput || "", }; const res = await createStockInLine(postData); if (res) { setLotNoInput(""); setDnQtyInput(""); onSubmitted(row); } console.log(res); } finally { setIsStarting(false); submitInFlightRef.current = false; } })(); }; const sils = row.stockInLine ?? []; const alreadyM18 = sils.reduce( (acc, sil) => acc + Number(sil.purchaseAcceptedQty ?? 0), 0, ); const alreadyStock = sils.reduce( (acc, sil) => acc + Number(sil.acceptedQty ?? 0), 0, ); const stockDemand = Number(row.stockUom?.stockQty ?? 0); const thisBatchStock = previewStockQty; const exceedByOrderUnit = orderQty > 0 && alreadyM18 + acceptedQty > orderQty * 1.1; const exceedByStockUnit = stockDemand > 0 && alreadyStock + thisBatchStock > stockDemand * 1.1; if (exceedByOrderUnit || exceedByStockUnit) { submitDialogWithWarning(doSubmit, t, { title: t("Confirm submit"), html: t("qtyExceedsOrderConfirm"), confirmButtonText: t("Submit"), }); } else { doSubmit(); } }, [ dnQtyInput, formatReceiptDate, getDnValues, isStarting, lotNoInput, onSubmitted, row, t, ], ); const totalStockReceived = row.stockInLine .filter((sil) => sil.purchaseOrderLineId === row.id) .reduce((acc, cur) => acc + (cur.acceptedQty ?? 0), 0); const receivedTotalText = decimalFormatter.format(totalStockReceived); const highlightColor = Number(receivedTotalText.replace(/,/g, "")) <= 0 ? "red" : "inherit"; const needsStockInAttention = canSeeStockInReminders && purchaseOrderLineHasIncompleteStockIn(row); return ( *": { borderBottom: "unset" }, color: "black", ...(needsStockInAttention ? (theme) => ({ boxShadow: `inset 4px 0 0 ${theme.palette.error.main}`, backgroundColor: alpha(theme.palette.error.main, 0.07), }) : {}), }} onClick={() => onSelect(row)} > {needsStockInAttention && ( `0 0 0 1px ${alpha(theme.palette.error.main, 0.45)}`, zIndex: 1, }} /> )} {row.itemNo} {row.itemName} {integerFormatter.format(row.qty)} {integerFormatter.format(row.processed)} {row.uom?.udfudesc} {decimalFormatter.format(totalStockReceived)} {row.stockUom.stockUomDesc} {t(`${row.status.toLowerCase()}`)} setLotNoInput(e.target.value)} onBlur={() => onInputBlur(row.id, lotNoInput, dnQtyInput)} onClick={(e) => e.stopPropagation()} /> {showDnQty ? ( setDnQtyInput(e.target.value.replace(/[^\d]/g, ""))} onBlur={() => onInputBlur(row.id, lotNoInput, dnQtyInput)} onClick={(e) => e.stopPropagation()} InputProps={{ inputProps: { min: 1, step: 1, inputMode: "numeric", pattern: "[0-9]*", }, }} /> ) : null} ); });