diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index 6c72bc6..81bb48e 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -210,8 +210,8 @@ export default function ProductionSchedulePage() { } }; - const fromDateDefault = dayjs().subtract(29, "day").format("YYYY-MM-DD"); - const toDateDefault = dayjs().format("YYYY-MM-DD"); + const fromDateDefault = dayjs().subtract(6, "day").format("YYYY-MM-DD"); + const toDateDefault = dayjs().add(1, "day").format("YYYY-MM-DD"); const fetchItemDailyOut = async (force: boolean = false) => { // Avoid starting a new fetch while an import is in progress, diff --git a/src/components/PoDetail/PoDetail.tsx b/src/components/PoDetail/PoDetail.tsx index a7fdb26..7ef386e 100644 --- a/src/components/PoDetail/PoDetail.tsx +++ b/src/components/PoDetail/PoDetail.tsx @@ -416,14 +416,17 @@ const PoDetail: React.FC = ({ po, warehouse, printerCombo }) => { }, []); useEffect(() => { - if (processedQty === row.qty) { + // `processedQty` comes from putAwayLines (stock unit). + // After the fix, `row.qty` is qtyM18 (M18 unit), so compare using stockUom demand. + const targetStockQty = Number(row.stockUom?.stockQty ?? row.qty ?? 0); + if (targetStockQty > 0 && processedQty >= targetStockQty) { setCurrStatus("completed".toUpperCase()); } else if (processedQty > 0) { setCurrStatus("receiving".toUpperCase()); } else { setCurrStatus("pending".toUpperCase()); } - }, [processedQty, row.qty]); + }, [processedQty, row.qty, row.stockUom?.stockQty]); const handleRowSelect = () => { // setSelectedRowId(row.id); diff --git a/src/components/PoDetail/PoInputGrid.tsx b/src/components/PoDetail/PoInputGrid.tsx index bccde84..f2bae05 100644 --- a/src/components/PoDetail/PoInputGrid.tsx +++ b/src/components/PoDetail/PoInputGrid.tsx @@ -153,7 +153,8 @@ function PoInputGrid({ const [btnIsLoading, setBtnIsLoading] = useState(false); const [currQty, setCurrQty] = useState(() => { const total = entries.reduce( - (acc, curr) => acc + (curr.acceptedQty || 0), + // remaining qty (M18 unit) + (acc, curr) => acc + (curr.purchaseAcceptedQty || 0), 0, ); return total; @@ -231,7 +232,8 @@ function PoInputGrid({ itemName: params.row.itemName, // purchaseOrderId: params.row.purchaseOrderId, purchaseOrderLineId: params.row.purchaseOrderLineId, - acceptedQty: params.row.acceptedQty, + // For PO-origin, backend expects M18 qty and converts it to stock qty. + acceptedQty: params.row.purchaseAcceptedQty ?? params.row.acceptedQty, }; const res = await createStockInLine(postData); console.log(res); @@ -516,7 +518,7 @@ function PoInputGrid({ // // flex: 0.6, // }, { - field: "acceptedQty", + field: "purchaseAcceptedQty", headerName: t("acceptedQty"), // flex: 0.5, width: 125, @@ -524,7 +526,7 @@ function PoInputGrid({ // editable: true, // replace with tooltip + content renderCell: (params) => { - const qty = params.row.purchaseAcceptedQty ?? params.row.acceptedQty ?? 0; + const qty = params.row.purchaseAcceptedQty ?? 0; return integerFormatter.format(qty); } }, @@ -818,7 +820,8 @@ function PoInputGrid({ purchaseOrderLineId: itemDetail.id, itemNo: itemDetail.itemNo, itemName: itemDetail.itemName, - acceptedQty: itemDetail.qty - currQty, // this bug + // User inputs qty in M18 unit; backend will convert to stock unit on create. + purchaseAcceptedQty: itemDetail.qty - currQty, uom: itemDetail.uom, status: "draft", }; @@ -840,8 +843,13 @@ function PoInputGrid({ const error: StockInLineEntryError = {}; console.log(newRow); console.log(currQty); - if (newRow.acceptedQty && newRow.acceptedQty > itemDetail.qty) { - error["acceptedQty"] = t("qty cannot be greater than remaining qty"); + if ( + newRow.purchaseAcceptedQty && + newRow.purchaseAcceptedQty > itemDetail.qty + ) { + error["purchaseAcceptedQty"] = t( + "qty cannot be greater than remaining qty", + ); } return Object.keys(error).length > 0 ? error : undefined; }, @@ -872,7 +880,7 @@ function PoInputGrid({ setEntries(newEntries); //update remaining qty const total = newEntries.reduce( - (acc, curr) => acc + (curr.acceptedQty || 0), + (acc, curr) => acc + (curr.purchaseAcceptedQty || 0), 0, ); setCurrQty(total); diff --git a/src/components/PoDetail/PutAwayForm.tsx b/src/components/PoDetail/PutAwayForm.tsx index 732b9db..0aa74b4 100644 --- a/src/components/PoDetail/PutAwayForm.tsx +++ b/src/components/PoDetail/PutAwayForm.tsx @@ -395,7 +395,7 @@ const PutAwayForm: React.FC = ({ itemDetail, warehouse=[], disabled, sugg @@ -403,7 +403,7 @@ const PutAwayForm: React.FC = ({ itemDetail, warehouse=[], disabled, sugg diff --git a/src/components/PoSearch/PoSearch.tsx b/src/components/PoSearch/PoSearch.tsx index ca95ebe..770b306 100644 --- a/src/components/PoSearch/PoSearch.tsx +++ b/src/components/PoSearch/PoSearch.tsx @@ -13,7 +13,7 @@ import { WarehouseResult } from "@/app/api/warehouse"; import NotificationIcon from "@mui/icons-material/NotificationImportant"; import { useSession } from "next-auth/react"; import { defaultPagingController } from "../SearchResults/SearchResults"; -import { fetchPoListClient, testing } from "@/app/api/po/actions"; +import { testing } from "@/app/api/po/actions"; import dayjs from "dayjs"; import { arrayToDateString, dayjsToDateString } from "@/app/utils/formatUtil"; import arraySupport from "dayjs/plugin/arraySupport"; @@ -289,7 +289,20 @@ const PoSearch: React.FC = ({ }; setAutoSyncStatus(null); - const res = await fetchPoListClient(params); + const cleanedQuery: Record = {}; + Object.entries(params).forEach(([k, v]) => { + if (v === undefined || v === null) return; + if (typeof v === "string" && (v as string).trim() === "") return; + cleanedQuery[k] = String(v); + }); + const baseListResp = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams(cleanedQuery).toString()}`, + { method: "GET" }, + ); + if (!baseListResp.ok) { + throw new Error(`PO list fetch failed: ${baseListResp.status}`); + } + const res = await baseListResp.json(); if (!res) return; if (res.records && res.records.length > 0) { @@ -340,14 +353,6 @@ const PoSearch: React.FC = ({ if (syncOk) { setAutoSyncStatus("成功找到PO"); - // Re-fetch /po/list directly from client to avoid cached server action results. - const cleanedQuery: Record = {}; - Object.entries(params).forEach(([k, v]) => { - if (v === undefined || v === null) return; - if (typeof v === "string" && v.trim() === "") return; - cleanedQuery[k] = String(v); - }); - const listResp = await clientAuthFetch( `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams( cleanedQuery, diff --git a/src/components/ProductionProcess/ProductionProcessList.tsx b/src/components/ProductionProcess/ProductionProcessList.tsx index f713ac6..b00c5e7 100644 --- a/src/components/ProductionProcess/ProductionProcessList.tsx +++ b/src/components/ProductionProcess/ProductionProcessList.tsx @@ -32,7 +32,14 @@ import { SessionWithTokens } from "@/config/authConfig"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; - +export type ProductionProcessListPersistedState = { + date: string; + itemCode: string | null; + jobOrderCode: string | null; + filter: "all" | "drink" | "other"; + page: number; + selectedItemCodes: string[]; +}; import { AllJoborderProductProcessInfoResponse, @@ -50,8 +57,21 @@ interface ProductProcessListProps { onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void; printerCombo: PrinterCombo[]; qcReady: boolean; + listPersistedState: ProductionProcessListPersistedState; + onListPersistedStateChange: React.Dispatch< + React.SetStateAction + >; +} +export function createDefaultProductionProcessListPersistedState(): ProductionProcessListPersistedState { + return { + date: dayjs().format("YYYY-MM-DD"), + itemCode: null, + jobOrderCode: null, + filter: "all", + page: 0, + selectedItemCodes: [], + }; } - type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType"; const PAGE_SIZE = 50; @@ -377,8 +397,14 @@ const ProductProcessList: React.FC = ({ onSelectProcess } /> - {t("Total job orders")}: {totalJobOrders} {selectedItemCodes.length > 0 ? `| ${t("Filtered")}: ${paged.length}` : ""} - + {t("Search date") /* 或在 zh/common.json 加鍵,例如「搜尋日期」 */}:{" "} + {appliedSearch.date && dayjs(appliedSearch.date).isValid() + ? dayjs(appliedSearch.date).format(OUTPUT_DATE_FORMAT) + : "-"} + {" | "} + {t("Total job orders")}: {totalJobOrders} + {selectedItemCodes.length > 0 ? ` | ${t("Filtered")}: ${paged.length}` : ""} + {paged.map((process) => { diff --git a/src/components/ProductionProcess/ProductionProcessPage.tsx b/src/components/ProductionProcess/ProductionProcessPage.tsx index 5ffa8f9..ef34f7c 100644 --- a/src/components/ProductionProcess/ProductionProcessPage.tsx +++ b/src/components/ProductionProcess/ProductionProcessPage.tsx @@ -3,7 +3,9 @@ import React, { useState, useEffect, useCallback } from "react"; import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material"; -import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList"; +import ProductionProcessList, { + createDefaultProductionProcessListPersistedState, +} from "@/components/ProductionProcess/ProductionProcessList"; import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; @@ -43,6 +45,13 @@ const ProductionProcessPage: React.FC = ({ printerCo pickOrderId: number; } | null>(null); const [tabIndex, setTabIndex] = useState(0); + /** 列表搜尋/分頁:保留在切換工單詳情時,返回後仍為同一條件 */ + const [productionListState, setProductionListState] = useState( + createDefaultProductionProcessListPersistedState, + ); + const [finishedQcListState, setFinishedQcListState] = useState( + createDefaultProductionProcessListPersistedState, + ); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; @@ -180,6 +189,8 @@ const ProductionProcessPage: React.FC = ({ printerCo { const id = jobOrderId ?? null; if (id !== null) { @@ -200,6 +211,8 @@ const ProductionProcessPage: React.FC = ({ printerCo { const id = jobOrderId ?? null; if (id !== null) { diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index ccd10c5..848fd14 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -40,6 +40,8 @@ interface BaseCriterion { paramName2?: T; // options?: T[] | string[]; defaultValue?: string; + /** 與 `defaultValue` 配對,用於 dateRange / datetimeRange 重置時的結束值 */ + defaultValueTo?: string; preFilledValue?: string | { from?: string; to?: string }; filterObj?: T; handleSelectionChange?: (selectedOptions: T[]) => void; @@ -159,7 +161,7 @@ function SearchBox({ tempCriteria = { ...tempCriteria, [c.paramName]: c.defaultValue ?? "", - [`${c.paramName}To`]: "", + [`${c.paramName}To`]: c.defaultValueTo ?? "", }; } return tempCriteria; @@ -188,7 +190,7 @@ function SearchBox({ {} as Record, ); return {...defaultInputs, ...preFilledCriteria} - }, [defaultInputs]) + }, [defaultInputs, criteria]) const [inputs, setInputs] = useState(preFilledInputs); const [isReset, setIsReset] = useState(false); diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index 9ee5daf..ce0ff79 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -17,7 +17,10 @@ "Process & Equipment": "製程與設備", "Sequence": "順序", "Process Name": "製程名稱", + "Plan start (from)": "開始日期(從)", + "Plan start (to)": "開始日期(至)", "Process Description": "說明", + "Search date": "搜索日期", "Confirm to Pass this Process?": "確認要通過此工序嗎?", "Equipment Name": "設備", "Confirm to update this Job Order?": "確認要完成此工單嗎?",