diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index 8bfee38..bde0549 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -749,8 +749,12 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { // onClick={(e) => e.stopPropagation()} /> - {row.itemNo} - {row.itemName} + + {row.itemNo} + + + {row.itemName} + {integerFormatter.format(row.qty)} {integerFormatter.format(row.processed)} {row.uom?.udfudesc} @@ -1135,8 +1139,8 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { - {t("itemNo")} - {t("itemName")} + {t("itemNo")} + {t("itemName")} {t("qty")} {t("processedQty")} {t("uom")} @@ -1168,32 +1172,19 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { {selectedRow ? `已選擇貨品: ${selectedRow?.itemNo ? selectedRow.itemNo : 'N/A'} - ${selectedRow?.itemName ? selectedRow?.itemName : 'N/A'}` : "未選擇貨品"} - + {selectedRow && ( - - - - - - - - - - - -
-
+ )}
diff --git a/src/components/PoDetail/PoDetailWrapper.tsx b/src/components/PoDetail/PoDetailWrapper.tsx index e5c3110..34d659f 100644 --- a/src/components/PoDetail/PoDetailWrapper.tsx +++ b/src/components/PoDetail/PoDetailWrapper.tsx @@ -32,7 +32,7 @@ const PoDetailWrapper: React.FC & SubComponents = async ({ id }) => { fetchPrinterCombo(), ]); // const poWithStockInLine = await fetchPoWithStockInLines(id) -console.log("%c pol:", "color:green", poWithStockInLine); +//console.log("%c pol:", "color:green", poWithStockInLine); return ; }; diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index f2bae05..c15f411 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -1,7 +1,6 @@ "use client"; import { FooterPropsOverrides, - GridActionsCellItem, GridCellParams, GridRowId, GridRowIdGetter, @@ -19,11 +18,12 @@ import { useCallback, useEffect, useMemo, + useRef, useState, } from "react"; import StyledDataGrid from "../StyledDataGrid"; import { GridColDef } from "@mui/x-data-grid"; -import { Box, Button, Grid, Typography } from "@mui/material"; +import { Box, Button, Grid, Typography, useMediaQuery, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { Add } from "@mui/icons-material"; import SaveIcon from "@mui/icons-material/Save"; @@ -35,7 +35,7 @@ import ShoppingCartIcon from "@mui/icons-material/ShoppingCart"; import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import { PurchaseOrderLine } from "@/app/api/po"; import { StockInLine } from "@/app/api/stockIn"; -import { createStockInLine, QcResult } from "@/app/api/stockIn/actions"; +import { createStockInLine, deleteStockInLine, QcResult } from "@/app/api/stockIn/actions"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { returnWeightUnit, @@ -67,6 +67,63 @@ import { EscalationResult } from "@/app/api/escalation"; import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions"; import { SessionWithTokens } from "@/config/authConfig"; import { EscalationCombo } from "@/app/api/user"; +import { deleteDialog } from "../Swal/CustomAlerts"; +import StockInLineRowActions from "./StockInLineRowActions"; + +/** Sum of fixed column widths (desktop) so the grid can scroll horizontally without squeezing cells. */ +const STOCK_IN_GRID_MIN_WIDTH_DESKTOP = 1062; + +const ACTIONS_COLUMN_WIDTH = 168; + +const ACTION_BUTTON_HEIGHT = 38; +const ACTION_BUTTON_GAP = 6; +/** Extra space for cell padding + outlined button borders */ +const ACTION_ROW_EXTRA_PADDING = 36; + +function getActionRowHeight(buttonCount: number): number { + return ( + buttonCount * ACTION_BUTTON_HEIGHT + + Math.max(0, buttonCount - 1) * ACTION_BUTTON_GAP + + ACTION_ROW_EXTRA_PADDING + ); +} + +function countActionButtonsForRow(row: StockInLineRow): number { + let count = 1; + const status = (row.status ?? "").toLowerCase(); + if (status === "rejected" || status === "partially_completed") { + count += 1; + } + if (status === "received") { + count += 1; + } + if (canDeleteStockInLine(row)) { + count += 1; + } + return count; +} + +/** Tighter horizontal padding for narrow data columns (headers unchanged). */ +const COMPACT_STOCK_IN_CELL_FIELDS = [ + "dnNo", + "productLotNo", + "purchaseAcceptedQty", + "uom", + "stockUom", + "status", +] as const; + +function canDeleteStockInLine(sil: StockInLineRow): boolean { + if (sil._isNew || sil.status === "draft") { + return true; + } + const hasPutAway = (sil.putAwayLines ?? []).some( + (p) => Number(p.stockQty ?? p.qty ?? 0) > 0, + ); + if (hasPutAway) return false; + const status = (sil.status ?? "").toLowerCase(); + return status !== "completed" && status !== "partially_completed"; +} interface ResultWithId { id: number; @@ -80,7 +137,7 @@ interface Props { itemDetail: PurchaseOrderLine; stockInLine: StockInLine[]; warehouse: WarehouseResult[]; - fetchPoDetail: (poId: string) => void; + fetchPoDetail: (poId: string, preserveDnNo?: boolean, preferredPolId?: number) => void; handleMailTemplateForStockInLine: (stockInLineId: number) => void; printerCombo: PrinterCombo[]; } @@ -127,6 +184,11 @@ function PoInputGrid({ }: Props) { const { t } = useTranslation("purchaseOrder"); + const theme = useTheme(); + /** Narrow phones: hide low-priority columns. */ + const isCompact = useMediaQuery(theme.breakpoints.down("md"), { noSsr: true }); + /** Tablet / sub-desktop (< xl): flex columns to fill available width. Desktop (≥ xl) keeps fixed widths. */ + const isTablet = useMediaQuery(theme.breakpoints.down("xl"), { noSsr: true }); const apiRef = useGridApiRef(); const [rowModesModel, setRowModesModel] = useState({}); const getRowId = useCallback>( @@ -151,6 +213,8 @@ function PoInputGrid({ const [putAwayOpen, setPutAwayOpen] = useState(false); const [rejectOpen, setRejectOpen] = useState(false); const [btnIsLoading, setBtnIsLoading] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const deleteInFlightRef = useRef(false); const [currQty, setCurrQty] = useState(() => { const total = entries.reduce( // remaining qty (M18 unit) @@ -180,6 +244,42 @@ function PoInputGrid({ }, [getRowId], ); + + const handleSoftDelete = useCallback( + (row: StockInLineRow) => { + if (deleteInFlightRef.current || isDeleting) return; + + const rowId = row.id as number; + const isDraft = row._isNew || row.status === "draft"; + + const doDelete = async () => { + if (deleteInFlightRef.current) return; + deleteInFlightRef.current = true; + setIsDeleting(true); + try { + if (isDraft) { + handleDelete(rowId)(); + return; + } + await deleteStockInLine(rowId); + await fetchPoDetail( + String(itemDetail.purchaseOrderId), + true, + itemDetail.id, + ); + } catch (error) { + console.error("Failed to delete stock in line:", error); + alert(t("Cannot delete put away record")); + } finally { + setIsDeleting(false); + deleteInFlightRef.current = false; + } + }; + + void deleteDialog(doDelete, t); + }, + [fetchPoDetail, handleDelete, isDeleting, itemDetail.id, itemDetail.purchaseOrderId, t], + ); const closeQcModal = useCallback(() => { setQcOpen(false); @@ -445,20 +545,37 @@ function PoInputGrid({ const getButtonSx = (sil : StockInLine) => { const status = sil?.status?.toLowerCase(); - let btnSx = {label:"", color:""}; + let btnSx = { label: "", color: "" }; switch (status) { - case "received": btnSx = {label: t("view putaway"), color:"secondary.main"}; break; - case "escalated": if (sessionToken?.id == sil?.handlerId) { - btnSx = {label: t("escalation processing"), color:"warning.main"}; - break;} - case "rejected": + case "received": + btnSx = { label: t("view putaway"), color: "secondary.main" }; + break; + case "escalated": + if (sessionToken?.id == sil?.handlerId) { + btnSx = { label: t("escalation processing"), color: "warning.main" }; + break; + } + btnSx = { label: t("qc processing"), color: "success.main" }; + break; + case "rejected": case "partially_completed": - case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break; - default: btnSx = {label: t("qc processing"), color:"success.main"}; + case "completed": + btnSx = { label: t("view stockin"), color: "info.main" }; + break; + default: + btnSx = { label: t("qc processing"), color: "success.main" }; } - return btnSx + return btnSx; }; + const columnVisibilityModel = useMemo( + () => ({ + uom: !isCompact, + stockUom: !isCompact, + }), + [isCompact], + ); + // const handleQrCode = useCallback( // (id: GridRowId, params: any) => () => { // setRowModesModel((prev) => ({ @@ -478,333 +595,172 @@ function PoInputGrid({ // [printQrcode], // ); - const columns = useMemo( - () => [ - // { - // field: "itemNo", - // headerName: t("itemNo"), - // width: 100, - // // flex: 0.4, - // }, + const columns = useMemo(() => { + const baseColumns: GridColDef[] = [ { field: "dnNo", headerName: t("dnNo"), - width: 125, - // renderCell: () => { - // return <>DN0000001 - // } - // flex: 0.4, + width: 92, }, { field: "receiptDate", headerName: t("receiptDate"), width: 125, - renderCell: (params) => { - // console.log(params.row) - // return <>07/08/2025 - return arrayToDateString(params.value) - } - // flex: 0.4, + renderCell: (params) => arrayToDateString(params.value), }, { field: "productLotNo", headerName: t("productLotNo"), - width: 125, + width: 100, }, - // { - // field: "itemName", - // headerName: t("itemName"), - // width: 100, - // // flex: 0.6, - // }, { field: "purchaseAcceptedQty", headerName: t("acceptedQty"), - // flex: 0.5, - width: 125, + width: 84, + align: "right", + headerAlign: "right", type: "number", - // editable: true, - // replace with tooltip + content renderCell: (params) => { const qty = params.row.purchaseAcceptedQty ?? 0; return integerFormatter.format(qty); - } + }, }, { field: "uom", headerName: t("uom"), - width: 150, - // flex: 0.5, - renderCell: (params) => { - return itemDetail.uom?.udfudesc; + width: 156, + renderCell: () => { + const text = itemDetail.uom?.udfudesc ?? "-"; + return ( + + {text} + + ); }, }, { field: "stockQty", headerName: t("Stock In Qty"), - // flex: 0.5, width: 125, type: "number", - // editable: true, - // replace with tooltip + content renderCell: (params) => { - // acceptedQty 現在就是庫存單位數量 const stockQty = params.row.acceptedQty ?? 0; return decimalFormatter.format(stockQty); - } + }, }, { field: "stockUom", headerName: t("Stock UoM"), - width: 150, - // flex: 0.5, - renderCell: (params) => { - return itemDetail.stockUom.stockUomDesc; + width: 124, + renderCell: () => { + const text = itemDetail.stockUom.stockUomDesc ?? "-"; + return ( + + {text} + + ); }, }, - // { - // field: "weight", - // headerName: t("weight"), - // width: 120, - // // flex: 0.5, - // renderCell: (params) => { - // const weight = calculateWeight( - // params.row.acceptedQty, - // params.row.uom, - // ); - // const weightUnit = returnWeightUnit(params.row.uom); - // return `${decimalFormatter.format(weight)} ${weightUnit}`; - // }, - // }, { field: "status", headerName: t("Status"), - width: 140, - // flex: 0.5, + width: 88, renderCell: (params) => { - const handlerId = params.row.handlerId - const status = params.row.status - return ( {t(`${params.row.status}`)} - ); + + ); }, }, { field: "actions", - type: "actions", - // headerName: `${t("start")} | ${t("qc")} | ${t("escalation")} | ${t( - // "stock in", - // )} | ${t("putaway")} | ${t("delete")}`, headerName: "操作", - // headerName: "start | qc | escalation | stock in | putaway | delete", - width: 350, //200 - // flex: 2, + width: ACTIONS_COLUMN_WIDTH, + sortable: false, + filterable: false, + disableColumnMenu: true, cellClassName: "actions", - getActions: (params) => { - const data = params.row; - // console.log(params.row.status); + renderCell: (params) => { + const data = params.row as StockInLineRow; const btnSx = getButtonSx(data); - // console.log(stockInLineStatusMap[status]); - // console.log(session?.user?.abilities?.includes("APPROVAL")); - return [ - - {btnSx.label}} - label="start" - sx={{ - // color: "primary.main", - // marginRight: 1, + const status = (data.status ?? "").toLowerCase(); + const canEmail = + status === "rejected" || status === "partially_completed"; + const canPrint = status === "received"; + const canDelete = canDeleteStockInLine(data); + + return ( + { + void handleNewQC(params.row.id, params)(); }} - onClick={handleNewQC(params.row.id, params)} - color="inherit" - key={`edit`} - />, - handleMailTemplateForStockInLine(params.row.id as number)} - > - {t("email supplier")} - ) : ( - - ) + canEmail={canEmail} + canPrint={canPrint} + canDelete={canDelete} + onEmail={() => + handleMailTemplateForStockInLine(params.row.id as number) } - label="start" - sx={{ - // color: "primary.main", - // marginRight: 1, - }} - // onClick={handleNewQC(params.row.id, params)} - color="inherit" - key="edit" - />, - // {t("putawayBtn")}} - // label="start" - // sx={{ - // color: "primary.main", - // // marginRight: 1, - // }} - // // disabled={!(stockInLineStatusMap[status] === 0)} - // // set _isNew to false after posting - // // or check status - // onClick={handleStart(params.row.id, params)} - // color="inherit" - // key="edit" - // />, - - // {t("qc processing")}} - // label="start" - // sx={{ - // color: "primary.main", - // // marginRight: 1, - // }} - // disabled={!(stockInLineStatusMap[status] === 0)} - // // set _isNew to false after posting - // // or check status - // onClick={handleStart(params.row.id, params)} - // color="inherit" - // key="edit" - // />, - // } - // label="qc" - // sx={{ - // color: "primary.main", - // // marginRight: 1, - // }} - // disabled={ - // // stockInLineStatusMap[status] === 9 || - // stockInLineStatusMap[status] < 1 - // } - // // set _isNew to false after posting - // // or check status - // onClick={handleQC(params.row.id, params)} - // color="inherit" - // key="edit" - // />, - // } - // label="escalation" - // sx={{ - // color: "primary.main", - // // marginRight: 1, - // }} - // disabled={ - // stockInLineStatusMap[status] === 9 || - // stockInLineStatusMap[status] <= 0 || - // stockInLineStatusMap[status] >= 5 - // } - // // set _isNew to false after posting - // // or check status - // onClick={handleEscalation(params.row.id, params)} - // color="inherit" - // key="edit" - // />, - // } - // label="stockin" - // sx={{ - // color: "primary.main", - // // marginRight: 1, - // }} - // disabled={ - // stockInLineStatusMap[status] === 9 || - // stockInLineStatusMap[status] <= 2 || - // stockInLineStatusMap[status] >= 7 || - // (stockInLineStatusMap[status] >= 3 && - // stockInLineStatusMap[status] <= 5 && - // !session?.user?.abilities?.includes("APPROVAL")) - // } - // // set _isNew to false after posting - // // or check status - // onClick={handleStockIn(params.row.id, params)} - // color="inherit" - // key="edit" - // />, - // } - // label="putaway" - // sx={{ - // color: "primary.main", - // // marginRight: 1, - // }} - // disabled={ - // stockInLineStatusMap[status] === 9 || - // stockInLineStatusMap[status] < 7 - // } - // // set _isNew to false after posting - // // or check status - // onClick={handlePutAway(params.row.id, params)} - // color="inherit" - // key="edit" - // />, - // // } - // // label="putaway" - // // sx={{ - // // color: "primary.main", - // // // marginRight: 1, - // // }} - // // disabled={stockInLineStatusMap[status] === 9 || stockInLineStatusMap[status] !== 8} - // // // set _isNew to false after posting - // // // or check status - // // onClick={handleQrCode(params.row.id, params)} - // // color="inherit" - // // key="edit" - // // />, - // = 1 ? ( - // - // ) : ( - // - // ) - // } - // label="Delete" - // sx={{ - // color: "error.main", - // }} - // disabled={ - // stockInLineStatusMap[status] >= 7 && - // stockInLineStatusMap[status] <= 9 - // } - // onClick={ - // stockInLineStatusMap[status] === 0 - // ? handleDelete(params.row.id) - // : handleReject(params.row.id, params) - // } - // color="inherit" - // key="edit" - // />, - ]; + onPrint={() => printQrcode(params.row)} + onDelete={() => handleSoftDelete(data)} + btnIsLoading={btnIsLoading} + isDeleting={isDeleting} + /> + ); }, }, - ], - [t, handleStart, handleEscalation, handleStockIn, handlePutAway, handleDelete, handleReject, itemDetail], - ); + ]; + + if (!isTablet) { + return baseColumns; + } + + return baseColumns.map((col) => { + if (col.field === "actions") { + return { ...col, flex: 0, width: ACTIONS_COLUMN_WIDTH }; + } + const minWidth = col.width ?? 80; + return { ...col, flex: 1, minWidth, width: undefined }; + }); + }, [ + t, + isTablet, + itemDetail, + handleNewQC, + handleMailTemplateForStockInLine, + printQrcode, + handleSoftDelete, + btnIsLoading, + isDeleting, + sessionToken?.id, + ]); const unsortableColumns = useMemo(() => columns.map(column => ({ ...column, sortable: false })) @@ -915,14 +871,36 @@ function PoInputGrid({ */} ); + + const getRowHeight = useCallback( + (params: { model: StockInLineRow }) => { + const count = countActionButtonsForRow(params.model); + return getActionRowHeight(count); + }, + [], + ); return ( <> + [ + [ + `& .MuiDataGrid-cell[data-field="${field}"]`, + { px: 1 }, + ], + [ + `& .MuiDataGrid-columnHeader[data-field="${field}"]`, + { px: 1 }, + ], + ]), + ), }} disableColumnMenu editMode="row" @@ -963,6 +963,7 @@ function PoInputGrid({ footer: { child: footer }, }} /> + {/* {modalInfo !== undefined && ( */} <> void; + canEmail: boolean; + canPrint: boolean; + canDelete: boolean; + onEmail: () => void; + onPrint: () => void; + onDelete: () => void; + btnIsLoading: boolean; + isDeleting: boolean; +}; + +export default function StockInLineRowActions({ + btnSx, + onPrimaryClick, + canEmail, + canPrint, + canDelete, + onEmail, + onPrint, + onDelete, + btnIsLoading, + isDeleting, +}: Props) { + const { t } = useTranslation("purchaseOrder"); + + const buttonSx = { + whiteSpace: "nowrap" as const, + fontSize: 14, + px: 1.5, + py: 0.75, + minHeight: 34, + width: "100%", + justifyContent: "center", + }; + + return ( + e.stopPropagation()} + > + + {canEmail && ( + + )} + {canPrint && ( + + )} + {canDelete && ( + + )} + + ); +} diff --git a/src/i18n/en/purchaseOrder.json b/src/i18n/en/purchaseOrder.json index c1d8eb0..1137ef1 100644 --- a/src/i18n/en/purchaseOrder.json +++ b/src/i18n/en/purchaseOrder.json @@ -62,6 +62,12 @@ "stock in": "Stock In", "putaway": "Put Away", "delete": "Delete", + "actionView": "View", + "actionProcess": "Process", + "moreActions": "More actions", + "Do you want to delete?": "Do you want to delete this delivery record?", + "Delete": "Delete", + "Cannot delete put away record": "Cannot delete a record that has already been put away", "Accept quantity must be greater than 0": "Accept quantity must be greater than 0", "QC items without result": "QC items without result", "Failed items must have failed quantity": "Failed items must have failed quantity", diff --git a/src/i18n/zh/purchaseOrder.json b/src/i18n/zh/purchaseOrder.json index 5777a6f..c023d8b 100644 --- a/src/i18n/zh/purchaseOrder.json +++ b/src/i18n/zh/purchaseOrder.json @@ -62,6 +62,12 @@ "stock in": "入庫", "putaway": "上架", "delete": "刪除", + "actionView": "查看", + "actionProcess": "處理", + "moreActions": "更多操作", + "Do you want to delete?": "確定要刪除此來貨記錄嗎?", + "Delete": "刪除", + "Cannot delete put away record": "已上架的來貨記錄不可刪除", "Accept quantity must be greater than 0": "揀收數量不能少於1", "QC items without result": "有未完成品檢項目", "Failed items must have failed quantity": "請輸入不合格數量",