|
- "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 (
- <TableRow
- hover
- title={
- needsStockInAttention
- ? "採購入庫未完成:此採購明細尚有入庫單為「待處理」或「收貨中」,請於下方完成入庫。"
- : undefined
- }
- sx={{
- "& > *": { 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)}
- >
- <TableCell align="center" sx={{ width: "60px", position: "relative" }}>
- {needsStockInAttention && (
- <Box
- component="span"
- aria-hidden
- sx={{
- position: "absolute",
- top: 6,
- left: 8,
- width: 10,
- height: 10,
- borderRadius: "50%",
- bgcolor: "error.main",
- border: "2px solid",
- borderColor: "background.paper",
- boxShadow: (theme) => `0 0 0 1px ${alpha(theme.palette.error.main, 0.45)}`,
- zIndex: 1,
- }}
- />
- )}
- <Radio checked={selected} />
- </TableCell>
- <TableCell
- align="left"
- sx={{ width: 88, maxWidth: 88, px: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
- title={row.itemNo}
- >
- {row.itemNo}
- </TableCell>
- <TableCell
- align="left"
- sx={{ width: 100, maxWidth: 100, px: 1, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
- title={row.itemName}
- >
- {row.itemName}
- </TableCell>
- <TableCell align="right">{integerFormatter.format(row.qty)}</TableCell>
- <TableCell align="right">{integerFormatter.format(row.processed)}</TableCell>
- <TableCell align="left">{row.uom?.udfudesc}</TableCell>
- <TableCell sx={{ color: highlightColor }} align="right">
- {decimalFormatter.format(totalStockReceived)}
- </TableCell>
- <TableCell sx={{ color: highlightColor }} align="left">
- {row.stockUom.stockUomDesc}
- </TableCell>
- <TableCell sx={{ color: highlightColor }} align="left">
- {t(`${row.status.toLowerCase()}`)}
- </TableCell>
- <TableCell align="center">
- <TextField
- id={`lotNo-${row.id}`}
- label="輸入貨品批號"
- type="text"
- variant="outlined"
- value={lotNoInput}
- onChange={(e) => setLotNoInput(e.target.value)}
- onBlur={() => onInputBlur(row.id, lotNoInput, dnQtyInput)}
- onClick={(e) => e.stopPropagation()}
- />
- </TableCell>
- {showDnQty ? (
- <TableCell align="center">
- <TextField
- id={`dnQty-${row.id}`}
- label="此批來貨數量"
- type="text"
- variant="outlined"
- value={dnQtyInput}
- onChange={(e) => 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]*",
- },
- }}
- />
- </TableCell>
- ) : null}
- <TableCell align="center">
- <Button
- variant="contained"
- disabled={isStarting}
- onMouseDown={(e) => {
- e.preventDefault();
- e.stopPropagation();
- }}
- onClick={(e) => {
- e.stopPropagation();
- handleStart();
- }}
- >
- {t("submit")}
- </Button>
- </TableCell>
- </TableRow>
- );
- });
|