|
- "use client";
-
- import { PurchaseOrderLine } from "@/app/api/po";
- import {
- Box,
- Button,
- InputAdornment,
- Radio,
- Stack,
- TableCell,
- TableRow,
- TextField,
- Typography,
- 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";
- import {
- formatQtyWithPurchaseUom,
- formatQtyWithStockUom,
- poLineNeedsStockQtyConversion,
- purchaseUomShortDesc,
- } from "./poPurchaseUom";
-
- 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,
- row.stockUom,
- );
-
- 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);
- const uomShort = purchaseUomShortDesc(row.uom);
- const orderQtyWithUnit = formatQtyWithPurchaseUom(row.qty, row.uom);
- const enteredBatchQty = Number(dnQtyInput.trim());
- const hasEnteredBatchQty = Number.isInteger(enteredBatchQty) && enteredBatchQty > 0;
- const showStockConversion =
- hasEnteredBatchQty &&
- poLineNeedsStockQtyConversion(
- Number(row.qty ?? 0),
- Number(row.stockUom?.stockQty ?? 0),
- row.uom,
- row.stockUom,
- );
- const convertedStockQty = previewPoBatchStockQty(
- Number(row.qty ?? 0),
- Number(row.stockUom?.stockQty ?? 0),
- enteredBatchQty,
- row.stockUom,
- );
- const convertedStockQtyWithUnit = formatQtyWithStockUom(
- convertedStockQty,
- row.stockUom,
- );
-
- 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"
- sx={{
- fontWeight: 800,
- fontVariantNumeric: "tabular-nums",
- whiteSpace: "nowrap",
- fontSize: "1.05rem",
- color: "text.primary",
- }}
- title={row.uom?.udfudesc || uomShort || undefined}
- >
- {orderQtyWithUnit}
- </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" sx={{ py: 1, overflow: "visible" }}>
- <Stack
- direction="row"
- spacing={1.25}
- alignItems="center"
- justifyContent="center"
- flexWrap="nowrap"
- sx={{ width: "max-content" }}
- onClick={(e) => e.stopPropagation()}
- >
- <TextField
- id={`dnQty-${row.id}`}
- label={t("dnQty")}
- 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()}
- sx={{
- minWidth: 168,
- width: 168,
- flexShrink: 0,
- "& .MuiInputLabel-root": {
- maxWidth: "calc(100% - 44px)",
- },
- "& .MuiInputBase-input": {
- pr: 0.5,
- },
- }}
- InputLabelProps={{ shrink: true }}
- InputProps={{
- notched: true,
- endAdornment: uomShort ? (
- <InputAdornment position="end" sx={{ ml: 0.5 }}>
- <Typography
- component="span"
- sx={{
- fontWeight: 800,
- fontSize: "1.05rem",
- color: "primary.main",
- lineHeight: 1,
- }}
- >
- {uomShort}
- </Typography>
- </InputAdornment>
- ) : undefined,
- inputProps: {
- min: 1,
- step: 1,
- inputMode: "numeric",
- pattern: "[0-9]*",
- "aria-label": `${t("dnQty")} ${uomShort}`.trim(),
- },
- }}
- />
- {showStockConversion && Number.isFinite(convertedStockQty) && convertedStockQty > 0 ? (
- <Box
- sx={{
- flexShrink: 0,
- width: "max-content",
- boxSizing: "border-box",
- px: 1.25,
- py: 0.75,
- borderRadius: 1,
- bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1),
- border: 1,
- borderColor: "primary.main",
- textAlign: "center",
- }}
- title={row.stockUom?.stockUomDesc || convertedStockQtyWithUnit}
- >
- <Typography
- component="div"
- sx={{
- fontSize: "0.7rem",
- lineHeight: 1.2,
- color: "text.secondary",
- fontWeight: 600,
- }}
- >
- {t("stockQtyRef")}
- </Typography>
- <Typography
- component="div"
- sx={{
- fontWeight: 800,
- fontSize: "1rem",
- lineHeight: 1.3,
- color: "primary.main",
- fontVariantNumeric: "tabular-nums",
- whiteSpace: "nowrap",
- px: 0.25,
- }}
- >
- {convertedStockQtyWithUnit}
- </Typography>
- </Box>
- ) : null}
- </Stack>
- </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>
- );
- });
|