From abe06be519a71c5bc507f27954f07f29c03b9139 Mon Sep 17 00:00:00 2001 From: "CANCERYS\\kw093" Date: Thu, 8 Jan 2026 11:44:34 +0800 Subject: [PATCH] update --- src/app/(main)/bag/page.tsx | 35 + src/app/(main)/jo/page.tsx | 2 +- src/app/api/bag/action.ts | 76 +- src/app/api/do/actions.tsx | 26 +- src/app/api/jo/actions.ts | 40 +- src/app/api/stockTake/actions.ts | 293 +++++++- src/components/BagSearch/BagSearch.tsx | 312 +++++++++ src/components/BagSearch/BagSearchWrapper.tsx | 15 + src/components/BagSearch/index.ts | 1 + src/components/JoSearch/JoSearch.tsx | 422 ++++++----- .../ProductionProcess/BagConsumptionForm.tsx | 8 +- .../ProductionProcessDetail.tsx | 658 ++++++++++++------ .../ProductionProcessJobOrderDetail.tsx | 157 ++++- .../ProductionProcessStepExecution.tsx | 1 + .../StockTakeManagement/ApproverCardList.tsx | 194 ++++++ .../StockTakeManagement/ApproverStockTake.tsx | 467 +++++++++++++ .../StockTakeManagement/PickerCardList.tsx | 211 ++++++ .../StockTakeManagement/PickerReStockTake.tsx | 543 +++++++++++++++ .../StockTakeManagement/PickerStockTake.tsx | 542 +++++++++++++++ .../StockTakeManagement.tsx | 2 +- .../StockTakeManagement/StockTakeTab.tsx | 501 +++---------- 21 files changed, 3721 insertions(+), 785 deletions(-) create mode 100644 src/app/(main)/bag/page.tsx create mode 100644 src/components/BagSearch/BagSearch.tsx create mode 100644 src/components/BagSearch/BagSearchWrapper.tsx create mode 100644 src/components/BagSearch/index.ts create mode 100644 src/components/StockTakeManagement/ApproverCardList.tsx create mode 100644 src/components/StockTakeManagement/ApproverStockTake.tsx create mode 100644 src/components/StockTakeManagement/PickerCardList.tsx create mode 100644 src/components/StockTakeManagement/PickerReStockTake.tsx create mode 100644 src/components/StockTakeManagement/PickerStockTake.tsx diff --git a/src/app/(main)/bag/page.tsx b/src/app/(main)/bag/page.tsx new file mode 100644 index 0000000..8e859ef --- /dev/null +++ b/src/app/(main)/bag/page.tsx @@ -0,0 +1,35 @@ +import BagSearchWrapper from "@/components/BagSearch/BagSearchWrapper"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import { Stack, Typography } from "@mui/material"; +import { Metadata } from "next"; +import React, { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Bag Usage Records" +} + +const bagPage: React.FC = async () => { + const { t } = await getServerI18n("jo"); + + return ( + <> + + + {t("Bag Usage")} + + + + }> + + + + + ) +} + +export default bagPage; \ No newline at end of file diff --git a/src/app/(main)/jo/page.tsx b/src/app/(main)/jo/page.tsx index 9027ebf..6e2f73a 100644 --- a/src/app/(main)/jo/page.tsx +++ b/src/app/(main)/jo/page.tsx @@ -23,7 +23,7 @@ const jo: React.FC = async () => { rowGap={2} > - {t("Job Order")} + {t("Search Job Order/ Create Job Order")} {/* TODO: Improve */} diff --git a/src/app/api/bag/action.ts b/src/app/api/bag/action.ts index 0232168..8d4bc21 100644 --- a/src/app/api/bag/action.ts +++ b/src/app/api/bag/action.ts @@ -44,4 +44,78 @@ export const getBagInfo = cache(async () => { body: JSON.stringify(request), } ); - }); \ No newline at end of file + }); + + export interface BagUsageRecordResponse { + id: number; + bagId: number; + bagLotLineId: number; + jobId: number; + jobOrderCode: string; + stockOutLineId: number; + startQty: number; + consumedQty: number; + scrapQty: number; + endQty: number; + date: string; + time: string; + bagName?: string; + bagCode?: string; + lotNo?: string; +} + +// 添加 API 调用函数: + +export const getBagUsageRecords = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/bag/bagUsageRecords`, + { + method: "GET", + next: { tags: ["bagUsageRecords"] }, + } + ); +}); +export interface BagSummaryResponse { + id: number; + bagName: string; + bagCode: string; + takenBagBalance: number; + deleted: boolean; +} +export interface BagLotLineResponse { + id: number; + bagId: number; + lotNo: string; + stockOutLineId: number; + startQty: number; + consumedQty: number; + scrapQty: number; + balanceQty: number; + firstUseDate: string; + lastUseDate: string; +} +export interface BagConsumptionResponse { + id: number; + bagId: number; + bagLotLineId: number; + jobId: number; + jobOrderCode: string; + stockOutLineId: number; + startQty: number; + consumedQty: number; + scrapQty: number; + endQty: number; + date: string; + time: string; +} +export const fetchBags = cache(async () => + serverFetchJson(`${BASE_API_URL}/bag/bags`, { method: "GET" }) +); + +export const fetchBagLotLines = cache(async (bagId: number) => + serverFetchJson(`${BASE_API_URL}/bag/bags/${bagId}/lot-lines`, { method: "GET" }) +); + +export const fetchBagConsumptions = cache(async (bagLotLineId: number) => + serverFetchJson(`${BASE_API_URL}/bag/lot-lines/${bagLotLineId}/consumptions`, { method: "GET" }) +); \ No newline at end of file diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index 5d54928..5007a80 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -318,6 +318,30 @@ export async function printDNLabels(request: PrintDNLabelsRequest){ return { success: true, message: "Print job sent successfully (labels)"} as PrintDeliveryNoteResponse } - +export interface Check4FTruckBatchResponse { + hasProblem: boolean; + problems: ProblemDoDto[]; +} +export interface ProblemDoDto { + deliveryOrderId: number; + deliveryOrderCode: string; + targetDate: string; + availableTrucks: TruckInfoDto[]; +} +export interface TruckInfoDto { + id: number; + truckLanceCode: string; + departureTime: string; + storeId: string; + shopCode: string; + shopName: string; +} +export const check4FTrucksBatch = cache(async (doIds: number[]) => { + return await serverFetchJson(`${BASE_API_URL}/do/check-4f-trucks-batch`, { + method: "POST", + body: JSON.stringify(doIds), + headers: { "Content-Type": "application/json" }, + }); +}); diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 306e1f4..2b76c7c 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -166,7 +166,19 @@ export const printFGStockInLabel = cache(async(data: PrintFGStockInLabelRequest) } ); }); +export interface UpdateJoReqQtyRequest { + id: number; + reqQty: number; +} +// 添加更新 reqQty 的函数 +export const updateJoReqQty = cache(async (data: UpdateJoReqQtyRequest) => { + return serverFetchJson(`${BASE_API_URL}/jo/updateReqQty`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }) +}) export const recordSecondScanIssue = cache(async ( pickOrderId: number, @@ -250,6 +262,7 @@ export interface ProductProcessWithLinesResponse { bomDescription: string; jobType: string; isDark: string; + bomBaseQty: number; isDense: number; isFloat: string; timeSequence: number; @@ -262,6 +275,7 @@ export interface ProductProcessWithLinesResponse { outputQty: number; outputQtyUom: string; productionPriority: number; + submitedBagRecord?: boolean; jobOrderLines: JobOrderLineInfo[]; productProcessLines: ProductProcessLineResponse[]; @@ -417,6 +431,7 @@ export interface JobOrderProcessLineDetailResponse { stopTime: string | number[]; totalPausedTimeMs?: number; // API 返回的是数组格式 status: string; + submitedBagRecord: boolean; outputFromProcessQty: number; outputFromProcessUom: string; defectQty: number; @@ -779,7 +794,14 @@ export const fetchProductProcessesByJobOrderId = cache(async (jobOrderId: number } ); }); - +export const newProductProcessLine = cache(async (lineId: number) => { + return serverFetchJson( + `${BASE_API_URL}/product-process/Demo/ProcessLine/new/${lineId}`, + { + method: "POST", + } + ); +}); // 获取 process 的所有 lines export const fetchProductProcessLines = cache(async (processId: number) => { return serverFetchJson( @@ -1129,4 +1151,20 @@ export const passProductProcessLine = async (lineId: number) => { headers: { "Content-Type": "application/json" }, } ); +}; +export interface UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest { + productProcessLineId: number; + processingTime: number; + setupTime: number; + changeoverTime: number; +} +export const updateProductProcessLineProcessingTimeSetupTimeChangeoverTime = async (lineId: number, request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest) => { + return serverFetchJson( + `${BASE_API_URL}/product-process/Demo/ProcessLine/update/processingTimeSetupTimeChangeoverTime/${lineId}`, + { + method: "POST", + body: JSON.stringify(request), + headers: { "Content-Type": "application/json" }, + } + ); }; \ No newline at end of file diff --git a/src/app/api/stockTake/actions.ts b/src/app/api/stockTake/actions.ts index b0cea1a..5e0c0e6 100644 --- a/src/app/api/stockTake/actions.ts +++ b/src/app/api/stockTake/actions.ts @@ -1,10 +1,91 @@ +// actions.ts "use server"; - -import { serverFetchString } from "@/app/utils/fetchUtil"; +import { cache } from 'react'; +import { serverFetchJson } from "@/app/utils/fetchUtil"; // 改为 serverFetchJson import { BASE_API_URL } from "@/config/api"; +export interface InventoryLotDetailResponse { + id: number; + inventoryLotId: number; + itemId: number; + itemCode: string; + itemName: string; + lotNo: string; + expiryDate: string; + productionDate: string; + stockInDate: string; + inQty: number; + outQty: number; + holdQty: number; + availableQty: number; + uom: string; + warehouseCode: string; + warehouseName: string; + warehouseSlot: string; + warehouseArea: string; + warehouse: string; + varianceQty: number | null; + status: string; + remarks: string | null; + stockTakeRecordStatus: string; + stockTakeRecordId: number | null; + firstStockTakeQty: number | null; + secondStockTakeQty: number | null; + firstBadQty: number | null; + secondBadQty: number | null; + approverQty: number | null; + approverBadQty: number | null; + finalQty: number | null; +} + +export const getInventoryLotDetailsBySection = async ( + stockTakeSection: string, + stockTakeId?: number | null +) => { + console.log('🌐 [API] getInventoryLotDetailsBySection called with:', { + stockTakeSection, + stockTakeId + }); + + const encodedSection = encodeURIComponent(stockTakeSection); + let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySection?stockTakeSection=${encodedSection}`; + if (stockTakeId != null && stockTakeId > 0) { + url += `&stockTakeId=${stockTakeId}`; + } + + console.log(' [API] Full URL:', url); + + const details = await serverFetchJson( + url, + { + method: "GET", + }, + ); + + console.log('[API] Response received:', details); + return details; +} +export interface SaveStockTakeRecordRequest { + stockTakeRecordId?: number | null; + inventoryLotLineId: number; + qty: number; + badQty: number; + //stockTakerName: string; + remark?: string | null; +} +export interface AllPickedStockTakeListReponse { + id: number; + stockTakeSession: string; + lastStockTakeDate: string | null; + status: string|null; + currentStockTakeItemNumber: number; + totalInventoryLotNumber: number; + stockTakeId: number; + stockTakerName: string | null; + totalItemNumber: number; +} export const importStockTake = async (data: FormData) => { - const importStockTake = await serverFetchString( + const importStockTake = await serverFetchJson( `${BASE_API_URL}/stockTake/import`, { method: "POST", @@ -12,4 +93,208 @@ export const importStockTake = async (data: FormData) => { }, ); return importStockTake; -} \ No newline at end of file +} + +export const getStockTakeRecords = async () => { + const stockTakeRecords = await serverFetchJson( // 改为 serverFetchJson + `${BASE_API_URL}/stockTakeRecord/AllPickedStockOutRecordList`, + { + method: "GET", + }, + ); + return stockTakeRecords; +} +export const getApproverStockTakeRecords = async () => { + const stockTakeRecords = await serverFetchJson( // 改为 serverFetchJson + `${BASE_API_URL}/stockTakeRecord/AllApproverStockTakeList`, + { + method: "GET", + }, + ); + return stockTakeRecords; +} +export const createStockTakeForSections = async () => { + const createStockTakeForSections = await serverFetchJson>( + `${BASE_API_URL}/stockTake/createForSections`, + { + method: "POST", + }, + ); + return createStockTakeForSections; +} +export const saveStockTakeRecord = async ( + request: SaveStockTakeRecordRequest, + stockTakeId: number, + stockTakerId: number +) => { + try { + const result = await serverFetchJson( + + `${BASE_API_URL}/stockTakeRecord/saveStockTakeRecord?stockTakeId=${stockTakeId}&stockTakerId=${stockTakerId}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }, + ); + console.log('saveStockTakeRecord: request:', request); + console.log('saveStockTakeRecord: stockTakeId:', stockTakeId); + console.log('saveStockTakeRecord: stockTakerId:', stockTakerId); + return result; + } catch (error: any) { + // 尝试从错误响应中提取消息 + if (error?.response) { + try { + const errorData = await error.response.json(); + const errorWithMessage = new Error(errorData.message || errorData.error || "Failed to save stock take record"); + (errorWithMessage as any).response = error.response; + throw errorWithMessage; + } catch { + throw error; + } + } + throw error; + } +} +export interface BatchSaveStockTakeRecordRequest { + stockTakeId: number; + stockTakeSection: string; + stockTakerId: number; + //stockTakerName: string; +} + +export interface BatchSaveStockTakeRecordResponse { + successCount: number; + errorCount: number; + errors: string[]; +} +export const batchSaveStockTakeRecords = cache(async (data: BatchSaveStockTakeRecordRequest) => { + return serverFetchJson(`${BASE_API_URL}/stockTakeRecord/batchSaveStockTakeRecords`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }) + }) + // Add these interfaces and functions + +export interface SaveApproverStockTakeRecordRequest { + stockTakeRecordId?: number | null; + qty: number; + badQty: number; + approverId?: number | null; + approverQty?: number | null; + approverBadQty?: number | null; +} + +export interface BatchSaveApproverStockTakeRecordRequest { + stockTakeId: number; + stockTakeSection: string; + approverId: number; +} + +export interface BatchSaveApproverStockTakeRecordResponse { + successCount: number; + errorCount: number; + errors: string[]; +} + + +export const saveApproverStockTakeRecord = async ( + request: SaveApproverStockTakeRecordRequest, + stockTakeId: number +) => { + try { + const result = await serverFetchJson( + `${BASE_API_URL}/stockTakeRecord/saveApproverStockTakeRecord?stockTakeId=${stockTakeId}`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(request), + }, + ); + return result; + } catch (error: any) { + if (error?.response) { + try { + const errorData = await error.response.json(); + const errorWithMessage = new Error(errorData.message || errorData.error || "Failed to save approver stock take record"); + (errorWithMessage as any).response = error.response; + throw errorWithMessage; + } catch { + throw error; + } + } + throw error; + } +} + +export const batchSaveApproverStockTakeRecords = cache(async (data: BatchSaveApproverStockTakeRecordRequest) => { + return serverFetchJson( + `${BASE_API_URL}/stockTakeRecord/batchSaveApproverStockTakeRecords`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + } + ) +} +) + +export const updateStockTakeRecordStatusToNotMatch = async ( + stockTakeRecordId: number +) => { + try { + const result = await serverFetchJson( + `${BASE_API_URL}/stockTakeRecord/updateStockTakeRecordStatusToNotMatch?stockTakeRecordId=${stockTakeRecordId}`, + { + method: "POST", + }, + ); + return result; + } catch (error: any) { + if (error?.response) { + try { + const errorData = await error.response.json(); + const errorWithMessage = new Error(errorData.message || errorData.error || "Failed to update stock take record status"); + (errorWithMessage as any).response = error.response; + throw errorWithMessage; + } catch { + throw error; + } + } + throw error; + } +} + +export const getInventoryLotDetailsBySectionNotMatch = async ( + stockTakeSection: string, + stockTakeId?: number | null +) => { + console.log('🌐 [API] getInventoryLotDetailsBySectionNotMatch called with:', { + stockTakeSection, + stockTakeId + }); + + const encodedSection = encodeURIComponent(stockTakeSection); + let url = `${BASE_API_URL}/stockTakeRecord/inventoryLotDetailsBySectionNotMatch?stockTakeSection=${encodedSection}`; + if (stockTakeId != null && stockTakeId > 0) { + url += `&stockTakeId=${stockTakeId}`; + } + + console.log(' [API] Full URL:', url); + + const details = await serverFetchJson( + url, + { + method: "GET", + }, + ); + + console.log('[API] Response received:', details); + return details; +} diff --git a/src/components/BagSearch/BagSearch.tsx b/src/components/BagSearch/BagSearch.tsx new file mode 100644 index 0000000..3302852 --- /dev/null +++ b/src/components/BagSearch/BagSearch.tsx @@ -0,0 +1,312 @@ +"use client"; + +import { useEffect, useMemo, useState, useCallback } from "react"; +import { + fetchBags, + fetchBagLotLines, + fetchBagConsumptions, + BagSummaryResponse, + BagLotLineResponse, + BagConsumptionResponse, +} from "@/app/api/bag/action"; +import SearchResults, { + Column, + defaultPagingController, +} from "../SearchResults/SearchResults"; +import { useTranslation } from "react-i18next"; +import { + integerFormatter, + arrayToDateString, + arrayToDateTimeString, +} from "@/app/utils/formatUtil"; +import { + Box, + Typography, + Stack, + IconButton, + Button, +} from "@mui/material"; +import VisibilityIcon from "@mui/icons-material/Visibility"; + +type ViewLevel = "bag" | "lot" | "consumption"; + +const BagSearch: React.FC = () => { + const { t } = useTranslation("jo"); + + const [level, setLevel] = useState("bag"); + + + const [selectedBag, setSelectedBag] = useState(null); + const [selectedLotLine, setSelectedLotLine] = useState(null); + + + const [bags, setBags] = useState([]); + const [lotLines, setLotLines] = useState([]); + const [consumptions, setConsumptions] = useState([]); + + const [pagingController, setPagingController] = useState(defaultPagingController); + const [totalCount, setTotalCount] = useState(0); + + const bagColumns = useMemo[]>(() => [ + { + name: "bagName", + label: t("Bag Name"), + flex: 2, + }, + { + name: "bagCode", + label: t("Bag Code"), + flex: 1, + }, + { + name: "takenBagBalance", + label: t("Balance"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.takenBagBalance ?? 0), + }, + { + name: "actions" as any, + label: t("Actions"), + align: "center", + headerAlign: "center", + renderCell: (row) => ( + handleViewBagDetail(row)} + > + + + ), + }, + ], [t]); + + + const lotColumns = useMemo[]>(() => [ + { + name: "lotNo", + label: t("Lot No"), + flex: 2, + }, + { + name: "startQty", + label: t("Start Qty"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.startQty ?? 0), + }, + { + name: "consumedQty", + label: t("Consumed Qty"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.consumedQty ?? 0), + }, + { + name: "scrapQty", + label: t("Scrap Qty"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.scrapQty ?? 0), + }, + { + name: "balanceQty", + label: t("Balance Qty"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.balanceQty ?? 0), + }, + { + name: "actions" as any, + label: t("Actions"), + align: "center", + headerAlign: "center", + renderCell: (row) => ( + handleViewLotDetail(row)} + > + + + ), + }, + ], [t]); + + + const consColumns = useMemo[]>(() => [ + { + name: "jobOrderCode", + label: t("Job Order Code"), + flex: 2, + }, + { + name: "startQty", + label: t("Start Qty"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.startQty ?? 0), + }, + { + name: "consumedQty", + label: t("Consumed Qty"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.consumedQty ?? 0), + }, + { + name: "scrapQty", + label: t("Scrap Qty"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.scrapQty ?? 0), + }, + { + name: "endQty", + label: t("End Qty"), + align: "right", + headerAlign: "right", + renderCell: (row) => integerFormatter.format(row.endQty ?? 0), + }, + { + name: "date", + label: t("Date"), + renderCell: (row) => + row.date ? arrayToDateString(row.date as any) : "-", + }, + { + name: "time", + label: t("Time"), + renderCell: (row) => + row.time ? arrayToDateTimeString(row.time as any) : "-", + }, + ], [t]); + + + useEffect(() => { + const load = async () => { + const data = await fetchBags(); + const safe = data ?? []; + setBags(safe); + setTotalCount(safe.length); + setPagingController(defaultPagingController); + setLevel("bag"); + setSelectedBag(null); + setSelectedLotLine(null); + setLotLines([]); + setConsumptions([]); + }; + load(); + }, []); + + const handleViewBagDetail = useCallback(async (row: BagSummaryResponse) => { + setSelectedBag(row); + setLevel("lot"); + setPagingController(defaultPagingController); + setSelectedLotLine(null); + setConsumptions([]); + const data = await fetchBagLotLines(row.id); + const safe = data ?? []; + setLotLines(safe); + setTotalCount(safe.length); + }, []); + + + const handleViewLotDetail = useCallback(async (row: BagLotLineResponse) => { + setSelectedLotLine(row); + setLevel("consumption"); + setPagingController(defaultPagingController); + const data = await fetchBagConsumptions(row.id); + const safe = data ?? []; + setConsumptions(safe); + setTotalCount(safe.length); + }, []); + + + const handleBack = useCallback(async () => { + if (level === "consumption" && selectedBag) { + + setLevel("lot"); + setSelectedLotLine(null); + setPagingController(defaultPagingController); + const data = await fetchBagLotLines(selectedBag.id); + const safe = data ?? []; + setLotLines(safe); + setTotalCount(safe.length); + return; + } + if (level === "lot") { + + setLevel("bag"); + setSelectedBag(null); + setSelectedLotLine(null); + setPagingController(defaultPagingController); + setTotalCount(bags.length); + return; + } + }, [level, selectedBag, bags]); + + + const { title, items, columns } = useMemo(() => { + if (level === "bag") { + return { + title: t("Bag List"), + items: bags, + columns: bagColumns, + }; + } + if (level === "lot") { + return { + title: `${t("Bag Lot Lines")}${ + selectedBag ? ` - ${selectedBag.bagName ?? ""} (${selectedBag.bagCode ?? ""})` : "" + }`, + items: lotLines, + columns: lotColumns, + }; + } + + return { + title: `${t("Bag Consumption Records")}${ + selectedLotLine ? ` - ${selectedLotLine.lotNo ?? ""}` : "" + }`, + items: consumptions, + columns: consColumns, + }; + }, [level, t, bags, bagColumns, lotLines, lotColumns, consumptions, consColumns, selectedBag, selectedLotLine]); + + + + return ( + + + + {t(" ")} + + + + {level !== "bag" && ( + + )} + + + + + {title} + + + + items={items ?? []} + columns={columns as any} + setPagingController={setPagingController} + pagingController={pagingController} + totalCount={totalCount} + isAutoPaging={false} + /> + + ); +}; + +export default BagSearch; \ No newline at end of file diff --git a/src/components/BagSearch/BagSearchWrapper.tsx b/src/components/BagSearch/BagSearchWrapper.tsx new file mode 100644 index 0000000..c496235 --- /dev/null +++ b/src/components/BagSearch/BagSearchWrapper.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import GeneralLoading from "../General/GeneralLoading"; +import BagSearch from "./BagSearch"; + +interface SubComponents { + Loading: typeof GeneralLoading; +} + +const BagSearchWrapper: React.FC & SubComponents = async () => { + return +} + +BagSearchWrapper.Loading = GeneralLoading; + +export default BagSearchWrapper; \ No newline at end of file diff --git a/src/components/BagSearch/index.ts b/src/components/BagSearch/index.ts new file mode 100644 index 0000000..866025e --- /dev/null +++ b/src/components/BagSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./BagSearchWrapper" \ No newline at end of file diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index 0b24f13..c9931c7 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -1,5 +1,5 @@ "use client" -import { SearchJoResultRequest, fetchJos, updateJo,updateProductProcessPriority } from "@/app/api/jo/actions"; +import { SearchJoResultRequest, fetchJos, updateJo, updateProductProcessPriority, updateJoReqQty } from "@/app/api/jo/actions"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Criterion } from "../SearchBox"; @@ -12,7 +12,7 @@ import { useRouter } from "next/navigation"; import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; import { StockInLineInput } from "@/app/api/stockIn"; import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo"; -import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment } from "@mui/material"; +import { Button, Stack, Dialog, DialogTitle, DialogContent, DialogActions, TextField, IconButton, InputAdornment, Typography, Box } from "@mui/material"; import { BomCombo } from "@/app/api/bom"; import JoCreateFormModal from "./JoCreateFormModal"; import AddIcon from '@mui/icons-material/Add'; @@ -55,14 +55,16 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT const [inventoryData, setInventoryData] = useState([]); const [detailedJos, setDetailedJos] = useState>(new Map()); - const [openOperationPriorityDialog, setOpenOperationPriorityDialog] = useState(false); - const [operationPriority, setOperationPriority] = useState(50); - const [selectedJo, setSelectedJo] = useState(null); - const [selectedProductProcessId, setSelectedProductProcessId] = useState(null); - const [openPlanStartDialog, setOpenPlanStartDialog] = useState(false); - const [planStartDate, setPlanStartDate] = useState(null); - const [selectedJoForDate, setSelectedJoForDate] = useState(null); - + + // 合并后的统一编辑 Dialog 状态 + const [openEditDialog, setOpenEditDialog] = useState(false); + const [selectedJoForEdit, setSelectedJoForEdit] = useState(null); + const [editPlanStartDate, setEditPlanStartDate] = useState(null); + const [editReqQtyMultiplier, setEditReqQtyMultiplier] = useState(1); + const [editBomForReqQty, setEditBomForReqQty] = useState(null); + const [editProductionPriority, setEditProductionPriority] = useState(50); + const [editProductProcessId, setEditProductProcessId] = useState(null); + const fetchJoDetailClient = async (id: number): Promise => { const response = await fetch(`/api/jo/detail?id=${id}`); if (!response.ok) { @@ -111,32 +113,6 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT fetchInventoryData(); }, []); - const handleOpenPriorityDialog = useCallback(async (jo: JobOrder) => { - setSelectedJo(jo); - setOperationPriority(jo.productionPriority ?? 50); - - // 获取 productProcessId - try { - const { fetchProductProcessesByJobOrderId } = await import("@/app/api/jo/actions"); - const processes = await fetchProductProcessesByJobOrderId(jo.id); - if (processes && processes.length > 0) { - setSelectedProductProcessId(processes[0].id); - setOpenOperationPriorityDialog(true); - } else { - msg(t("No product process found for this job order")); - } - } catch (error) { - console.error("Error fetching product process:", error); - msg(t("Error loading product process")); - } - }, [t]); - - const handleClosePriorityDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { - setOpenOperationPriorityDialog(false); - setSelectedJo(null); - setSelectedProductProcessId(null); - }, []); - const getStockAvailable = (pickLine: JoDetailPickLine) => { const inventory = inventoryData.find(inventory => inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name @@ -175,19 +151,72 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT options: jobTypes.map(jt => jt.name) }, ], [t, jobTypes]) - const handleOpenPlanStartDialog = useCallback((jo: JobOrder) => { - setSelectedJoForDate(jo); - // 将 planStart 数组转换为 dayjs 对象 + + const fetchBomForJo = useCallback(async (jo: JobOrder): Promise => { + try { + const detailedJo = detailedJos.get(jo.id) || await fetchJoDetailClient(jo.id); + const matchingBom = bomCombo.find(bom => { + return true; // 临时占位 + }); + return matchingBom || null; + } catch (error) { + console.error("Error fetching BOM for JO:", error); + return null; + } + }, [bomCombo, detailedJos]); + + // 统一的打开编辑对话框函数 + const handleOpenEditDialog = useCallback(async (jo: JobOrder) => { + setSelectedJoForEdit(jo); + + // 设置 Plan Start Date if (jo.planStart && Array.isArray(jo.planStart)) { - setPlanStartDate(arrayToDayjs(jo.planStart)); + setEditPlanStartDate(arrayToDayjs(jo.planStart)); } else { - setPlanStartDate(dayjs()); + setEditPlanStartDate(dayjs()); } - setOpenPlanStartDialog(true); + + // 设置 Production Priority + setEditProductionPriority(jo.productionPriority ?? 50); + + // 获取 productProcessId + try { + const { fetchProductProcessesByJobOrderId } = await import("@/app/api/jo/actions"); + const processes = await fetchProductProcessesByJobOrderId(jo.id); + if (processes && processes.length > 0) { + setEditProductProcessId(processes[0].id); + } + } catch (error) { + console.error("Error fetching product process:", error); + } + + // 设置 ReqQty + const bom = await fetchBomForJo(jo); + if (bom) { + setEditBomForReqQty(bom); + const currentMultiplier = bom.outputQty > 0 + ? Math.round(jo.reqQty / bom.outputQty) + : 1; + setEditReqQtyMultiplier(currentMultiplier); + } + + setOpenEditDialog(true); + }, [fetchBomForJo]); + + // 统一的关闭函数 + const handleCloseEditDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { + setOpenEditDialog(false); + setSelectedJoForEdit(null); + setEditPlanStartDate(null); + setEditReqQtyMultiplier(1); + setEditBomForReqQty(null); + setEditProductionPriority(50); + setEditProductProcessId(null); }, []); const columns = useMemo[]>( () => [ + { name: "planStart", label: t("Estimated Production Date"), @@ -196,19 +225,20 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT renderCell: (row) => { return ( - {row.planStart ? arrayToDateString(row.planStart) : '-'} - {row.status == "planning" && ( - { - e.stopPropagation(); - handleOpenPlanStartDialog(row); - }} - sx={{ padding: '4px' }} - > - - + {row.status == "planning" && ( + { + e.stopPropagation(); + handleOpenEditDialog(row); + }} + sx={{ padding: '4px' }} + > + + )} + {row.planStart ? arrayToDateString(row.planStart) : '-'} + ); } @@ -220,16 +250,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT return ( {integerFormatter.format(row.productionPriority)} - { - e.stopPropagation(); - handleOpenPriorityDialog(row); - }} - sx={{ padding: '4px' }} - > - - + ); } @@ -239,12 +260,11 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT label: t("Code"), flex: 2 }, - { name: "item", label: `${t("Item Name")}`, renderCell: (row) => { - return row.item ? <>{t(row.item.code)} {t(row.item.name)} : '-' + return row.item ? <>{t(row.jobTypeName)} {t(row.item.code)} {t(row.item.name)} : '-' } }, { @@ -253,7 +273,12 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT align: "right", headerAlign: "right", renderCell: (row) => { - return integerFormatter.format(row.reqQty) + return ( + + {integerFormatter.format(row.reqQty)} + + + ); } }, { @@ -288,13 +313,6 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT } }, - { - name: "jobTypeName", - label: t("Job Type"), - renderCell: (row) => { - return row.jobTypeName ? t(row.jobTypeName) : '-' - } - }, { name: "id", label: t("Actions"), @@ -313,10 +331,9 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT ) } }, - ], [t, inventoryData, detailedJos, handleOpenPriorityDialog,handleOpenPlanStartDialog] + ], [t, inventoryData, detailedJos, handleOpenEditDialog] ) - // 按照 PoSearch 的模式:创建 newPageFetch 函数 const newPageFetch = useCallback( async ( pagingController: { pageNum: number; pageSize: number }, @@ -333,7 +350,6 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT if (response && response.records) { console.log("newPageFetch - setting filteredJos with", response.records.length, "records"); setTotalCount(response.total); - // 后端已经按 id DESC 排序,不需要再次排序 setFilteredJos(response.records); console.log("newPageFetch - filteredJos set, first record id:", response.records[0]?.id); } else { @@ -343,21 +359,73 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT }, [], ); + + const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => { + try { + const response = await updateJoReqQty({ + id: jobOrderId, + reqQty: newReqQty + }); + + if (response) { + msg(t("update success")); + await newPageFetch(pagingController, inputs); + } + } catch (error) { + console.error("Error updating reqQty:", error); + msg(t("update failed")); + } + }, [pagingController, inputs, newPageFetch, t]); + + const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => { + const response = await updateJoPlanStart({ id: jobOrderId, planStart }); + if (response) { + await newPageFetch(pagingController, inputs); + } + }, [pagingController, inputs, newPageFetch]); + const handleUpdateOperationPriority = useCallback(async (productProcessId: number, productionPriority: number) => { const response = await updateProductProcessPriority(productProcessId, productionPriority) if (response) { - // 刷新数据 await newPageFetch(pagingController, inputs); } }, [pagingController, inputs, newPageFetch]); - const handleConfirmPriority = useCallback(async () => { - if (!selectedProductProcessId) return; - await handleUpdateOperationPriority(selectedProductProcessId, Number(operationPriority)); - setOpenOperationPriorityDialog(false); - setSelectedJo(null); - setSelectedProductProcessId(null); - }, [selectedProductProcessId, operationPriority, handleUpdateOperationPriority]); - // 按照 PoSearch 的模式:使用相同的 useEffect 逻辑 + + // 统一的确认函数 + const handleConfirmEdit = useCallback(async () => { + if (!selectedJoForEdit) return; + + try { + // 更新 Plan Start + if (editPlanStartDate) { + const dateString = `${dayjsToDateString(editPlanStartDate, "input")}T00:00:00`; + await handleUpdatePlanStart(selectedJoForEdit.id, dateString); + } + + // 更新 ReqQty + if (editBomForReqQty) { + const newReqQty = editReqQtyMultiplier * editBomForReqQty.outputQty; + await handleUpdateReqQty(selectedJoForEdit.id, newReqQty); + } + + // 更新 Production Priority + if (editProductProcessId) { + await handleUpdateOperationPriority(editProductProcessId, Number(editProductionPriority)); + } + + setOpenEditDialog(false); + setSelectedJoForEdit(null); + setEditPlanStartDate(null); + setEditReqQtyMultiplier(1); + setEditBomForReqQty(null); + setEditProductionPriority(50); + setEditProductProcessId(null); + } catch (error) { + console.error("Error updating:", error); + msg(t("update failed")); + } + }, [selectedJoForEdit, editPlanStartDate, editBomForReqQty, editReqQtyMultiplier, editProductionPriority, editProductProcessId, handleUpdatePlanStart, handleUpdateReqQty, handleUpdateOperationPriority, t]); + useEffect(() => { newPageFetch(pagingController, inputs); }, [newPageFetch, pagingController, inputs]); @@ -378,7 +446,6 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT const res = await createStockInLine(postData); console.log(`%c Created Stock In Line`, "color:lime", res); msg(t("update success")); - // 重置为默认输入,让 useEffect 自动触发 setInputs(defaultInputs); setPagingController(defaultPagingController); } @@ -427,7 +494,6 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT const closeNewModal = useCallback(() => { setOpenModal(false); - setInputs(defaultInputs); setPagingController(defaultPagingController); }, [defaultInputs]); @@ -440,7 +506,6 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT jobTypeName: query.jobTypeName && query.jobTypeName !== "All" ? query.jobTypeName : "" }; - setInputs({ code: transformedQuery.code, itemName: transformedQuery.itemName, @@ -452,38 +517,11 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT setPagingController(defaultPagingController); }, [defaultInputs]) - const handleClosePlanStartDialog = useCallback((_event?: object, _reason?: "backdropClick" | "escapeKeyDown") => { - setOpenPlanStartDialog(false); - setSelectedJoForDate(null); - setPlanStartDate(null); - }, []); - - const handleUpdatePlanStart = useCallback(async (jobOrderId: number, planStart: string) => { - const response = await updateJoPlanStart({ id: jobOrderId, planStart }); - if (response) { - // 刷新数据 - await newPageFetch(pagingController, inputs); - } - }, [pagingController, inputs, newPageFetch]); - - const handleConfirmPlanStart = useCallback(async () => { - if (!selectedJoForDate?.id || !planStartDate) return; - - // 将日期转换为后端需要的格式 (YYYY-MM-DDTHH:mm:ss) - const dateString = `${dayjsToDateString(planStartDate, "input")}T00:00:00`; - await handleUpdatePlanStart(selectedJoForDate.id, dateString); - setOpenPlanStartDialog(false); - setSelectedJoForDate(null); - setPlanStartDate(null); - }, [selectedJoForDate, planStartDate, handleUpdatePlanStart]); - const onReset = useCallback(() => { - setInputs(defaultInputs); setPagingController(defaultPagingController); }, [defaultInputs]) - const onOpenCreateJoModal = useCallback(() => { setIsCreateJoModalOpen(() => true) }, []) @@ -526,7 +564,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT jobTypes={jobTypes} onClose={onCloseCreateJoModal} onSearch={() => { - setInputs({ ...defaultInputs }); // 创建新对象,确保引用变化 + setInputs({ ...defaultInputs }); setPagingController(defaultPagingController); }} /> @@ -538,66 +576,128 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT inputDetail={modalInfo} printerCombo={printerCombo} /> - - {t("Update Production Priority")} - - setOperationPriority(Number(e.target.value))} - /> - - - - - - - - {t("Update Estimated Production Date")} + {t("Edit Job Order")} - - setPlanStartDate(newValue)} - slotProps={{ - textField: { - fullWidth: true, - margin: "dense", - autoFocus: true, + + {/* Plan Start Date */} + + setEditPlanStartDate(newValue)} + slotProps={{ + textField: { + fullWidth: true, + margin: "dense", + } + }} + /> + + + {/* Production Priority */} + { + const val = Number(e.target.value); + if (val >= 1 && val <= 100) { + setEditProductionPriority(val); } }} + inputProps={{ + min: 1, + max: 100, + step: 1 + }} /> - + + {/* ReqQty */} + {editBomForReqQty && ( + + + + {editBomForReqQty.outputQtyUom} + + + ) : null + }} + sx={{ flex: 1 }} + /> + + × + + { + const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value))); + setEditReqQtyMultiplier(val); + }} + inputProps={{ + min: 1, + step: 1 + }} + sx={{ flex: 1 }} + /> + + = + + + + {editBomForReqQty.outputQtyUom} + + + ) : null + }} + sx={{ flex: 1 }} + /> + + )} + - + - - } export default JoSearch; \ No newline at end of file diff --git a/src/components/ProductionProcess/BagConsumptionForm.tsx b/src/components/ProductionProcess/BagConsumptionForm.tsx index b98a7a2..8d5483c 100644 --- a/src/components/ProductionProcess/BagConsumptionForm.tsx +++ b/src/components/ProductionProcess/BagConsumptionForm.tsx @@ -39,6 +39,7 @@ interface BagConsumptionFormProps { lineId: number; bomDescription?: string; isLastLine: boolean; + submitedBagRecord?: boolean; onRefresh?: () => void; } @@ -47,6 +48,7 @@ const BagConsumptionForm: React.FC = ({ lineId, bomDescription, isLastLine, + submitedBagRecord, onRefresh, }) => { const { t } = useTranslation(["common", "jo"]); @@ -59,8 +61,12 @@ const BagConsumptionForm: React.FC = ({ // 判断是否显示表单 const shouldShow = useMemo(() => { + // 如果 submitedBagRecord 为 true,则不显示表单 + if (submitedBagRecord === true) { + return false; + } return bomDescription === "FG" && isLastLine; - }, [bomDescription, isLastLine]); + }, [bomDescription, isLastLine, submitedBagRecord]); // 加载 Bag 列表 useEffect(() => { diff --git a/src/components/ProductionProcess/ProductionProcessDetail.tsx b/src/components/ProductionProcess/ProductionProcessDetail.tsx index 0095983..345040d 100644 --- a/src/components/ProductionProcess/ProductionProcessDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessDetail.tsx @@ -1,5 +1,8 @@ "use client"; import React, { useCallback, useEffect, useState, useRef } from "react"; +import EditIcon from "@mui/icons-material/Edit"; +import AddIcon from '@mui/icons-material/Add'; +import Fab from '@mui/material/Fab'; import { Box, Button, @@ -21,6 +24,7 @@ import { DialogTitle, DialogContent, DialogActions, + IconButton } from "@mui/material"; import QrCodeIcon from '@mui/icons-material/QrCode'; import { useTranslation } from "react-i18next"; @@ -40,9 +44,12 @@ import { ProductProcessLineInfoResponse, startProductProcessLine, fetchProductProcessesByJobOrderId, - ProductProcessWithLinesResponse, // ✅ 添加 + ProductProcessWithLinesResponse, // 添加 ProductProcessLineResponse, passProductProcessLine, + newProductProcessLine, + updateProductProcessLineProcessingTimeSetupTimeChangeoverTime, + UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest, } from "@/app/api/jo/actions"; import { updateProductProcessLineStatus } from "@/app/api/jo/actions"; @@ -61,16 +68,21 @@ const ProductionProcessDetail: React.FC = ({ onBack, fromJosave, }) => { + console.log(" ProductionProcessDetail RENDER", { jobOrderId, fromJosave }); + const { t } = useTranslation("common"); const { data: session } = useSession() as { data: SessionWithTokens | null }; const currentUserId = session?.id ? parseInt(session.id) : undefined; const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const [showOutputPage, setShowOutputPage] = useState(false); // 基本信息 - const [processData, setProcessData] = useState(null); // ✅ 修改类型 - const [lines, setLines] = useState([]); // ✅ 修改类型 + const [processData, setProcessData] = useState(null); // 修改类型 + const [lines, setLines] = useState([]); // 修改类型 const [loading, setLoading] = useState(false); - + const linesRef = useRef([]); + const onBackRef = useRef(onBack); +const fetchProcessDetailRef = useRef<() => Promise>(); + // 选中的 line 和执行状态 const [selectedLineId, setSelectedLineId] = useState(null); const [isExecutingLine, setIsExecutingLine] = useState(false); @@ -88,8 +100,14 @@ const ProductionProcessDetail: React.FC = ({ const [lineDetailForScan, setLineDetailForScan] = useState(null); const [showScanDialog, setShowScanDialog] = useState(false); const autoSubmitTimerRef = useRef(null); + const [openTimeDialog, setOpenTimeDialog] = useState(false); + const [editingLineId, setEditingLineId] = useState(null); + const [timeValues, setTimeValues] = useState({ + durationInMinutes: 0, + prepTimeInMinutes: 0, + postProdTimeInMinutes: 0, + }); - // 产出表单 const [outputData, setOutputData] = useState({ byproductName: "", byproductQty: "", @@ -110,43 +128,122 @@ const ProductionProcessDetail: React.FC = ({ setSelectedLineId(null); setShowOutputPage(false); }; - + useEffect(() => { + onBackRef.current = onBack; + }, [onBack]); // 获取 process 和 lines 数据 const fetchProcessDetail = useCallback(async () => { + console.log(" fetchProcessDetail CALLED", { jobOrderId, timestamp: new Date().toISOString() }); setLoading(true); try { - console.log(`🔍 Loading process detail for JobOrderId: ${jobOrderId}`); + console.log(` Loading process detail for JobOrderId: ${jobOrderId}`); - // 使用 fetchProductProcessesByJobOrderId 获取基础数据 const processesWithLines = await fetchProductProcessesByJobOrderId(jobOrderId); if (!processesWithLines || processesWithLines.length === 0) { throw new Error("No processes found for this job order"); } - // 如果有多个 process,取第一个(或者可以根据需要选择) const currentProcess = processesWithLines[0]; - setProcessData(currentProcess); - // 使用 productProcessLines 字段(API 返回的字段名) const lines = currentProcess.productProcessLines || []; setLines(lines); - - console.log(" Process data loaded:", currentProcess); - console.log(" Lines loaded:", lines); + linesRef.current = lines; + console.log(" Process data loaded:", currentProcess); + console.log(" Lines loaded:", lines); } catch (error) { - console.error("❌ Error loading process detail:", error); - //alert(`无法加载 Job Order ID ${jobOrderId} 的生产流程。该记录可能不存在。`); - onBack(); + console.error(" Error loading process detail:", error); + onBackRef.current(); } finally { setLoading(false); } - }, [jobOrderId, onBack]); - + }, [jobOrderId]); + + const handleOpenTimeDialog = useCallback((lineId: number) => { + console.log("🔓 handleOpenTimeDialog CALLED", { lineId, timestamp: new Date().toISOString() }); + + // 直接使用 linesRef.current,避免触发 setLines + const line = linesRef.current.find(l => l.id === lineId); + if (line) { + console.log(" Found line:", line); + setEditingLineId(lineId); + setTimeValues({ + durationInMinutes: line.durationInMinutes || 0, + prepTimeInMinutes: line.prepTimeInMinutes || 0, + postProdTimeInMinutes: line.postProdTimeInMinutes || 0, + }); + setOpenTimeDialog(true); + console.log(" Dialog opened"); + } else { + console.warn(" Line not found:", lineId); + } + }, []); useEffect(() => { - fetchProcessDetail(); + fetchProcessDetailRef.current = fetchProcessDetail; }, [fetchProcessDetail]); + const handleCloseTimeDialog = useCallback(() => { + console.log("🔒 handleCloseTimeDialog CALLED", { timestamp: new Date().toISOString() }); + setOpenTimeDialog(false); + setEditingLineId(null); + setTimeValues({ + durationInMinutes: 0, + prepTimeInMinutes: 0, + postProdTimeInMinutes: 0, + }); + console.log(" Dialog closed"); + }, []); + + const handleConfirmTimeUpdate = useCallback(async () => { + console.log("💾 handleConfirmTimeUpdate CALLED", { editingLineId, timeValues, timestamp: new Date().toISOString() }); + if (!editingLineId) return; + + try { + const request: UpdateProductProcessLineProcessingTimeSetupTimeChangeoverTimeRequest = { + productProcessLineId: editingLineId, + processingTime: timeValues.durationInMinutes, + setupTime: timeValues.prepTimeInMinutes, + changeoverTime: timeValues.postProdTimeInMinutes, + }; + + await updateProductProcessLineProcessingTimeSetupTimeChangeoverTime(editingLineId, request); + await fetchProcessDetail(); + handleCloseTimeDialog(); + } catch (error) { + console.error("Error updating time:", error); + alert(t("update failed")); + } + }, [editingLineId, timeValues, fetchProcessDetail, handleCloseTimeDialog, t]); + + useEffect(() => { + console.log("🔄 useEffect [jobOrderId] TRIGGERED", { + jobOrderId, + timestamp: new Date().toISOString() + }); + if (fetchProcessDetailRef.current) { + fetchProcessDetailRef.current(); + } + }, [jobOrderId]); + + // 添加监听 openTimeDialog 变化的 useEffect + useEffect(() => { + console.log(" openTimeDialog changed:", { openTimeDialog, timestamp: new Date().toISOString() }); + }, [openTimeDialog]); + + // 添加监听 timeValues 变化的 useEffect + useEffect(() => { + console.log(" timeValues changed:", { timeValues, timestamp: new Date().toISOString() }); + }, [timeValues]); + + // 添加监听 lines 变化的 useEffect + useEffect(() => { + console.log(" lines changed:", { count: lines.length, lines, timestamp: new Date().toISOString() }); + }, [lines]); + + // 添加监听 editingLineId 变化的 useEffect + useEffect(() => { + console.log(" editingLineId changed:", { editingLineId, timestamp: new Date().toISOString() }); + }, [editingLineId]); const handlePassLine = useCallback(async (lineId: number) => { try { @@ -158,7 +255,16 @@ const ProductionProcessDetail: React.FC = ({ alert(t("Failed to pass line. Please try again.")); } }, [fetchProcessDetail, t]); - + const handleCreateNewLine = useCallback(async (lineId: number) => { + try { + await newProductProcessLine(lineId); + // 刷新数据 + await fetchProcessDetail(); + } catch (error) { + console.error("Error creating new line:", error); + alert(t("Failed to create new line. Please try again.")); + } + }, [fetchProcessDetail, t]); // 提交产出数据 const processQrCode = useCallback((qrValue: string, lineId: number) => { // 设备快捷格式:{2fitesteXXX} - XXX 直接作为设备代码 @@ -257,7 +363,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { try { const lineDetail = lineDetailForScan || await fetchProductProcessLineDetail(lineId); - // ✅ 统一使用一个最终的 equipmentCode(优先用 scannedEquipmentCode,其次用 scannedEquipmentTypeSubTypeEquipmentNo) + // 统一使用一个最终的 equipmentCode(优先用 scannedEquipmentCode,其次用 scannedEquipmentTypeSubTypeEquipmentNo) const effectiveEquipmentCode = scannedEquipmentCode ?? null; @@ -340,7 +446,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { setProcessedQrCodes(new Set()); setScannedOperatorId(null); setScannedEquipmentId(null); - setScannedStaffNo(null); // ✅ Add this + setScannedStaffNo(null); // Add this setScannedEquipmentCode(null); setIsAutoSubmitting(false); // 添加:重置自动提交状态 setLineDetailForScan(null); @@ -366,7 +472,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { setIsManualScanning(false); setIsAutoSubmitting(false); - setScannedStaffNo(null); // ✅ Add this + setScannedStaffNo(null); // Add this setScannedEquipmentCode(null); stopScan(); resetScan(); @@ -392,7 +498,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { isManualScanning, }); - // ✅ Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId + // Update condition to check for either equipmentTypeSubTypeEquipmentNo OR equipmentDetailId if ( scanningLineId && scannedStaffNo !== null && @@ -455,6 +561,13 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { }; const selectedLine = lines.find(l => l.id === selectedLineId); + // 添加组件卸载日志 + useEffect(() => { + return () => { + console.log("🗑️ ProductionProcessDetail UNMOUNTING"); + }; + }, []); + if (loading) { return ( @@ -474,188 +587,230 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { {!isExecutingLine ? ( /* ========== 步骤列表视图 ========== */ - - - - {t("Seq")} - {t("Step Name")} - {t("Description")} - {t("EquipmentType-EquipmentName-Code")} - {t("Operator")} - {t("Assume End Time")} - - - - {t("Time Information(mins)")} - - - - {t("Status")} - - {!fromJosave&&({t("Action")})} - - - - {lines.map((line) => { - const status = (line as any).status || ''; - const statusLower = status.toLowerCase(); - const equipmentName = line.equipment_name || "-"; - - const isCompleted = statusLower === 'completed'; - const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; - const isPaused = statusLower === 'paused'; - const isPending = statusLower === 'pending' || status === ''; - const isPass = statusLower === 'pass'; - const isPassDisabled = isCompleted || isPass; - return ( - - {line.seqNo} - - {line.name} - - {line.description || "-"} - {line.equipmentDetailCode||equipmentName} - {line.operatorName} - - - {line.startTime && line.durationInMinutes - ? dayjs(line.startTime) - .add(line.durationInMinutes, 'minute') - .format('MM-DD HH:mm') - : '-'} - - - - - - {t("Processing Time")}: {line.durationInMinutes}{t("mins")} - - - {t("Setup Time")}: {line.prepTimeInMinutes} {t("mins")} - - - {t("Changeover Time")}: {line.postProdTimeInMinutes} {t("mins")} - - - - - {isCompleted ? ( - { - setSelectedLineId(line.id); - setShowOutputPage(false); // 不显示输出页面 - setIsExecutingLine(true); - await fetchProcessDetail(); - }} - /> - ) : isInProgress ? ( - { - setSelectedLineId(line.id); - setShowOutputPage(false); // 不显示输出页面 - setIsExecutingLine(true); - await fetchProcessDetail(); - }} /> - ) : isPending ? ( - - ) : isPaused ? ( - - ) : isPass ? ( - - ) : ( - - ) - } - - {!fromJosave&&( - - - {statusLower === 'pending' ? ( - <> - - - - ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? ( - <> - - - - ) : ( - <> - - - - )} - - - )} - - ); - })} - -
-
+ + + + {t(" ")} + {t("Seq")} + {t("Step Name")} + {t("Description")} + {t("EquipmentType-EquipmentName-Code")} + {t("Operator")} + {t("Assume End Time")} + + + + {t("Time Information(mins)")} + + + + {t("Status")} + + {!fromJosave&&({t("Action")})} + + + + {lines.map((line) => { + const status = (line as any).status || ''; + const statusLower = status.toLowerCase(); + const equipmentName = line.equipment_name || "-"; + + const isCompleted = statusLower === 'completed'; + const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress'; + const isPaused = statusLower === 'paused'; + const isPending = statusLower === 'pending' || status === ''; + const isPass = statusLower === 'pass'; + const isPassDisabled = isCompleted || isPass; + return ( + + + handleCreateNewLine(line.id)} + sx={{ + width: 32, + height: 32, + minHeight: 32, + boxShadow: 1, + '&:hover': { boxShadow: 3 }, + }} + > + + + + + + {line.seqNo} + + + + {line.name} + + + {line.description || "-"} + + + {line.equipmentDetailCode||equipmentName} + + + {line.operatorName} + + + + {line.startTime && line.durationInMinutes + ? dayjs(line.startTime) + .add(line.durationInMinutes, 'minute') + .format('MM-DD HH:mm') + : '-'} + + + + + + + {t("Processing Time")}: {line.durationInMinutes || 0}{t("mins")} + + {processData?.jobOrderStatus === "planning" && ( + { + console.log("🖱️ Edit button clicked for line:", line.id); + handleOpenTimeDialog(line.id); + }} + sx={{ padding: 0.5 }} + > + + + )} + + + {t("Setup Time")}: {line.prepTimeInMinutes || 0} {t("mins")} + + + {t("Changeover Time")}: {line.postProdTimeInMinutes || 0} {t("mins")} + + + + + {isCompleted ? ( + { + setSelectedLineId(line.id); + setShowOutputPage(false); + setIsExecutingLine(true); + await fetchProcessDetail(); + }} + /> + ) : isInProgress ? ( + { + setSelectedLineId(line.id); + setShowOutputPage(false); + setIsExecutingLine(true); + await fetchProcessDetail(); + }} /> + ) : isPending ? ( + + ) : isPaused ? ( + + ) : isPass ? ( + + ) : ( + + ) + } + + {!fromJosave&&( + + + {statusLower === 'pending' ? ( + <> + + + + ) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? ( + <> + + + + ) : ( + <> + + + + )} + + + )} + + ); + })} + +
+ ) : ( /* ========== 步骤执行视图 ========== */ )} @@ -703,13 +858,14 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => { - + + {t("Update Time Information")} + + + { + console.log("⌨️ Processing Time onChange:", { + value: e.target.value, + openTimeDialog, + editingLineId, + timestamp: new Date().toISOString() + }); + const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0; + setTimeValues(prev => ({ + ...prev, + durationInMinutes: Math.max(0, value) + })); + }} + inputProps={{ + min: 0, + step: 1 + }} + /> + { + console.log("⌨️ Setup Time onChange:", { + value: e.target.value, + openTimeDialog, + editingLineId, + timestamp: new Date().toISOString() + }); + const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0; + setTimeValues(prev => ({ + ...prev, + prepTimeInMinutes: Math.max(0, value) + })); + }} + inputProps={{ + min: 0, + step: 1 + }} + /> + { + console.log("⌨️ Changeover Time onChange:", { + value: e.target.value, + openTimeDialog, + editingLineId, + timestamp: new Date().toISOString() + }); + const value = e.target.value === '' ? 0 : parseInt(e.target.value) || 0; + setTimeValues(prev => ({ + ...prev, + postProdTimeInMinutes: Math.max(0, value) + })); + }} + inputProps={{ + min: 0, + step: 1 + }} + /> + + + + + + +
); }; diff --git a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx index 802dfa0..f7b2b5d 100644 --- a/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx +++ b/src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx @@ -23,8 +23,10 @@ import { } from "@mui/material"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import { useTranslation } from "react-i18next"; -import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart} from "@/app/api/jo/actions"; +import { fetchProductProcessesByJobOrderId ,deleteJobOrder, updateProductProcessPriority, updateJoPlanStart,updateJoReqQty,newProductProcessLine} from "@/app/api/jo/actions"; import ProductionProcessDetail from "./ProductionProcessDetail"; +import { BomCombo } from "@/app/api/bom"; +import { fetchBomCombo } from "@/app/api/bom/index"; import dayjs from "dayjs"; import { OUTPUT_DATE_FORMAT, integerFormatter, arrayToDateString } from "@/app/utils/formatUtil"; import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; @@ -79,7 +81,11 @@ const ProductionProcessJobOrderDetail: React.FC(null); - + const [openReqQtyDialog, setOpenReqQtyDialog] = useState(false); +const [reqQtyMultiplier, setReqQtyMultiplier] = useState(1); +const [selectedBomForReqQty, setSelectedBomForReqQty] = useState(null); +const [bomCombo, setBomCombo] = useState([]); + const fetchData = useCallback(async () => { setLoading(true); @@ -97,6 +103,61 @@ const ProductionProcessJobOrderDetail: React.FC { + if (!processData || !processData.outputQty || !processData.outputQtyUom) { + alert(t("BOM data not available")); + return; + } + + const baseOutputQty = processData.bomBaseQty; + const currentMultiplier = baseOutputQty > 0 + ? Math.round(processData.outputQty / baseOutputQty) + : 1; + const bomData = { + id: processData.bomId || 0, + value: processData.bomId || 0, + label: processData.bomDescription || "", + outputQty: baseOutputQty, + outputQtyUom: processData.outputQtyUom, + description: processData.bomDescription || "" + }; + + setSelectedBomForReqQty(bomData); + setReqQtyMultiplier(currentMultiplier); + setOpenReqQtyDialog(true); + }, [processData, t]); + + const handleCloseReqQtyDialog = useCallback(() => { + setOpenReqQtyDialog(false); + setSelectedBomForReqQty(null); + setReqQtyMultiplier(1); + }, []); + + const handleUpdateReqQty = useCallback(async (jobOrderId: number, newReqQty: number) => { + try { + const response = await updateJoReqQty({ + id: jobOrderId, + reqQty: Math.round(newReqQty) + }); + if (response) { + await fetchData(); + } + } catch (error) { + console.error("Error updating reqQty:", error); + alert(t("update failed")); + } + }, [fetchData, t]); + + const handleConfirmReqQty = useCallback(async () => { + if (!jobOrderId || !selectedBomForReqQty) return; + const newReqQty = reqQtyMultiplier * selectedBomForReqQty.outputQty; + await handleUpdateReqQty(jobOrderId, newReqQty); + setOpenReqQtyDialog(false); + setSelectedBomForReqQty(null); + setReqQtyMultiplier(1); + }, [jobOrderId, selectedBomForReqQty, reqQtyMultiplier, handleUpdateReqQty]); // 获取库存数据 useEffect(() => { const fetchInventoryData = async () => { @@ -302,6 +363,15 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { fullWidth disabled={true} value={processData?.outputQty + "(" + processData?.outputQtyUom + ")" || ""} + InputProps={{ + endAdornment: (processData?.jobOrderStatus === "planning" ? ( + + + + + + ) : null), + }} /> @@ -681,7 +751,88 @@ const handleRelease = useCallback(async ( jobOrderId: number) => { - + + {t("Update Required Quantity")} + + + + + + {selectedBomForReqQty.outputQtyUom} + + + ) : null + }} + sx={{ flex: 1 }} + /> + + × + + { + const val = e.target.value === "" ? 1 : Math.max(1, Math.floor(Number(e.target.value))); + setReqQtyMultiplier(val); + }} + inputProps={{ + min: 1, + step: 1 + }} + sx={{ flex: 1 }} + /> + + = + + + + {selectedBomForReqQty.outputQtyUom} + + + ) : null + }} + sx={{ flex: 1 }} + /> + + + + + + + + diff --git a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx index c65ce7d..fb9eca6 100644 --- a/src/components/ProductionProcess/ProductionProcessStepExecution.tsx +++ b/src/components/ProductionProcess/ProductionProcessStepExecution.tsx @@ -987,6 +987,7 @@ const ProductionProcessStepExecution: React.FC )} diff --git a/src/components/StockTakeManagement/ApproverCardList.tsx b/src/components/StockTakeManagement/ApproverCardList.tsx new file mode 100644 index 0000000..1baad2a --- /dev/null +++ b/src/components/StockTakeManagement/ApproverCardList.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { + Box, + Button, + Card, + CardContent, + CardActions, + Stack, + Typography, + Chip, + CircularProgress, + TablePagination, + Grid, + LinearProgress, +} from "@mui/material"; +import { useState, useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + getApproverStockTakeRecords, + AllPickedStockTakeListReponse, + +} from "@/app/api/stockTake/actions"; +import dayjs from "dayjs"; +import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; + +const PER_PAGE = 6; + +interface ApproverCardListProps { + onCardClick: (session: AllPickedStockTakeListReponse) => void; +} + +const ApproverCardList: React.FC = ({ onCardClick }) => { + const { t } = useTranslation(["inventory", "common"]); + + const [loading, setLoading] = useState(false); + const [stockTakeSessions, setStockTakeSessions] = useState([]); + const [page, setPage] = useState(0); + const [creating, setCreating] = useState(false); + + const fetchStockTakeSessions = useCallback(async () => { + setLoading(true); + try { + const data = await getApproverStockTakeRecords(); + setStockTakeSessions(Array.isArray(data) ? data : []); + setPage(0); + } catch (e) { + console.error(e); + setStockTakeSessions([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchStockTakeSessions(); + }, [fetchStockTakeSessions]); + + const startIdx = page * PER_PAGE; + const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); + + + const getStatusColor = (status: string | null) => { + if (!status) return "default"; + const statusLower = status.toLowerCase(); + if (statusLower === "completed") return "success"; + if (statusLower === "in_progress" || statusLower === "processing") return "primary"; + if (statusLower === "no_cycle") return "default"; + if (statusLower === "approving") return "info"; + return "warning"; + }; + + const getCompletionRate = (session: AllPickedStockTakeListReponse): number => { + if (session.totalInventoryLotNumber === 0) return 0; + return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100); + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + {t("Total Sections")}: {stockTakeSessions.length} + + + + + {paged.map((session) => { + const statusColor = getStatusColor(session.status); + const lastStockTakeDate = session.lastStockTakeDate + ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT) + : "-"; + const completionRate = getCompletionRate(session); + const isDisabled = session.status === null; + + return ( + + { + if (!isDisabled && session.status !== null) { + onCardClick(session); + } + }} + > + + + + {t("Section")}: {session.stockTakeSession} + + {session.status ? ( + + ) : ( + + )} + + + + {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} + + + {session.totalInventoryLotNumber > 0 && ( + + + + {t("Progress")} + + + {completionRate}% + + + + + )} + + + + + + + + ); + })} + + + {stockTakeSessions.length > 0 && ( + setPage(p)} + rowsPerPageOptions={[PER_PAGE]} + /> + )} + + ); +}; + +export default ApproverCardList; \ No newline at end of file diff --git a/src/components/StockTakeManagement/ApproverStockTake.tsx b/src/components/StockTakeManagement/ApproverStockTake.tsx new file mode 100644 index 0000000..4426965 --- /dev/null +++ b/src/components/StockTakeManagement/ApproverStockTake.tsx @@ -0,0 +1,467 @@ +"use client"; + +import { + Box, + Button, + Stack, + Typography, + Chip, + CircularProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TextField, + Radio, +} from "@mui/material"; +import { useState, useCallback, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { + AllPickedStockTakeListReponse, + getInventoryLotDetailsBySection, + InventoryLotDetailResponse, + saveApproverStockTakeRecord, + SaveApproverStockTakeRecordRequest, + BatchSaveApproverStockTakeRecordRequest, + batchSaveApproverStockTakeRecords, + updateStockTakeRecordStatusToNotMatch, +} from "@/app/api/stockTake/actions"; +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; +import dayjs from "dayjs"; +import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; + +interface ApproverStockTakeProps { + selectedSession: AllPickedStockTakeListReponse; + onBack: () => void; + onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; +} + +type QtySelectionType = "first" | "second" | "approver"; + +const ApproverStockTake: React.FC = ({ + selectedSession, + onBack, + onSnackbar, +}) => { + const { t } = useTranslation(["inventory", "common"]); + const { data: session } = useSession() as { data: SessionWithTokens | null }; + + const [inventoryLotDetails, setInventoryLotDetails] = useState([]); + const [loadingDetails, setLoadingDetails] = useState(false); + + // 每个记录的选择状态,key 为 detail.id + const [qtySelection, setQtySelection] = useState>({}); + const [approverQty, setApproverQty] = useState>({}); + const [approverBadQty, setApproverBadQty] = useState>({}); + const [saving, setSaving] = useState(false); + const [batchSaving, setBatchSaving] = useState(false); + const [updatingStatus, setUpdatingStatus] = useState(false); + const currentUserId = session?.id ? parseInt(session.id) : undefined; + const handleBatchSubmitAllRef = useRef<() => Promise>(); + + useEffect(() => { + const loadDetails = async () => { + setLoadingDetails(true); + try { + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + } finally { + setLoadingDetails(false); + } + }; + loadDetails(); + }, [selectedSession]); + + const handleSaveApproverStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { + if (!selectedSession || !currentUserId) { + return; + } + + const selection = qtySelection[detail.id] || "first"; + let finalQty: number; + let finalBadQty: number; + + if (selection === "first") { + if (!detail.firstStockTakeQty || detail.firstStockTakeQty === 0) { + onSnackbar(t("First QTY is not available"), "error"); + return; + } + finalQty = detail.firstStockTakeQty; + finalBadQty = detail.firstBadQty || 0; + } else if (selection === "second") { + if (!detail.secondStockTakeQty || detail.secondStockTakeQty === 0) { + onSnackbar(t("Second QTY is not available"), "error"); + return; + } + finalQty = detail.secondStockTakeQty; + finalBadQty = detail.secondBadQty || 0; + } else { + // Approver input + const approverQtyValue = approverQty[detail.id]; + const approverBadQtyValue = approverBadQty[detail.id]; + if (!approverQtyValue || !approverBadQtyValue) { + onSnackbar(t("Please enter Approver QTY and Bad QTY"), "error"); + return; + } + finalQty = parseFloat(approverQtyValue); + finalBadQty = parseFloat(approverBadQtyValue); + } + + setSaving(true); + try { + const request: SaveApproverStockTakeRecordRequest = { + stockTakeRecordId: detail.stockTakeRecordId || null, + qty: finalQty, + badQty: finalBadQty, + approverId: currentUserId, + approverQty: selection === "approver" ? finalQty : null, + approverBadQty: selection === "approver" ? finalBadQty : null, + }; + + await saveApproverStockTakeRecord( + request, + selectedSession.stockTakeId + ); + + onSnackbar(t("Approver stock take record saved successfully"), "success"); + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e: any) { + console.error("Save approver stock take record error:", e); + let errorMessage = t("Failed to save approver stock take record"); + + if (e?.message) { + errorMessage = e.message; + } else if (e?.response) { + try { + const errorData = await e.response.json(); + errorMessage = errorData.message || errorData.error || errorMessage; + } catch { + // ignore + } + } + + onSnackbar(errorMessage, "error"); + } finally { + setSaving(false); + } + }, [selectedSession, qtySelection, approverQty, approverBadQty, t, currentUserId, onSnackbar]); + const handleUpdateStatusToNotMatch = useCallback(async (detail: InventoryLotDetailResponse) => { + if (!detail.stockTakeRecordId) { + onSnackbar(t("Stock take record ID is required"), "error"); + return; + } + + setUpdatingStatus(true); + try { + await updateStockTakeRecordStatusToNotMatch(detail.stockTakeRecordId); + + onSnackbar(t("Stock take record status updated to not match"), "success"); + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e: any) { + console.error("Update stock take record status error:", e); + let errorMessage = t("Failed to update stock take record status"); + + if (e?.message) { + errorMessage = e.message; + } else if (e?.response) { + try { + const errorData = await e.response.json(); + errorMessage = errorData.message || errorData.error || errorMessage; + } catch { + // ignore + } + } + + onSnackbar(errorMessage, "error"); + } finally { + setUpdatingStatus(false); + } + }, [selectedSession, t, onSnackbar]); + const handleBatchSubmitAll = useCallback(async () => { + if (!selectedSession || !currentUserId) { + console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); + return; + } + + console.log('handleBatchSubmitAll: Starting batch approver save...'); + setBatchSaving(true); + try { + const request: BatchSaveApproverStockTakeRecordRequest = { + stockTakeId: selectedSession.stockTakeId, + stockTakeSection: selectedSession.stockTakeSession, + approverId: currentUserId, + }; + + const result = await batchSaveApproverStockTakeRecords(request); + console.log('handleBatchSubmitAll: Result:', result); + + onSnackbar( + t("Batch approver save completed: {{success}} success, {{errors}} errors", { + success: result.successCount, + errors: result.errorCount, + }), + result.errorCount > 0 ? "warning" : "success" + ); + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e: any) { + console.error("handleBatchSubmitAll: Error:", e); + let errorMessage = t("Failed to batch save approver stock take records"); + + if (e?.message) { + errorMessage = e.message; + } else if (e?.response) { + try { + const errorData = await e.response.json(); + errorMessage = errorData.message || errorData.error || errorMessage; + } catch { + // ignore + } + } + + onSnackbar(errorMessage, "error"); + } finally { + setBatchSaving(false); + } + }, [selectedSession, t, currentUserId, onSnackbar]); + + useEffect(() => { + handleBatchSubmitAllRef.current = handleBatchSubmitAll; + }, [handleBatchSubmitAll]); + + const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { + // Only allow editing if there's a first stock take qty + if (!detail.firstStockTakeQty || detail.firstStockTakeQty === 0) { + return true; + } + return false; + }, []); + + return ( + + + + {t("Stock Take Section")}: {selectedSession.stockTakeSession} + + {loadingDetails ? ( + + + + ) : ( + + + + + {t("Warehouse Location")} + {t("Item")} + {t("Stock Take Qty")} + {t("Remark")} + {t("UOM")} + {t("Record Status")} + {t("Action")} + + + + {inventoryLotDetails.length === 0 ? ( + + + + {t("No data")} + + + + ) : ( + inventoryLotDetails.map((detail) => { + const submitDisabled = isSubmitDisabled(detail); + const hasFirst = detail.firstStockTakeQty != null && detail.firstStockTakeQty > 0; + const hasSecond = detail.secondStockTakeQty != null && detail.secondStockTakeQty > 0; + const selection = qtySelection[detail.id] || "first"; + + return ( + + {detail.warehouseArea || "-"}{detail.warehouseSlot || "-"} + + + {detail.itemCode || "-"} {detail.itemName || "-"} + {detail.lotNo || "-"} + {detail.expiryDate ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) : "-"} + + + + + + {detail.finalQty != null ? ( + // 提交后只显示差异行 + + + {t("Difference")}: {detail.finalQty?.toFixed(2) || "0.00"} - {(detail.availableQty || 0).toFixed(2)} = {((detail.finalQty || 0) - (detail.availableQty || 0)).toFixed(2)} + + + ) : ( + + {/* 第一行:First Qty(默认选中) */} + {hasFirst && ( + + setQtySelection({ ...qtySelection, [detail.id]: "first" })} + /> + + {t("First")}: {(detail.firstStockTakeQty??0)+(detail.firstBadQty??0) || "0.00"} ({detail.firstBadQty??0}) + + + )} + + {/* 第二行:Second Qty(如果存在) */} + {hasSecond && ( + + setQtySelection({ ...qtySelection, [detail.id]: "second" })} + /> + + {t("Second")}: {(detail.secondStockTakeQty??0)+(detail.secondBadQty??0) || "0.00"} ({detail.secondBadQty??0}) + + + )} + + {/* 第三行:Approver Input(仅在 second qty 存在时显示) */} + {hasSecond && ( + + setQtySelection({ ...qtySelection, [detail.id]: "approver" })} + /> + {t("Approver Input")}: + setApproverQty({ ...approverQty, [detail.id]: e.target.value })} + sx={{ width: 100 }} + disabled={selection !== "approver"} + /> + - + setApproverBadQty({ ...approverBadQty, [detail.id]: e.target.value })} + sx={{ width: 100 }} + disabled={selection !== "approver"} + /> + + )} + + {/* 差异行:显示 selected qty - bookqty = result */} + {(() => { + let selectedQty = 0; + + if (selection === "first") { + selectedQty = detail.firstStockTakeQty || 0; + } else if (selection === "second") { + selectedQty = detail.secondStockTakeQty || 0; + } else if (selection === "approver") { + selectedQty = parseFloat(approverQty[detail.id] || "0") || 0; + } + + const bookQty = detail.availableQty || 0; + const difference = selectedQty - bookQty; + + return ( + + {t("Difference")}: {selectedQty.toFixed(2)} - {bookQty.toFixed(2)} = {difference.toFixed(2)} + + ); + })()} + + )} + + + + + {detail.remarks || "-"} + + + + {detail.uom || "-"} + + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + + )} + + + {detail.stockTakeRecordId && detail.stockTakeRecordStatus !== "notMatch" && ( + + )} + {detail.finalQty == null && ( + + )} + + + ); + }) + )} + +
+
+ )} +
+ ); +}; + +export default ApproverStockTake; \ No newline at end of file diff --git a/src/components/StockTakeManagement/PickerCardList.tsx b/src/components/StockTakeManagement/PickerCardList.tsx new file mode 100644 index 0000000..9b10871 --- /dev/null +++ b/src/components/StockTakeManagement/PickerCardList.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { + Box, + Button, + Card, + CardContent, + CardActions, + Stack, + Typography, + Chip, + CircularProgress, + TablePagination, + Grid, + LinearProgress, +} from "@mui/material"; +import { useState, useCallback, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { + getStockTakeRecords, + AllPickedStockTakeListReponse, + createStockTakeForSections, +} from "@/app/api/stockTake/actions"; +import dayjs from "dayjs"; +import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; + +const PER_PAGE = 6; + +interface PickerCardListProps { + onCardClick: (session: AllPickedStockTakeListReponse) => void; + onReStockTakeClick: (session: AllPickedStockTakeListReponse) => void; +} + +const PickerCardList: React.FC = ({ onCardClick, onReStockTakeClick }) => { + const { t } = useTranslation(["inventory", "common"]); + + const [loading, setLoading] = useState(false); + const [stockTakeSessions, setStockTakeSessions] = useState([]); + const [page, setPage] = useState(0); + const [creating, setCreating] = useState(false); + + const fetchStockTakeSessions = useCallback(async () => { + setLoading(true); + try { + const data = await getStockTakeRecords(); + setStockTakeSessions(Array.isArray(data) ? data : []); + setPage(0); + } catch (e) { + console.error(e); + setStockTakeSessions([]); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchStockTakeSessions(); + }, [fetchStockTakeSessions]); + + const startIdx = page * PER_PAGE; + const paged = stockTakeSessions.slice(startIdx, startIdx + PER_PAGE); + + const handleCreateStockTake = useCallback(async () => { + setCreating(true); + try { + const result = await createStockTakeForSections(); + const createdCount = Object.values(result).filter(msg => msg.startsWith("Created:")).length; + const skippedCount = Object.values(result).filter(msg => msg.startsWith("Skipped:")).length; + const errorCount = Object.values(result).filter(msg => msg.startsWith("Error:")).length; + + let message = `${t("Created")}: ${createdCount}, ${t("Skipped")}: ${skippedCount}`; + if (errorCount > 0) { + message += `, ${t("Errors")}: ${errorCount}`; + } + + console.log(message); + + await fetchStockTakeSessions(); + } catch (e) { + console.error(e); + } finally { + setCreating(false); + } + }, [fetchStockTakeSessions, t]); + + const getStatusColor = (status: string) => { + const statusLower = status.toLowerCase(); + if (statusLower === "completed") return "success"; + if (statusLower === "in_progress" || statusLower === "processing") return "primary"; + if (statusLower === "approving") return "info"; + if (statusLower === "no_cycle") return "default"; + return "warning"; + }; + + const getCompletionRate = (session: AllPickedStockTakeListReponse): number => { + if (session.totalInventoryLotNumber === 0) return 0; + return Math.round((session.currentStockTakeItemNumber / session.totalInventoryLotNumber) * 100); + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + {t("Total Sections")}: {stockTakeSessions.length} + + + + + + {paged.map((session) => { + const statusColor = getStatusColor(session.status || ""); + const lastStockTakeDate = session.lastStockTakeDate + ? dayjs(session.lastStockTakeDate).format(OUTPUT_DATE_FORMAT) + : "-"; + const completionRate = getCompletionRate(session); + + return ( + + + + + + {t("Section")}: {session.stockTakeSession} + + + + + + {t("Last Stock Take Date")}: {lastStockTakeDate || "-"} + + {t("Stock Taker")}: {session.stockTakerName} + {t("Total Item Number")}: {session.totalItemNumber} + {session.totalInventoryLotNumber > 0 && ( + + + + {t("Progress")} + + + {completionRate}% + + + + + )} + + + + + + + + + ); + })} + + + {stockTakeSessions.length > 0 && ( + setPage(p)} + rowsPerPageOptions={[PER_PAGE]} + /> + )} + + ); +}; + +export default PickerCardList; \ No newline at end of file diff --git a/src/components/StockTakeManagement/PickerReStockTake.tsx b/src/components/StockTakeManagement/PickerReStockTake.tsx new file mode 100644 index 0000000..fee9a6b --- /dev/null +++ b/src/components/StockTakeManagement/PickerReStockTake.tsx @@ -0,0 +1,543 @@ +"use client"; + +import { + Box, + Button, + Stack, + Typography, + Chip, + CircularProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TextField, +} from "@mui/material"; +import { useState, useCallback, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { + AllPickedStockTakeListReponse, + getInventoryLotDetailsBySection, + InventoryLotDetailResponse, + saveStockTakeRecord, + SaveStockTakeRecordRequest, + BatchSaveStockTakeRecordRequest, + batchSaveStockTakeRecords, + getInventoryLotDetailsBySectionNotMatch +} from "@/app/api/stockTake/actions"; +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; +import dayjs from "dayjs"; +import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; + +interface PickerStockTakeProps { + selectedSession: AllPickedStockTakeListReponse; + onBack: () => void; + onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; +} + +const PickerStockTake: React.FC = ({ + selectedSession, + onBack, + onSnackbar, +}) => { + const { t } = useTranslation(["inventory", "common"]); + const { data: session } = useSession() as { data: SessionWithTokens | null }; + + const [inventoryLotDetails, setInventoryLotDetails] = useState([]); + const [loadingDetails, setLoadingDetails] = useState(false); + + // 编辑状态 + const [editingRecord, setEditingRecord] = useState(null); + const [firstQty, setFirstQty] = useState(""); + const [secondQty, setSecondQty] = useState(""); + const [firstBadQty, setFirstBadQty] = useState(""); + const [secondBadQty, setSecondBadQty] = useState(""); + const [remark, setRemark] = useState(""); + const [saving, setSaving] = useState(false); + const [batchSaving, setBatchSaving] = useState(false); + const [shortcutInput, setShortcutInput] = useState(""); + + const currentUserId = session?.id ? parseInt(session.id) : undefined; + const handleBatchSubmitAllRef = useRef<() => Promise>(); + + useEffect(() => { + const loadDetails = async () => { + setLoadingDetails(true); + try { + const details = await getInventoryLotDetailsBySectionNotMatch( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + } finally { + setLoadingDetails(false); + } + }; + loadDetails(); + }, [selectedSession]); + + const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { + setEditingRecord(detail); + setFirstQty(detail.firstStockTakeQty?.toString() || ""); + setSecondQty(detail.secondStockTakeQty?.toString() || ""); + setFirstBadQty(detail.firstBadQty?.toString() || ""); + setSecondBadQty(detail.secondBadQty?.toString() || ""); + setRemark(detail.remarks || ""); + }, []); + + const handleCancelEdit = useCallback(() => { + setEditingRecord(null); + setFirstQty(""); + setSecondQty(""); + setFirstBadQty(""); + setSecondBadQty(""); + setRemark(""); + }, []); + + const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { + if (!selectedSession || !currentUserId) { + return; + } + + const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; + const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; + + const qty = isFirstSubmit ? firstQty : secondQty; + const badQty = isFirstSubmit ? firstBadQty : secondBadQty; + + if (!qty || !badQty) { + onSnackbar( + isFirstSubmit + ? t("Please enter QTY and Bad QTY") + : t("Please enter Second QTY and Bad QTY"), + "error" + ); + return; + } + + setSaving(true); + try { + const request: SaveStockTakeRecordRequest = { + stockTakeRecordId: detail.stockTakeRecordId || null, + inventoryLotLineId: detail.id, + qty: parseFloat(qty), + badQty: parseFloat(badQty), + remark: isSecondSubmit ? (remark || null) : null, + }; + console.log('handleSaveStockTake: request:', request); + console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); + console.log('handleSaveStockTake: currentUserId:', currentUserId); + await saveStockTakeRecord( + request, + selectedSession.stockTakeId, + currentUserId + ); + + onSnackbar(t("Stock take record saved successfully"), "success"); + handleCancelEdit(); + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e: any) { + console.error("Save stock take record error:", e); + let errorMessage = t("Failed to save stock take record"); + + if (e?.message) { + errorMessage = e.message; + } else if (e?.response) { + try { + const errorData = await e.response.json(); + errorMessage = errorData.message || errorData.error || errorMessage; + } catch { + // ignore + } + } + + onSnackbar(errorMessage, "error"); + } finally { + setSaving(false); + } + }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar]); + + const handleBatchSubmitAll = useCallback(async () => { + if (!selectedSession || !currentUserId) { + console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); + return; + } + + console.log('handleBatchSubmitAll: Starting batch save...'); + setBatchSaving(true); + try { + const request: BatchSaveStockTakeRecordRequest = { + stockTakeId: selectedSession.stockTakeId, + stockTakeSection: selectedSession.stockTakeSession, + stockTakerId: currentUserId, + }; + + const result = await batchSaveStockTakeRecords(request); + console.log('handleBatchSubmitAll: Result:', result); + + onSnackbar( + t("Batch save completed: {{success}} success, {{errors}} errors", { + success: result.successCount, + errors: result.errorCount, + }), + result.errorCount > 0 ? "warning" : "success" + ); + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e: any) { + console.error("handleBatchSubmitAll: Error:", e); + let errorMessage = t("Failed to batch save stock take records"); + + if (e?.message) { + errorMessage = e.message; + } else if (e?.response) { + try { + const errorData = await e.response.json(); + errorMessage = errorData.message || errorData.error || errorMessage; + } catch { + // ignore + } + } + + onSnackbar(errorMessage, "error"); + } finally { + setBatchSaving(false); + } + }, [selectedSession, t, currentUserId, onSnackbar]); + + useEffect(() => { + handleBatchSubmitAllRef.current = handleBatchSubmitAll; + }, [handleBatchSubmitAll]); + + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + const target = e.target as HTMLElement; + if (target && ( + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.isContentEditable + )) { + return; + } + + if (e.ctrlKey || e.metaKey || e.altKey) { + return; + } + + if (e.key.length === 1) { + setShortcutInput(prev => { + const newInput = prev + e.key; + + if (newInput === '{2fitestall}') { + console.log('✅ Shortcut {2fitestall} detected!'); + setTimeout(() => { + if (handleBatchSubmitAllRef.current) { + console.log('Calling handleBatchSubmitAll...'); + handleBatchSubmitAllRef.current().catch(err => { + console.error('Error in handleBatchSubmitAll:', err); + }); + } else { + console.error('handleBatchSubmitAllRef.current is null'); + } + }, 0); + return ""; + } + + if (newInput.length > 15) { + return ""; + } + + if (newInput.length > 0 && !newInput.startsWith('{')) { + return ""; + } + + if (newInput.length > 5 && !newInput.startsWith('{2fi')) { + return ""; + } + + return newInput; + }); + } else if (e.key === 'Backspace') { + setShortcutInput(prev => prev.slice(0, -1)); + } else if (e.key === 'Escape') { + setShortcutInput(""); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => { + window.removeEventListener('keydown', handleKeyPress); + }; + }, []); + + const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { + if (detail.stockTakeRecordStatus === "pass") { + return true; + } + return false; + }, []); + + return ( + + + + {t("Stock Take Section")}: {selectedSession.stockTakeSession} + + {/* + {shortcutInput && ( + + + {t("Shortcut Input")}: {shortcutInput} + + + )} + */} + {loadingDetails ? ( + + + + ) : ( + + + + + {t("Warehouse Location")} + {t("Item")} + {/*{t("Item Name")}*/} + {/*{t("Lot No")}*/} + {t("Expiry Date")} + {t("Qty")} + {t("Bad Qty")} + {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/} + {t("Remark")} + + {t("UOM")} + {t("Status")} + {t("Record Status")} + {t("Action")} + + + + {inventoryLotDetails.length === 0 ? ( + + + + {t("No data")} + + + + ) : ( + inventoryLotDetails.map((detail) => { + const isEditing = editingRecord?.id === detail.id; + const submitDisabled = isSubmitDisabled(detail); + const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; + const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; + + return ( + + {detail.warehouseCode || "-"} + {detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""} + {/* + + {detail.itemName || "-"} + */} + {/*{detail.lotNo || "-"}*/} + + {detail.expiryDate + ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) + : "-"} + + + + + {isEditing && isFirstSubmit ? ( + setFirstQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.firstStockTakeQty ? ( + + {t("First")}: {detail.firstStockTakeQty.toFixed(2)} + + ) : null} + + {isEditing && isSecondSubmit ? ( + setSecondQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.secondStockTakeQty ? ( + + {t("Second")}: {detail.secondStockTakeQty.toFixed(2)} + + ) : null} + + {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && ( + + - + + )} + + + + + {isEditing && isFirstSubmit ? ( + setFirstBadQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.firstBadQty ? ( + + {t("First")}: {detail.firstBadQty.toFixed(2)} + + ) : null} + + {isEditing && isSecondSubmit ? ( + setSecondBadQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.secondBadQty ? ( + + {t("Second")}: {detail.secondBadQty.toFixed(2)} + + ) : null} + + {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( + + - + + )} + + + + + {isEditing && isSecondSubmit ? ( + <> + {t("Remark")} + setRemark(e.target.value)} + sx={{ width: 150 }} + // If you want a single-line input, remove multiline/rows: + // multiline + // rows={2} + /> + + ) : ( + + {detail.remarks || "-"} + + )} + + {detail.uom || "-"} + + {detail.status ? ( + + ) : ( + "-" + )} + + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + + )} + + + {isEditing ? ( + + + + + + ) : ( + + )} + + + ); + }) + )} + +
+
+ )} +
+ ); +}; + +export default PickerStockTake; \ No newline at end of file diff --git a/src/components/StockTakeManagement/PickerStockTake.tsx b/src/components/StockTakeManagement/PickerStockTake.tsx new file mode 100644 index 0000000..d480718 --- /dev/null +++ b/src/components/StockTakeManagement/PickerStockTake.tsx @@ -0,0 +1,542 @@ +"use client"; + +import { + Box, + Button, + Stack, + Typography, + Chip, + CircularProgress, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, + TextField, +} from "@mui/material"; +import { useState, useCallback, useEffect, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { + AllPickedStockTakeListReponse, + getInventoryLotDetailsBySection, + InventoryLotDetailResponse, + saveStockTakeRecord, + SaveStockTakeRecordRequest, + BatchSaveStockTakeRecordRequest, + batchSaveStockTakeRecords, +} from "@/app/api/stockTake/actions"; +import { useSession } from "next-auth/react"; +import { SessionWithTokens } from "@/config/authConfig"; +import dayjs from "dayjs"; +import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; + +interface PickerStockTakeProps { + selectedSession: AllPickedStockTakeListReponse; + onBack: () => void; + onSnackbar: (message: string, severity: "success" | "error" | "warning") => void; +} + +const PickerStockTake: React.FC = ({ + selectedSession, + onBack, + onSnackbar, +}) => { + const { t } = useTranslation(["inventory", "common"]); + const { data: session } = useSession() as { data: SessionWithTokens | null }; + + const [inventoryLotDetails, setInventoryLotDetails] = useState([]); + const [loadingDetails, setLoadingDetails] = useState(false); + + // 编辑状态 + const [editingRecord, setEditingRecord] = useState(null); + const [firstQty, setFirstQty] = useState(""); + const [secondQty, setSecondQty] = useState(""); + const [firstBadQty, setFirstBadQty] = useState(""); + const [secondBadQty, setSecondBadQty] = useState(""); + const [remark, setRemark] = useState(""); + const [saving, setSaving] = useState(false); + const [batchSaving, setBatchSaving] = useState(false); + const [shortcutInput, setShortcutInput] = useState(""); + + const currentUserId = session?.id ? parseInt(session.id) : undefined; + const handleBatchSubmitAllRef = useRef<() => Promise>(); + + useEffect(() => { + const loadDetails = async () => { + setLoadingDetails(true); + try { + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e) { + console.error(e); + setInventoryLotDetails([]); + } finally { + setLoadingDetails(false); + } + }; + loadDetails(); + }, [selectedSession]); + + const handleStartEdit = useCallback((detail: InventoryLotDetailResponse) => { + setEditingRecord(detail); + setFirstQty(detail.firstStockTakeQty?.toString() || ""); + setSecondQty(detail.secondStockTakeQty?.toString() || ""); + setFirstBadQty(detail.firstBadQty?.toString() || ""); + setSecondBadQty(detail.secondBadQty?.toString() || ""); + setRemark(detail.remarks || ""); + }, []); + + const handleCancelEdit = useCallback(() => { + setEditingRecord(null); + setFirstQty(""); + setSecondQty(""); + setFirstBadQty(""); + setSecondBadQty(""); + setRemark(""); + }, []); + + const handleSaveStockTake = useCallback(async (detail: InventoryLotDetailResponse) => { + if (!selectedSession || !currentUserId) { + return; + } + + const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; + const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; + + const qty = isFirstSubmit ? firstQty : secondQty; + const badQty = isFirstSubmit ? firstBadQty : secondBadQty; + + if (!qty || !badQty) { + onSnackbar( + isFirstSubmit + ? t("Please enter QTY and Bad QTY") + : t("Please enter Second QTY and Bad QTY"), + "error" + ); + return; + } + + setSaving(true); + try { + const request: SaveStockTakeRecordRequest = { + stockTakeRecordId: detail.stockTakeRecordId || null, + inventoryLotLineId: detail.id, + qty: parseFloat(qty), + badQty: parseFloat(badQty), + remark: isSecondSubmit ? (remark || null) : null, + }; + console.log('handleSaveStockTake: request:', request); + console.log('handleSaveStockTake: selectedSession.stockTakeId:', selectedSession.stockTakeId); + console.log('handleSaveStockTake: currentUserId:', currentUserId); + await saveStockTakeRecord( + request, + selectedSession.stockTakeId, + currentUserId + ); + + onSnackbar(t("Stock take record saved successfully"), "success"); + handleCancelEdit(); + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e: any) { + console.error("Save stock take record error:", e); + let errorMessage = t("Failed to save stock take record"); + + if (e?.message) { + errorMessage = e.message; + } else if (e?.response) { + try { + const errorData = await e.response.json(); + errorMessage = errorData.message || errorData.error || errorMessage; + } catch { + // ignore + } + } + + onSnackbar(errorMessage, "error"); + } finally { + setSaving(false); + } + }, [selectedSession, firstQty, secondQty, firstBadQty, secondBadQty, remark, handleCancelEdit, t, currentUserId, onSnackbar]); + + const handleBatchSubmitAll = useCallback(async () => { + if (!selectedSession || !currentUserId) { + console.log('handleBatchSubmitAll: Missing selectedSession or currentUserId'); + return; + } + + console.log('handleBatchSubmitAll: Starting batch save...'); + setBatchSaving(true); + try { + const request: BatchSaveStockTakeRecordRequest = { + stockTakeId: selectedSession.stockTakeId, + stockTakeSection: selectedSession.stockTakeSession, + stockTakerId: currentUserId, + }; + + const result = await batchSaveStockTakeRecords(request); + console.log('handleBatchSubmitAll: Result:', result); + + onSnackbar( + t("Batch save completed: {{success}} success, {{errors}} errors", { + success: result.successCount, + errors: result.errorCount, + }), + result.errorCount > 0 ? "warning" : "success" + ); + + const details = await getInventoryLotDetailsBySection( + selectedSession.stockTakeSession, + selectedSession.stockTakeId > 0 ? selectedSession.stockTakeId : null + ); + setInventoryLotDetails(Array.isArray(details) ? details : []); + } catch (e: any) { + console.error("handleBatchSubmitAll: Error:", e); + let errorMessage = t("Failed to batch save stock take records"); + + if (e?.message) { + errorMessage = e.message; + } else if (e?.response) { + try { + const errorData = await e.response.json(); + errorMessage = errorData.message || errorData.error || errorMessage; + } catch { + // ignore + } + } + + onSnackbar(errorMessage, "error"); + } finally { + setBatchSaving(false); + } + }, [selectedSession, t, currentUserId, onSnackbar]); + + useEffect(() => { + handleBatchSubmitAllRef.current = handleBatchSubmitAll; + }, [handleBatchSubmitAll]); + + useEffect(() => { + const handleKeyPress = (e: KeyboardEvent) => { + const target = e.target as HTMLElement; + if (target && ( + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.isContentEditable + )) { + return; + } + + if (e.ctrlKey || e.metaKey || e.altKey) { + return; + } + + if (e.key.length === 1) { + setShortcutInput(prev => { + const newInput = prev + e.key; + + if (newInput === '{2fitestall}') { + console.log('✅ Shortcut {2fitestall} detected!'); + setTimeout(() => { + if (handleBatchSubmitAllRef.current) { + console.log('Calling handleBatchSubmitAll...'); + handleBatchSubmitAllRef.current().catch(err => { + console.error('Error in handleBatchSubmitAll:', err); + }); + } else { + console.error('handleBatchSubmitAllRef.current is null'); + } + }, 0); + return ""; + } + + if (newInput.length > 15) { + return ""; + } + + if (newInput.length > 0 && !newInput.startsWith('{')) { + return ""; + } + + if (newInput.length > 5 && !newInput.startsWith('{2fi')) { + return ""; + } + + return newInput; + }); + } else if (e.key === 'Backspace') { + setShortcutInput(prev => prev.slice(0, -1)); + } else if (e.key === 'Escape') { + setShortcutInput(""); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => { + window.removeEventListener('keydown', handleKeyPress); + }; + }, []); + + const isSubmitDisabled = useCallback((detail: InventoryLotDetailResponse): boolean => { + if (detail.stockTakeRecordStatus === "pass") { + return true; + } + return false; + }, []); + + return ( + + + + {t("Stock Take Section")}: {selectedSession.stockTakeSession} + + {/* + {shortcutInput && ( + + + {t("Shortcut Input")}: {shortcutInput} + + + )} + */} + {loadingDetails ? ( + + + + ) : ( + + + + + {t("Warehouse Location")} + {t("Item")} + {/*{t("Item Name")}*/} + {/*{t("Lot No")}*/} + {t("Expiry Date")} + {t("Qty")} + {t("Bad Qty")} + {/*{inventoryLotDetails.some(d => editingRecord?.id === d.id) && (*/} + {t("Remark")} + + {t("UOM")} + {t("Status")} + {t("Record Status")} + {t("Action")} + + + + {inventoryLotDetails.length === 0 ? ( + + + + {t("No data")} + + + + ) : ( + inventoryLotDetails.map((detail) => { + const isEditing = editingRecord?.id === detail.id; + const submitDisabled = isSubmitDisabled(detail); + const isFirstSubmit = !detail.stockTakeRecordId || !detail.firstStockTakeQty; + const isSecondSubmit = detail.stockTakeRecordId && detail.firstStockTakeQty && !detail.secondStockTakeQty; + + return ( + + {detail.warehouseCode || "-"} + {detail.itemCode || "-"}{detail.lotNo || "-"}{detail.itemName ? ` - ${detail.itemName}` : ""} + {/* + + {detail.itemName || "-"} + */} + {/*{detail.lotNo || "-"}*/} + + {detail.expiryDate + ? dayjs(detail.expiryDate).format(OUTPUT_DATE_FORMAT) + : "-"} + + + + + {isEditing && isFirstSubmit ? ( + setFirstQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.firstStockTakeQty ? ( + + {t("First")}: {detail.firstStockTakeQty.toFixed(2)} + + ) : null} + + {isEditing && isSecondSubmit ? ( + setSecondQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.secondStockTakeQty ? ( + + {t("Second")}: {detail.secondStockTakeQty.toFixed(2)} + + ) : null} + + {!detail.firstStockTakeQty && !detail.secondStockTakeQty && !isEditing && ( + + - + + )} + + + + + {isEditing && isFirstSubmit ? ( + setFirstBadQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.firstBadQty ? ( + + {t("First")}: {detail.firstBadQty.toFixed(2)} + + ) : null} + + {isEditing && isSecondSubmit ? ( + setSecondBadQty(e.target.value)} + sx={{ width: 100 }} + + /> + ) : detail.secondBadQty ? ( + + {t("Second")}: {detail.secondBadQty.toFixed(2)} + + ) : null} + + {!detail.firstBadQty && !detail.secondBadQty && !isEditing && ( + + - + + )} + + + + + {isEditing && isSecondSubmit ? ( + <> + {t("Remark")} + setRemark(e.target.value)} + sx={{ width: 150 }} + // If you want a single-line input, remove multiline/rows: + // multiline + // rows={2} + /> + + ) : ( + + {detail.remarks || "-"} + + )} + + {detail.uom || "-"} + + {detail.status ? ( + + ) : ( + "-" + )} + + + {detail.stockTakeRecordStatus === "pass" ? ( + + ) : detail.stockTakeRecordStatus === "notMatch" ? ( + + ) : ( + + )} + + + {isEditing ? ( + + + + + + ) : ( + + )} + + + ); + }) + )} + +
+
+ )} +
+ ); +}; + +export default PickerStockTake; \ No newline at end of file diff --git a/src/components/StockTakeManagement/StockTakeManagement.tsx b/src/components/StockTakeManagement/StockTakeManagement.tsx index 20f59e9..9f9a543 100644 --- a/src/components/StockTakeManagement/StockTakeManagement.tsx +++ b/src/components/StockTakeManagement/StockTakeManagement.tsx @@ -40,7 +40,7 @@ const StockTakeManagement: React.FC = () => { return ( - {t("Inventory Exception Management")} + {t("Stock Take Management")} diff --git a/src/components/StockTakeManagement/StockTakeTab.tsx b/src/components/StockTakeManagement/StockTakeTab.tsx index 55ec814..d61aa23 100644 --- a/src/components/StockTakeManagement/StockTakeTab.tsx +++ b/src/components/StockTakeManagement/StockTakeTab.tsx @@ -1,430 +1,115 @@ "use client"; -import { - Box, - Button, - Card, - CardContent, - Stack, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TextField, - Typography, - Paper, - Alert, - Dialog, - DialogTitle, - DialogContent, - DialogActions, -} from "@mui/material"; -import { useState, useMemo, useCallback } from "react"; +import { Box, Tab, Tabs, Snackbar, Alert } from "@mui/material"; +import { useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; -import SearchBox, { Criterion } from "../SearchBox"; -import SearchResults, { Column } from "../SearchResults"; -import { defaultPagingController } from "../SearchResults/SearchResults"; - -// Fake data types -interface Floor { - id: number; - code: string; - name: string; - warehouseCode: string; - warehouseName: string; -} - -interface Zone { - id: number; - floorId: number; - code: string; - name: string; - description: string; -} - -interface InventoryLotLineForStockTake { - id: number; - zoneId: number; - itemCode: string; - itemName: string; - lotNo: string; - location: string; - systemQty: number; - countedQty?: number; - variance?: number; - uom: string; -} - -// Fake data -const fakeFloors: Floor[] = [ - { id: 1, code: "F1", name: "1st Floor", warehouseCode: "WH001", warehouseName: "Main Warehouse" }, - { id: 2, code: "F2", name: "2nd Floor", warehouseCode: "WH001", warehouseName: "Main Warehouse" }, - { id: 3, code: "F3", name: "3rd Floor", warehouseCode: "WH001", warehouseName: "Main Warehouse" }, -]; - -const fakeZones: Zone[] = [ - { id: 1, floorId: 1, code: "Z-A", name: "Zone A", description: "Row 1-5" }, - { id: 2, floorId: 1, code: "Z-B", name: "Zone B", description: "Row 6-10" }, - { id: 3, floorId: 2, code: "Z-C", name: "Zone C", description: "Row 1-5" }, - { id: 4, floorId: 2, code: "Z-D", name: "Zone D", description: "Row 6-10" }, - { id: 5, floorId: 3, code: "Z-E", name: "Zone E", description: "Row 1-5" }, -]; - -const fakeLots: InventoryLotLineForStockTake[] = [ - { id: 1, zoneId: 1, itemCode: "M001", itemName: "Material A", lotNo: "LOT-2024-001", location: "A-01-01", systemQty: 100, uom: "PCS" }, - { id: 2, zoneId: 1, itemCode: "M002", itemName: "Material B", lotNo: "LOT-2024-002", location: "A-01-02", systemQty: 50, uom: "PCS" }, - { id: 3, zoneId: 1, itemCode: "M003", itemName: "Material C", lotNo: "LOT-2024-003", location: "A-01-03", systemQty: 75, uom: "KG" }, - { id: 4, zoneId: 2, itemCode: "M004", itemName: "Material D", lotNo: "LOT-2024-004", location: "B-01-01", systemQty: 200, uom: "PCS" }, - { id: 5, zoneId: 2, itemCode: "M005", itemName: "Material E", lotNo: "LOT-2024-005", location: "B-01-02", systemQty: 150, uom: "KG" }, -]; - -type FloorSearchQuery = { - floorCode: string; - floorName: string; - warehouseCode: string; -}; - -type FloorSearchParamNames = keyof FloorSearchQuery; +import { AllPickedStockTakeListReponse } from "@/app/api/stockTake/actions"; +import PickerCardList from "./PickerCardList"; +import ApproverCardList from "./ApproverCardList"; +import PickerStockTake from "./PickerStockTake"; +import PickerReStockTake from "./PickerReStockTake"; +import ApproverStockTake from "./ApproverStockTake"; const StockTakeTab: React.FC = () => { - const { t } = useTranslation(["inventory"]); - - // Search states for floors - const defaultFloorInputs = useMemo(() => ({ - floorCode: "", - floorName: "", - warehouseCode: "", - }), []); - const [floorInputs, setFloorInputs] = useState>(defaultFloorInputs); - - // Selection states - const [selectedFloor, setSelectedFloor] = useState(null); - const [selectedZone, setSelectedZone] = useState(null); - - // Paging controllers - const [floorsPagingController, setFloorsPagingController] = useState(defaultPagingController); - const [zonesPagingController, setZonesPagingController] = useState(defaultPagingController); - const [lotsPagingController, setLotsPagingController] = useState(defaultPagingController); - - // Stock take dialog - const [stockTakeDialogOpen, setStockTakeDialogOpen] = useState(false); - const [selectedLot, setSelectedLot] = useState(null); - const [countedQty, setCountedQty] = useState(0); - const [remark, setRemark] = useState(""); - - // Filtered data - const filteredFloors = useMemo(() => { - return fakeFloors.filter(floor => { - if (floorInputs.floorCode && !floor.code.toLowerCase().includes(floorInputs.floorCode.toLowerCase())) { - return false; - } - if (floorInputs.floorName && !floor.name.toLowerCase().includes(floorInputs.floorName.toLowerCase())) { - return false; - } - if (floorInputs.warehouseCode && !floor.warehouseCode.toLowerCase().includes(floorInputs.warehouseCode.toLowerCase())) { - return false; - } - return true; - }); - }, [floorInputs]); - - const filteredZones = useMemo(() => { - if (!selectedFloor) return []; - return fakeZones.filter(zone => zone.floorId === selectedFloor.id); - }, [selectedFloor]); - - const filteredLots = useMemo(() => { - if (!selectedZone) return []; - return fakeLots.filter(lot => lot.zoneId === selectedZone.id); - }, [selectedZone]); - - // Search criteria - const floorSearchCriteria: Criterion[] = useMemo( - () => [ - { label: t("Floor Code"), paramName: "floorCode", type: "text" }, - { label: t("Floor Name"), paramName: "floorName", type: "text" }, - { label: t("Warehouse Code"), paramName: "warehouseCode", type: "text" }, - ], - [t], - ); - - // Handlers - const handleFloorSearch = useCallback((query: Record) => { - setFloorInputs(() => query); - setFloorsPagingController(() => defaultPagingController); - }, []); - - const handleFloorReset = useCallback(() => { - setFloorInputs(() => defaultFloorInputs); - setFloorsPagingController(() => defaultPagingController); - setSelectedFloor(null); - setSelectedZone(null); - }, [defaultFloorInputs]); - - const handleFloorClick = useCallback((floor: Floor) => { - setSelectedFloor(floor); - setSelectedZone(null); - setZonesPagingController(() => defaultPagingController); - setLotsPagingController(() => defaultPagingController); + const { t } = useTranslation(["inventory", "common"]); + const [tabValue, setTabValue] = useState(0); + const [selectedSession, setSelectedSession] = useState(null); + const [viewMode, setViewMode] = useState<"details" | "reStockTake">("details"); + const [snackbar, setSnackbar] = useState<{ + open: boolean; + message: string; + severity: "success" | "error" | "warning" + }>({ + open: false, + message: "", + severity: "success", + }); + + const handleCardClick = useCallback((session: AllPickedStockTakeListReponse) => { + setSelectedSession(session); + setViewMode("details"); }, []); - const handleZoneClick = useCallback((zone: Zone) => { - setSelectedZone(zone); - setLotsPagingController(() => defaultPagingController); + const handleReStockTakeClick = useCallback((session: AllPickedStockTakeListReponse) => { + setSelectedSession(session); + setViewMode("reStockTake"); }, []); - const handleStockTakeClick = useCallback((lot: InventoryLotLineForStockTake) => { - setSelectedLot(lot); - setCountedQty(lot.countedQty || lot.systemQty); - setRemark(""); - setStockTakeDialogOpen(true); + const handleBackToList = useCallback(() => { + setSelectedSession(null); + setViewMode("details"); }, []); - const handleStockTakeSubmit = useCallback(() => { - if (!selectedLot) return; - - const variance = countedQty - selectedLot.systemQty; - - // Here you would call the API to submit stock take - console.log("Stock Take Submitted:", { - lotId: selectedLot.id, - systemQty: selectedLot.systemQty, - countedQty: countedQty, - variance: variance, - remark: remark, + const handleSnackbar = useCallback((message: string, severity: "success" | "error" | "warning") => { + setSnackbar({ + open: true, + message, + severity, }); - - alert(`${t("Stock take recorded successfully!")}\n${t("Variance")}: ${variance > 0 ? '+' : ''}${variance}`); - - // Update the lot with counted qty (in real app, this would come from backend) - selectedLot.countedQty = countedQty; - selectedLot.variance = variance; - - setStockTakeDialogOpen(false); - setSelectedLot(null); - }, [selectedLot, countedQty, remark, t]); - - const handleDialogClose = useCallback(() => { - setStockTakeDialogOpen(false); - setSelectedLot(null); }, []); - // Floor columns - const floorColumns: Column[] = useMemo( - () => [ - { name: "code", label: t("Floor Code") }, - { name: "name", label: t("Floor Name") }, - { - name: "warehouseCode", - label: t("Warehouse"), - renderCell: (params) => `${params.warehouseCode} - ${params.warehouseName}`, - }, - ], - [t], - ); - - // Zone columns - const zoneColumns: Column[] = useMemo( - () => [ - { name: "code", label: t("Zone Code") }, - { name: "name", label: t("Zone Name") }, - { name: "description", label: t("Description") }, - ], - [t], - ); - - // Lot columns - const lotColumns: Column[] = useMemo( - () => [ - { name: "itemCode", label: t("Item Code") }, - { name: "itemName", label: t("Item Name") }, - { name: "lotNo", label: t("Lot No") }, - { name: "location", label: t("Location") }, - { name: "systemQty", label: t("System Qty"), align: "right", type: "integer" }, - { - name: "countedQty", - label: t("Counted Qty"), - align: "right", - renderCell: (params) => params.countedQty || "-", - }, - { - name: "variance", - label: t("Variance"), - align: "right", - renderCell: (params) => { - if (params.variance === undefined) return "-"; - const variance = params.variance; - return ( - 0 ? "success.main" : "error.main", - fontWeight: variance !== 0 ? "bold" : "normal", - }} - > - {variance > 0 ? `+${variance}` : variance} - - ); - }, - }, - { name: "uom", label: t("UOM") }, - { - name: "id", - label: t("Action"), - renderCell: (params) => ( - - ), - }, - ], - [t, handleStockTakeClick], - ); + if (selectedSession) { + return ( + + {tabValue === 0 ? ( + viewMode === "reStockTake" ? ( + + ) : ( + + ) + ) : ( + + )} + setSnackbar({ ...snackbar, open: false })} + > + setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}> + {snackbar.message} + + + + ); + } return ( - - {t("This is a demo with fake data. API integration pending.")} - - - {/* Step 1: Select Floor */} - - {t("Step 1: Select Floor")} - - - - items={filteredFloors} - columns={floorColumns} - pagingController={floorsPagingController} - setPagingController={setFloorsPagingController} - totalCount={filteredFloors.length} - onRowClick={handleFloorClick} - /> - - {/* Step 2: Select Zone */} - {selectedFloor && ( - <> - - {t("Step 2: Select Zone")} - {selectedFloor.name} - - - items={filteredZones} - columns={zoneColumns} - pagingController={zonesPagingController} - setPagingController={setZonesPagingController} - totalCount={filteredZones.length} - onRowClick={handleZoneClick} - /> - - )} - - {/* Step 3: Stock Take */} - {selectedZone && ( - <> - - {t("Step 3: Perform Stock Take")} - {selectedZone.name} - - - items={filteredLots} - columns={lotColumns} - pagingController={lotsPagingController} - setPagingController={setLotsPagingController} - totalCount={filteredLots.length} - /> - + setTabValue(newValue)} sx={{ mb: 2 }}> + + + + + {tabValue === 0 ? ( + + ) : ( + )} - {/* Stock Take Dialog */} - - {t("Stock Take")} - - {selectedLot && ( - - - - {t("Item")} - - - {selectedLot.itemCode} - {selectedLot.itemName} - - - - - - {t("Lot No")} - - {selectedLot.lotNo} - - - - - {t("Location")} - - {selectedLot.location} - - - - - {t("System Qty")} - - - {selectedLot.systemQty} {selectedLot.uom} - - - - setCountedQty(parseInt(e.target.value) || 0)} - fullWidth - autoFocus - /> - - - - {t("Variance")} - - 0 - ? "success.main" - : "error.main", - }} - > - {countedQty - selectedLot.systemQty > 0 ? "+" : ""} - {countedQty - selectedLot.systemQty} - - - - setRemark(e.target.value)} - fullWidth - /> - - )} - - - - - - + setSnackbar({ ...snackbar, open: false })} + > + setSnackbar({ ...snackbar, open: false })} severity={snackbar.severity}> + {snackbar.message} + + ); };