| @@ -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) => { | const fetchItemDailyOut = async (force: boolean = false) => { | ||||
| // Avoid starting a new fetch while an import is in progress, | // Avoid starting a new fetch while an import is in progress, | ||||
| @@ -416,14 +416,17 @@ const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||||
| }, []); | }, []); | ||||
| useEffect(() => { | 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()); | setCurrStatus("completed".toUpperCase()); | ||||
| } else if (processedQty > 0) { | } else if (processedQty > 0) { | ||||
| setCurrStatus("receiving".toUpperCase()); | setCurrStatus("receiving".toUpperCase()); | ||||
| } else { | } else { | ||||
| setCurrStatus("pending".toUpperCase()); | setCurrStatus("pending".toUpperCase()); | ||||
| } | } | ||||
| }, [processedQty, row.qty]); | |||||
| }, [processedQty, row.qty, row.stockUom?.stockQty]); | |||||
| const handleRowSelect = () => { | const handleRowSelect = () => { | ||||
| // setSelectedRowId(row.id); | // setSelectedRowId(row.id); | ||||
| @@ -153,7 +153,8 @@ function PoInputGrid({ | |||||
| const [btnIsLoading, setBtnIsLoading] = useState(false); | const [btnIsLoading, setBtnIsLoading] = useState(false); | ||||
| const [currQty, setCurrQty] = useState(() => { | const [currQty, setCurrQty] = useState(() => { | ||||
| const total = entries.reduce( | const total = entries.reduce( | ||||
| (acc, curr) => acc + (curr.acceptedQty || 0), | |||||
| // remaining qty (M18 unit) | |||||
| (acc, curr) => acc + (curr.purchaseAcceptedQty || 0), | |||||
| 0, | 0, | ||||
| ); | ); | ||||
| return total; | return total; | ||||
| @@ -231,7 +232,8 @@ function PoInputGrid({ | |||||
| itemName: params.row.itemName, | itemName: params.row.itemName, | ||||
| // purchaseOrderId: params.row.purchaseOrderId, | // purchaseOrderId: params.row.purchaseOrderId, | ||||
| purchaseOrderLineId: params.row.purchaseOrderLineId, | 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); | const res = await createStockInLine(postData); | ||||
| console.log(res); | console.log(res); | ||||
| @@ -516,7 +518,7 @@ function PoInputGrid({ | |||||
| // // flex: 0.6, | // // flex: 0.6, | ||||
| // }, | // }, | ||||
| { | { | ||||
| field: "acceptedQty", | |||||
| field: "purchaseAcceptedQty", | |||||
| headerName: t("acceptedQty"), | headerName: t("acceptedQty"), | ||||
| // flex: 0.5, | // flex: 0.5, | ||||
| width: 125, | width: 125, | ||||
| @@ -524,7 +526,7 @@ function PoInputGrid({ | |||||
| // editable: true, | // editable: true, | ||||
| // replace with tooltip + content | // replace with tooltip + content | ||||
| renderCell: (params) => { | renderCell: (params) => { | ||||
| const qty = params.row.purchaseAcceptedQty ?? params.row.acceptedQty ?? 0; | |||||
| const qty = params.row.purchaseAcceptedQty ?? 0; | |||||
| return integerFormatter.format(qty); | return integerFormatter.format(qty); | ||||
| } | } | ||||
| }, | }, | ||||
| @@ -818,7 +820,8 @@ function PoInputGrid({ | |||||
| purchaseOrderLineId: itemDetail.id, | purchaseOrderLineId: itemDetail.id, | ||||
| itemNo: itemDetail.itemNo, | itemNo: itemDetail.itemNo, | ||||
| itemName: itemDetail.itemName, | 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, | uom: itemDetail.uom, | ||||
| status: "draft", | status: "draft", | ||||
| }; | }; | ||||
| @@ -840,8 +843,13 @@ function PoInputGrid({ | |||||
| const error: StockInLineEntryError = {}; | const error: StockInLineEntryError = {}; | ||||
| console.log(newRow); | console.log(newRow); | ||||
| console.log(currQty); | 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; | return Object.keys(error).length > 0 ? error : undefined; | ||||
| }, | }, | ||||
| @@ -872,7 +880,7 @@ function PoInputGrid({ | |||||
| setEntries(newEntries); | setEntries(newEntries); | ||||
| //update remaining qty | //update remaining qty | ||||
| const total = newEntries.reduce( | const total = newEntries.reduce( | ||||
| (acc, curr) => acc + (curr.acceptedQty || 0), | |||||
| (acc, curr) => acc + (curr.purchaseAcceptedQty || 0), | |||||
| 0, | 0, | ||||
| ); | ); | ||||
| setCurrQty(total); | setCurrQty(total); | ||||
| @@ -395,7 +395,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||||
| <TextField | <TextField | ||||
| label={t("acceptedPutawayQty")} // TODO: fix it back to acceptedQty after db is fixed | label={t("acceptedPutawayQty")} // TODO: fix it back to acceptedQty after db is fixed | ||||
| fullWidth | fullWidth | ||||
| value={itemDetail.acceptedQty ?? itemDetail.demandQty} | |||||
| value={itemDetail.qty ?? itemDetail.purchaseAcceptedQty ?? itemDetail.acceptedQty ?? itemDetail.demandQty} | |||||
| disabled | disabled | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| @@ -403,7 +403,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, sugg | |||||
| <TextField | <TextField | ||||
| label={t("uom")} | label={t("uom")} | ||||
| fullWidth | fullWidth | ||||
| value={itemDetail.uom?.udfudesc} | |||||
| value={itemDetail.purchaseUomDesc ?? itemDetail.uom?.udfudesc} | |||||
| disabled | disabled | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| @@ -13,7 +13,7 @@ import { WarehouseResult } from "@/app/api/warehouse"; | |||||
| import NotificationIcon from "@mui/icons-material/NotificationImportant"; | import NotificationIcon from "@mui/icons-material/NotificationImportant"; | ||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { defaultPagingController } from "../SearchResults/SearchResults"; | 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 dayjs from "dayjs"; | ||||
| import { arrayToDateString, dayjsToDateString } from "@/app/utils/formatUtil"; | import { arrayToDateString, dayjsToDateString } from "@/app/utils/formatUtil"; | ||||
| import arraySupport from "dayjs/plugin/arraySupport"; | import arraySupport from "dayjs/plugin/arraySupport"; | ||||
| @@ -289,7 +289,20 @@ const PoSearch: React.FC<Props> = ({ | |||||
| }; | }; | ||||
| setAutoSyncStatus(null); | setAutoSyncStatus(null); | ||||
| const res = await fetchPoListClient(params); | |||||
| const cleanedQuery: Record<string, string> = {}; | |||||
| 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) return; | ||||
| if (res.records && res.records.length > 0) { | if (res.records && res.records.length > 0) { | ||||
| @@ -340,14 +353,6 @@ const PoSearch: React.FC<Props> = ({ | |||||
| if (syncOk) { | if (syncOk) { | ||||
| setAutoSyncStatus("成功找到PO"); | setAutoSyncStatus("成功找到PO"); | ||||
| // Re-fetch /po/list directly from client to avoid cached server action results. | |||||
| const cleanedQuery: Record<string, string> = {}; | |||||
| 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( | const listResp = await clientAuthFetch( | ||||
| `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams( | `${NEXT_PUBLIC_API_URL}/po/list?${new URLSearchParams( | ||||
| cleanedQuery, | cleanedQuery, | ||||
| @@ -32,7 +32,14 @@ import { SessionWithTokens } from "@/config/authConfig"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import SearchBox, { Criterion } from "@/components/SearchBox/SearchBox"; | 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 { | import { | ||||
| AllJoborderProductProcessInfoResponse, | AllJoborderProductProcessInfoResponse, | ||||
| @@ -50,8 +57,21 @@ interface ProductProcessListProps { | |||||
| onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void; | onSelectMatchingStock: (jobOrderId: number|undefined, productProcessId: number|undefined,pickOrderId: number|undefined) => void; | ||||
| printerCombo: PrinterCombo[]; | printerCombo: PrinterCombo[]; | ||||
| qcReady: boolean; | qcReady: boolean; | ||||
| listPersistedState: ProductionProcessListPersistedState; | |||||
| onListPersistedStateChange: React.Dispatch< | |||||
| React.SetStateAction<ProductionProcessListPersistedState> | |||||
| >; | |||||
| } | |||||
| 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"; | type SearchParam = "date" | "itemCode" | "jobOrderCode" | "processType"; | ||||
| const PAGE_SIZE = 50; | const PAGE_SIZE = 50; | ||||
| @@ -377,8 +397,14 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess | |||||
| } | } | ||||
| /> | /> | ||||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | ||||
| {t("Total job orders")}: {totalJobOrders} {selectedItemCodes.length > 0 ? `| ${t("Filtered")}: ${paged.length}` : ""} | |||||
| </Typography> | |||||
| {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}` : ""} | |||||
| </Typography> | |||||
| <Grid container spacing={2}> | <Grid container spacing={2}> | ||||
| {paged.map((process) => { | {paged.map((process) => { | ||||
| @@ -3,7 +3,9 @@ import React, { useState, useEffect, useCallback } from "react"; | |||||
| import { useSession } from "next-auth/react"; | import { useSession } from "next-auth/react"; | ||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import { Box, Tabs, Tab, Stack, Typography, Autocomplete, TextField } from "@mui/material"; | 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 ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail"; | ||||
| import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; | import ProductionProcessJobOrderDetail from "@/components/ProductionProcess/ProductionProcessJobOrderDetail"; | ||||
| import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | import JobPickExecutionsecondscan from "@/components/Jodetail/JobPickExecutionsecondscan"; | ||||
| @@ -43,6 +45,13 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| pickOrderId: number; | pickOrderId: number; | ||||
| } | null>(null); | } | null>(null); | ||||
| const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
| /** 列表搜尋/分頁:保留在切換工單詳情時,返回後仍為同一條件 */ | |||||
| const [productionListState, setProductionListState] = useState( | |||||
| createDefaultProductionProcessListPersistedState, | |||||
| ); | |||||
| const [finishedQcListState, setFinishedQcListState] = useState( | |||||
| createDefaultProductionProcessListPersistedState, | |||||
| ); | |||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | const currentUserId = session?.id ? parseInt(session.id) : undefined; | ||||
| @@ -180,6 +189,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| <ProductionProcessList | <ProductionProcessList | ||||
| printerCombo={printerCombo} | printerCombo={printerCombo} | ||||
| qcReady={false} | qcReady={false} | ||||
| listPersistedState={productionListState} | |||||
| onListPersistedStateChange={setProductionListState} | |||||
| onSelectProcess={(jobOrderId) => { | onSelectProcess={(jobOrderId) => { | ||||
| const id = jobOrderId ?? null; | const id = jobOrderId ?? null; | ||||
| if (id !== null) { | if (id !== null) { | ||||
| @@ -200,6 +211,8 @@ const ProductionProcessPage: React.FC<ProductionProcessPageProps> = ({ printerCo | |||||
| <ProductionProcessList | <ProductionProcessList | ||||
| printerCombo={printerCombo} | printerCombo={printerCombo} | ||||
| qcReady={true} | qcReady={true} | ||||
| listPersistedState={finishedQcListState} | |||||
| onListPersistedStateChange={setFinishedQcListState} | |||||
| onSelectProcess={(jobOrderId) => { | onSelectProcess={(jobOrderId) => { | ||||
| const id = jobOrderId ?? null; | const id = jobOrderId ?? null; | ||||
| if (id !== null) { | if (id !== null) { | ||||
| @@ -40,6 +40,8 @@ interface BaseCriterion<T extends string> { | |||||
| paramName2?: T; | paramName2?: T; | ||||
| // options?: T[] | string[]; | // options?: T[] | string[]; | ||||
| defaultValue?: string; | defaultValue?: string; | ||||
| /** 與 `defaultValue` 配對,用於 dateRange / datetimeRange 重置時的結束值 */ | |||||
| defaultValueTo?: string; | |||||
| preFilledValue?: string | { from?: string; to?: string }; | preFilledValue?: string | { from?: string; to?: string }; | ||||
| filterObj?: T; | filterObj?: T; | ||||
| handleSelectionChange?: (selectedOptions: T[]) => void; | handleSelectionChange?: (selectedOptions: T[]) => void; | ||||
| @@ -159,7 +161,7 @@ function SearchBox<T extends string>({ | |||||
| tempCriteria = { | tempCriteria = { | ||||
| ...tempCriteria, | ...tempCriteria, | ||||
| [c.paramName]: c.defaultValue ?? "", | [c.paramName]: c.defaultValue ?? "", | ||||
| [`${c.paramName}To`]: "", | |||||
| [`${c.paramName}To`]: c.defaultValueTo ?? "", | |||||
| }; | }; | ||||
| } | } | ||||
| return tempCriteria; | return tempCriteria; | ||||
| @@ -188,7 +190,7 @@ function SearchBox<T extends string>({ | |||||
| {} as Record<T | `${T}To`, string>, | {} as Record<T | `${T}To`, string>, | ||||
| ); | ); | ||||
| return {...defaultInputs, ...preFilledCriteria} | return {...defaultInputs, ...preFilledCriteria} | ||||
| }, [defaultInputs]) | |||||
| }, [defaultInputs, criteria]) | |||||
| const [inputs, setInputs] = useState(preFilledInputs); | const [inputs, setInputs] = useState(preFilledInputs); | ||||
| const [isReset, setIsReset] = useState(false); | const [isReset, setIsReset] = useState(false); | ||||
| @@ -17,7 +17,10 @@ | |||||
| "Process & Equipment": "製程與設備", | "Process & Equipment": "製程與設備", | ||||
| "Sequence": "順序", | "Sequence": "順序", | ||||
| "Process Name": "製程名稱", | "Process Name": "製程名稱", | ||||
| "Plan start (from)": "開始日期(從)", | |||||
| "Plan start (to)": "開始日期(至)", | |||||
| "Process Description": "說明", | "Process Description": "說明", | ||||
| "Search date": "搜索日期", | |||||
| "Confirm to Pass this Process?": "確認要通過此工序嗎?", | "Confirm to Pass this Process?": "確認要通過此工序嗎?", | ||||
| "Equipment Name": "設備", | "Equipment Name": "設備", | ||||
| "Confirm to update this Job Order?": "確認要完成此工單嗎?", | "Confirm to update this Job Order?": "確認要完成此工單嗎?", | ||||