# Conflicts: # src/components/FinishedGoodSearch/FinishedGoodSearch.tsx # src/i18n/zh/pickOrder.jsonmaster
| @@ -267,6 +267,84 @@ export interface AutoAssignReleaseByStoreRequest { | |||
| userId: number; | |||
| storeId: string; // "2/F" | "4/F" | |||
| } | |||
| export interface UpdateDoPickOrderHideStatusRequest { | |||
| id: number; | |||
| name: string; | |||
| code: string; | |||
| type: string; | |||
| message: string; | |||
| errorPosition: string; | |||
| } | |||
| export interface CompletedDoPickOrderResponse { | |||
| id: number; | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| pickOrderConsoCode: string; | |||
| pickOrderStatus: string; | |||
| deliveryOrderId: number; | |||
| deliveryNo: string; | |||
| deliveryDate: string; | |||
| shopId: number; | |||
| shopCode: string; | |||
| shopName: string; | |||
| shopAddress: string; | |||
| ticketNo: string; | |||
| shopPoNo: string; | |||
| numberOfCartons: number; | |||
| truckNo: string; | |||
| storeId: string; | |||
| completedDate: string; | |||
| fgPickOrders: FGPickOrderResponse[]; | |||
| } | |||
| // ✅ 新增:搜索参数接口 | |||
| export interface CompletedDoPickOrderSearchParams { | |||
| pickOrderCode?: string; | |||
| shopName?: string; | |||
| deliveryNo?: string; | |||
| ticketNo?: string; | |||
| } | |||
| // ✅ 新增:获取已完成的 DO Pick Orders API | |||
| export const fetchCompletedDoPickOrders = async ( | |||
| userId: number, | |||
| searchParams?: CompletedDoPickOrderSearchParams | |||
| ): Promise<CompletedDoPickOrderResponse[]> => { | |||
| const params = new URLSearchParams(); | |||
| if (searchParams?.pickOrderCode) { | |||
| params.append('pickOrderCode', searchParams.pickOrderCode); | |||
| } | |||
| if (searchParams?.shopName) { | |||
| params.append('shopName', searchParams.shopName); | |||
| } | |||
| if (searchParams?.deliveryNo) { | |||
| params.append('deliveryNo', searchParams.deliveryNo); | |||
| } | |||
| if (searchParams?.ticketNo) { | |||
| params.append('ticketNo', searchParams.ticketNo); | |||
| } | |||
| const queryString = params.toString(); | |||
| const url = `${BASE_API_URL}/pickOrder/completed-do-pick-orders/${userId}${queryString ? `?${queryString}` : ''}`; | |||
| const response = await serverFetchJson<CompletedDoPickOrderResponse[]>(url, { | |||
| method: "GET", | |||
| }); | |||
| return response; | |||
| }; | |||
| export const updatePickOrderHideStatus = async (pickOrderId: number, hide: boolean) => { | |||
| const response = await serverFetchJson<UpdateDoPickOrderHideStatusRequest>( | |||
| `${BASE_API_URL}/pickOrder/update-hide-status/${pickOrderId}?hide=${hide}`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("pickorder"); | |||
| return response; | |||
| }; | |||
| export const fetchFGPickOrders = async (pickOrderId: number) => { | |||
| const response = await serverFetchJson<FGPickOrderResponse>( | |||
| @@ -581,7 +659,28 @@ const fetchSuggestionsWithStatus = async (pickOrderLineId: number) => { | |||
| return []; | |||
| } | |||
| }; | |||
| export const fetchAllPickOrderLotsHierarchical = cache(async (userId: number): Promise<any> => { | |||
| try { | |||
| console.log("🔍 Fetching hierarchical pick order lots for userId:", userId); | |||
| const data = await serverFetchJson<any>( | |||
| `${BASE_API_URL}/pickOrder/all-lots-hierarchical/${userId}`, | |||
| { | |||
| method: 'GET', | |||
| next: { tags: ["pickorder"] }, | |||
| } | |||
| ); | |||
| console.log("✅ Fetched hierarchical lot details:", data); | |||
| return data; | |||
| } catch (error) { | |||
| console.error("❌ Error fetching hierarchical lot details:", error); | |||
| return { | |||
| pickOrder: null, | |||
| pickOrderLines: [] | |||
| }; | |||
| } | |||
| }); | |||
| // Update the existing function to use the non-auto-assign endpoint | |||
| export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Promise<any[]> => { | |||
| try { | |||
| @@ -4,13 +4,14 @@ import { BASE_API_URL } from "../../../config/api"; | |||
| // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { cache } from "react"; | |||
| import { PoResult, StockInLine } from "."; | |||
| import { PoResult } from "."; | |||
| //import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { serverFetchJson, serverFetchWithNoContent } from "../../utils/fetchUtil"; | |||
| import { QcItemResult } from "../settings/qcItem"; | |||
| import { RecordsRes } from "../utils"; | |||
| import { Uom } from "../settings/uom"; | |||
| import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | |||
| import { StockInLine } from "../stockIn"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| export interface PostStockInLineResponse<T> { | |||
| @@ -24,11 +25,12 @@ export interface PostStockInLineResponse<T> { | |||
| // entity: StockInLine | StockInLine[] | |||
| } | |||
| // DEPRECIATED | |||
| export interface StockInLineEntry { | |||
| id?: number; | |||
| itemId: number; | |||
| purchaseOrderId: number; | |||
| purchaseOrderLineId: number; | |||
| purchaseOrderId?: number; | |||
| purchaseOrderLineId?: number; | |||
| acceptedQty: number; | |||
| status?: string; | |||
| expiryDate?: string; | |||
| @@ -38,6 +40,7 @@ export interface StockInLineEntry { | |||
| dnNo?: string; | |||
| } | |||
| // DEPRECIATED | |||
| export interface PurchaseQcResult{ | |||
| id?: number; | |||
| qcItemId: number; | |||
| @@ -46,6 +49,7 @@ export interface PurchaseQcResult{ | |||
| remarks?: string; | |||
| escalationLogId?: number; | |||
| } | |||
| // DEPRECIATED | |||
| export interface StockInInput { | |||
| status: string; | |||
| poCode: string; | |||
| @@ -53,6 +57,7 @@ export interface StockInInput { | |||
| dnNo?: string; | |||
| dnDate?: string; | |||
| itemName: string; | |||
| lotNo?: string; | |||
| invoiceNo?: string; | |||
| receiptDate: string; | |||
| supplier: string; | |||
| @@ -64,6 +69,7 @@ export interface StockInInput { | |||
| expiryDate: string; | |||
| uom: Uom; | |||
| } | |||
| // DEPRECIATED | |||
| export interface PurchaseQCInput { | |||
| status: string; | |||
| acceptQty: number; | |||
| @@ -75,6 +81,7 @@ export interface PurchaseQCInput { | |||
| qcDecision?: number; | |||
| qcResult: PurchaseQcResult[]; | |||
| } | |||
| // DEPRECIATED | |||
| export interface EscalationInput { | |||
| status: string; | |||
| remarks?: string; | |||
| @@ -84,6 +91,8 @@ export interface EscalationInput { | |||
| acceptedQty?: number; // this is the qty to be escalated | |||
| // escalationQty: number | |||
| } | |||
| // DEPRECIATED | |||
| export interface PutAwayLine { | |||
| id?: number | |||
| qty: number | |||
| @@ -92,6 +101,8 @@ export interface PutAwayLine { | |||
| printQty: number; | |||
| _isNew?: boolean; | |||
| } | |||
| // DEPRECIATED | |||
| export interface PutAwayInput { | |||
| status: string; | |||
| acceptedQty: number; | |||
| @@ -99,12 +110,14 @@ export interface PutAwayInput { | |||
| putAwayLines: PutAwayLine[] | |||
| } | |||
| // DEPRECIATED | |||
| export type ModalFormInput = Partial< | |||
| PurchaseQCInput & StockInInput & PutAwayInput | |||
| > & { | |||
| escalationLog? : Partial<EscalationInput> | |||
| }; | |||
| // DEPRECIATED | |||
| export interface PrintQrCodeForSilRequest { | |||
| stockInLineId: number; | |||
| printerId: number; | |||
| @@ -117,6 +130,7 @@ export const testFetch = cache(async (id: number) => { | |||
| }); | |||
| }); | |||
| // DEPRECIATED | |||
| export const fetchStockInLineInfo = cache(async (stockInLineId: number) => { | |||
| return serverFetchJson<StockInLine>( | |||
| `${BASE_API_URL}/stockInLine/${stockInLineId}`, | |||
| @@ -126,6 +140,7 @@ export const fetchStockInLineInfo = cache(async (stockInLineId: number) => { | |||
| ); | |||
| }); | |||
| // DEPRECIATED | |||
| export const createStockInLine = async (data: StockInLineEntry) => { | |||
| const stockInLine = await serverFetchJson< | |||
| PostStockInLineResponse<StockInLine> | |||
| @@ -138,6 +153,7 @@ export const createStockInLine = async (data: StockInLineEntry) => { | |||
| return stockInLine; | |||
| }; | |||
| // DEPRECIATED | |||
| export const updateStockInLine = async ( | |||
| data: StockInLineEntry & ModalFormInput, | |||
| ) => { | |||
| @@ -228,6 +244,7 @@ export const testing = cache(async (queryParams?: Record<string, any>) => { | |||
| } | |||
| }); | |||
| // DEPRECIATED | |||
| export const printQrCodeForSil = cache(async(data: PrintQrCodeForSilRequest) => { | |||
| const params = convertObjToURLSearchParams(data) | |||
| return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/printQrCode?${params}`, | |||
| @@ -7,6 +7,7 @@ import { BASE_API_URL } from "../../../config/api"; | |||
| import { Uom } from "../settings/uom"; | |||
| import { RecordsRes } from "../utils"; | |||
| import { PutAwayLine } from "./actions"; | |||
| import { StockInLine } from "../stockIn"; | |||
| export enum StockInStatus { | |||
| PENDING = "pending", | |||
| @@ -60,7 +61,8 @@ export interface StockUomForPoLine { | |||
| purchaseRatioD: number; | |||
| } | |||
| export interface StockInLine { | |||
| // DEPRECIATED | |||
| export interface StockInLine_old { | |||
| id: number; | |||
| stockInId?: number; | |||
| purchaseOrderId?: number; | |||
| @@ -15,6 +15,14 @@ export interface QcItemWithChecks { | |||
| description: string | undefined; | |||
| } | |||
| export interface QcResult{ | |||
| id?: number; | |||
| qcItemId: number; | |||
| qcPassed?: boolean; | |||
| failQty?: number; | |||
| remarks?: string; | |||
| escalationLogId?: number; | |||
| } | |||
| export interface QcData { | |||
| id?: number, | |||
| qcItemId: number, | |||
| @@ -0,0 +1,243 @@ | |||
| "use server"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| import { BASE_API_URL } from "../../../config/api"; | |||
| // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { cache } from "react"; | |||
| import { PoResult, StockInLine } from "."; | |||
| //import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { serverFetchJson, serverFetchWithNoContent } from "../../utils/fetchUtil"; | |||
| import { QcItemResult } from "../settings/qcItem"; | |||
| import { RecordsRes } from "../utils"; | |||
| import { Uom } from "../settings/uom"; | |||
| import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| export interface PostStockInLineResponse<T> { | |||
| id: number | null; | |||
| name: string; | |||
| code: string; | |||
| type?: string; | |||
| message: string | null; | |||
| errorPosition: string | keyof T; | |||
| entity: T | T[]; | |||
| // entity: StockInLine | StockInLine[] | |||
| } | |||
| export interface StockInLineEntry { | |||
| id?: number; | |||
| itemId: number; | |||
| acceptedQty: number; | |||
| purchaseOrderId?: number; | |||
| purchaseOrderLineId?: number; | |||
| status?: string; | |||
| expiryDate?: string; | |||
| productLotNo?: string; | |||
| receiptDate?: string; | |||
| dnDate?: string; | |||
| dnNo?: string; | |||
| } | |||
| export interface PurchaseQcResult{ | |||
| id?: number; | |||
| qcItemId: number; | |||
| qcPassed?: boolean; | |||
| failQty?: number; | |||
| remarks?: string; | |||
| escalationLogId?: number; | |||
| } | |||
| export interface StockInInput { | |||
| status: string; | |||
| poCode: string; | |||
| productLotNo?: string; | |||
| dnNo?: string; | |||
| dnDate?: string; | |||
| itemName: string; | |||
| lotNo?: string; | |||
| invoiceNo?: string; | |||
| receiptDate: string; | |||
| supplier: string; | |||
| acceptedQty: number; | |||
| qty: number; | |||
| receivedQty: number; | |||
| acceptedWeight?: number; | |||
| productionDate?: string; | |||
| expiryDate: string; | |||
| uom: Uom; | |||
| } | |||
| export interface PurchaseQCInput { | |||
| status: string; | |||
| acceptQty: number; | |||
| passingQty: number; | |||
| sampleRate?: number; | |||
| sampleWeight?: number; | |||
| totalWeight?: number; | |||
| qcAccept: boolean; | |||
| qcDecision?: number; | |||
| qcResult: PurchaseQcResult[]; | |||
| } | |||
| export interface EscalationInput { | |||
| status: string; | |||
| remarks?: string; | |||
| reason?: string; | |||
| handlerId: number; | |||
| productLotNo?: string; | |||
| acceptedQty?: number; // this is the qty to be escalated | |||
| // escalationQty: number | |||
| } | |||
| export interface PutAwayLine { | |||
| id?: number | |||
| qty: number | |||
| warehouseId: number; | |||
| warehouse: string; | |||
| printQty: number; | |||
| _isNew?: boolean; | |||
| } | |||
| export interface PutAwayInput { | |||
| status: string; | |||
| acceptedQty: number; | |||
| warehouseId: number; | |||
| putAwayLines: PutAwayLine[] | |||
| } | |||
| export type ModalFormInput = Partial< | |||
| PurchaseQCInput & StockInInput & PutAwayInput | |||
| > & { | |||
| escalationLog? : Partial<EscalationInput> | |||
| }; | |||
| export interface PrintQrCodeForSilRequest { | |||
| stockInLineId: number; | |||
| printerId: number; | |||
| printQty?: number; | |||
| } | |||
| export const testFetch = cache(async (id: number) => { | |||
| return serverFetchJson<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| export const fetchStockInLineInfo = cache(async (stockInLineId: number) => { | |||
| return serverFetchJson<StockInLine>( | |||
| `${BASE_API_URL}/stockInLine/${stockInLineId}`, | |||
| { | |||
| next: { tags: ["stockInLine"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| export const createStockInLine = async (data: StockInLineEntry) => { | |||
| const stockInLine = await serverFetchJson< | |||
| PostStockInLineResponse<StockInLine> | |||
| >(`${BASE_API_URL}/stockInLine/create`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| // revalidateTag("po"); | |||
| return stockInLine; | |||
| }; | |||
| export const updateStockInLine = async ( | |||
| data: StockInLineEntry & ModalFormInput, | |||
| ) => { | |||
| const stockInLine = await serverFetchJson< | |||
| PostStockInLineResponse<StockInLine & ModalFormInput> | |||
| >(`${BASE_API_URL}/stockInLine/update`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| // revalidateTag("po"); | |||
| return stockInLine; | |||
| }; | |||
| export const startPo = async (poId: number) => { | |||
| const po = await serverFetchJson<PostStockInLineResponse<PoResult>>( | |||
| `${BASE_API_URL}/po/start/${poId}`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify({ poId }), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("po"); | |||
| return po; | |||
| }; | |||
| export const checkPolAndCompletePo = async (poId: number) => { | |||
| const po = await serverFetchJson<PostStockInLineResponse<PoResult>>( | |||
| `${BASE_API_URL}/po/check/${poId}`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify({ poId }), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("po"); | |||
| return po; | |||
| }; | |||
| export const fetchPoInClient = cache(async (id: number) => { | |||
| return serverFetchJson<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
| next: { tags: ["po"] }, | |||
| }); | |||
| }); | |||
| export const fetchPoListClient = cache( | |||
| async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<PoResult[]>>( | |||
| `${BASE_API_URL}/po/list?${queryString}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["po"] }, | |||
| }, | |||
| ); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<PoResult[]>>( | |||
| `${BASE_API_URL}/po/list`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["po"] }, | |||
| }, | |||
| ); | |||
| } | |||
| }, | |||
| ); | |||
| export const testing = cache(async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<PoResult[]>>( | |||
| `${BASE_API_URL}/po/testing?${queryString}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["po"] }, | |||
| }, | |||
| ); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<PoResult[]>>( | |||
| `${BASE_API_URL}/po/testing`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["po"] }, | |||
| }, | |||
| ); | |||
| } | |||
| }); | |||
| export const printQrCodeForSil = cache(async(data: PrintQrCodeForSilRequest) => { | |||
| const params = convertObjToURLSearchParams(data) | |||
| return serverFetchWithNoContent(`${BASE_API_URL}/stockInLine/printQrCode?${params}`, | |||
| { | |||
| method: "GET", | |||
| headers: { "Content-Type": "application/json" }, | |||
| next: { | |||
| tags: ["printQrCodeForSil"], | |||
| }, | |||
| }, | |||
| ) | |||
| }) | |||
| @@ -0,0 +1,168 @@ | |||
| import { cache } from "react"; | |||
| import "server-only"; | |||
| // import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| // import { BASE_API_URL } from "@/config/api"; | |||
| import { serverFetchJson } from "../../utils/fetchUtil"; | |||
| import { BASE_API_URL } from "../../../config/api"; | |||
| import { Uom } from "../settings/uom"; | |||
| import { RecordsRes } from "../utils"; | |||
| import { QcResult } from "../qc"; | |||
| import { EscalationResult } from "../escalation"; | |||
| export enum StockInStatus { | |||
| PENDING = "pending", | |||
| RECEIVED = "received", | |||
| APPROVED = "escalated", | |||
| REJECTED = "rejected", | |||
| COMPLETED = "completed", | |||
| PARTIALLY_COMPLETED = "partially_completed", | |||
| } | |||
| export interface StockInLineInput { | |||
| id?: number; | |||
| itemId?: number; | |||
| acceptedQty?: number; | |||
| receivedQty?: number; | |||
| status?: string; | |||
| expiryDate?: string; | |||
| productLotNo?: string; | |||
| receiptDate?: string; | |||
| dnDate?: number[]; | |||
| dnNo?: string; | |||
| } | |||
| export interface StockInInput { | |||
| stockInId?: number; | |||
| poCode?: string; | |||
| productLotNo?: string; | |||
| dnNo?: string; | |||
| dnDate?: string; | |||
| itemName?: string; | |||
| lotNo?: string; | |||
| invoiceNo?: string; | |||
| receiptDate: string; | |||
| supplier?: string; | |||
| acceptedQty: number; | |||
| qty: number; | |||
| receivedQty: number; | |||
| acceptedWeight?: number; | |||
| productionDate?: string; | |||
| expiryDate: string; | |||
| uom?: Uom; | |||
| } | |||
| export interface PoResult { | |||
| id: number; | |||
| code: string; | |||
| orderDate: string; | |||
| supplier: string; | |||
| estimatedArrivalDate: string; | |||
| completedDate: string; | |||
| itemDetail?: string; | |||
| itemCode?: string; | |||
| itemName?: string; | |||
| itemQty?: string; | |||
| itemSumAcceptedQty?: string; | |||
| itemUom?: string; | |||
| escalated: boolean; | |||
| status: string; | |||
| pol?: PurchaseOrderLine[]; | |||
| } | |||
| export interface PurchaseOrderLine { | |||
| id: number; | |||
| purchaseOrderId: number; | |||
| itemId: number; | |||
| itemNo: string; | |||
| itemName: string; | |||
| qty: number; | |||
| processed: number; | |||
| receivedQty: number; | |||
| uom: Uom; | |||
| stockUom: StockUomForPoLine; | |||
| price: number; | |||
| status: string; | |||
| stockInLine: StockInLine[]; | |||
| } | |||
| export interface StockUomForPoLine { | |||
| id: number; | |||
| stockUomCode: string; | |||
| stockUomDesc: string; | |||
| stockQty: number; | |||
| stockRatioN: number; | |||
| stockRatioD: number; | |||
| purchaseRatioN: number; | |||
| purchaseRatioD: number; | |||
| } | |||
| export interface StockInLine { | |||
| id: number; | |||
| stockInId?: number; | |||
| purchaseOrderId?: number; | |||
| purchaseOrderLineId: number; | |||
| itemId: number; | |||
| itemNo: string; | |||
| itemName: string; | |||
| itemType: string; | |||
| demandQty: number; | |||
| acceptedQty: number; | |||
| qty?: number; | |||
| receivedQty?: number; | |||
| processed?: number; | |||
| price?: number; | |||
| priceUnit?: string; | |||
| shelfLife?: number; | |||
| receiptDate?: string; | |||
| productionDate?: string; | |||
| productLotNo?: string; | |||
| expiryDate?: string; | |||
| status: string; | |||
| supplier?: string; | |||
| lotNo?: string; | |||
| poCode?: string; | |||
| uom?: Uom; | |||
| defaultWarehouseId: number; // id for now | |||
| dnNo?: string; | |||
| dnDate?: number[]; | |||
| stockQty?: number; | |||
| handlerId?: number; | |||
| putAwayLines?: PutAwayLine[]; | |||
| qcResult?: QcResult[]; | |||
| escResult?: EscalationResult[]; | |||
| } | |||
| export interface EscalationInput { | |||
| status: string; | |||
| remarks?: string; | |||
| reason?: string; | |||
| handlerId: number; | |||
| productLotNo?: string; | |||
| acceptedQty?: number; // this is the qty to be escalated | |||
| // escalationQty: number | |||
| } | |||
| export interface PutAwayLine { | |||
| id?: number | |||
| qty: number | |||
| warehouseId: number; | |||
| warehouse: string; | |||
| printQty: number; | |||
| _isNew?: boolean; | |||
| } | |||
| export interface PutAwayInput { | |||
| status: string; | |||
| acceptedQty: number; | |||
| warehouseId: number; | |||
| putAwayLines: PutAwayLine[] | |||
| } | |||
| export interface QcInput { | |||
| acceptQty: number; | |||
| qcAccept: boolean; | |||
| qcDecision?: number; | |||
| qcResult: QcResult[]; | |||
| } | |||
| export type ModalFormInput = Partial< | |||
| QcInput & StockInInput & PutAwayInput | |||
| > & { | |||
| escalationLog? : Partial<EscalationInput> | |||
| }; | |||
| @@ -86,11 +86,11 @@ export const dayjsToDateString = (date: Dayjs) => { | |||
| return date.format(OUTPUT_DATE_FORMAT); | |||
| }; | |||
| export const dayjsToInputDateString = (date: Dayjs) => { | |||
| export const dayjsToInputDateString = (date: Dayjs) => { // TODO fix remove the time format (need global check) | |||
| return date.format(INPUT_DATE_FORMAT + "T" + INPUT_TIME_FORMAT); | |||
| }; | |||
| export const dayjsToInputDatetimeString = (date: Dayjs) => { | |||
| export const dayjsToInputDateStringFIX = (date: Dayjs) => { // TODO fix it after the above one is fixed | |||
| return date.format(INPUT_DATE_FORMAT); | |||
| }; | |||
| @@ -98,6 +98,17 @@ export const dayjsToInputDateTimeString = (date: Dayjs) => { | |||
| return date.format(`${INPUT_DATE_FORMAT}T${OUTPUT_TIME_FORMAT}`); | |||
| }; | |||
| export const dayjsToArray = (date: Dayjs) => { | |||
| return [ | |||
| date.year(), | |||
| date.month() + 1, // Months are 0-based in Day.js, so add 1 | |||
| date.date(), | |||
| date.hour(), // (24-hour format) | |||
| date.minute(), | |||
| date.second(), | |||
| ]; | |||
| }; | |||
| export const outputDateStringToInputDateString = (date: string) => { | |||
| return dayjsToInputDateString(dateStringToDayjs(date)) | |||
| } | |||
| @@ -64,7 +64,7 @@ const DoDetail: React.FC<Props> = ({ | |||
| if (response) { | |||
| formProps.setValue("status", response.entity.status) | |||
| setSuccessMessage("DO released successfully! Pick orders created.") | |||
| setSuccessMessage(t("DO released successfully! Pick orders created.")) | |||
| } | |||
| } | |||
| } catch (e) { | |||
| @@ -109,16 +109,7 @@ const FGPickOrderCard: React.FC<Props> = ({ fgOrder, onQrCodeClick }) => { | |||
| value={fgOrder.ticketNo} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}> | |||
| <Button | |||
| variant="contained" | |||
| startIcon={<QrCodeIcon />} | |||
| onClick={() => onQrCodeClick(fgOrder.pickOrderId)} | |||
| sx={{ minWidth: 120 }} | |||
| > | |||
| {t("Print DN/Label")} | |||
| </Button> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </CardContent> | |||
| @@ -29,6 +29,7 @@ import Jobcreatitem from "./Jobcreatitem"; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import PickExecutionDetail from "./GoodPickExecutiondetail"; | |||
| import GoodPickExecutionRecord from "./GoodPickExecutionRecord"; | |||
| import Swal from "sweetalert2"; | |||
| import { PrintDeliveryNoteRequest, printDN } from "@/app/api/do/actions"; | |||
| @@ -50,12 +51,24 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| const [isOpenCreateModal, setIsOpenCreateModal] = useState(false) | |||
| const [items, setItems] = useState<ItemCombo[]>([]) | |||
| const [printButtonsEnabled, setPrintButtonsEnabled] = useState(false); | |||
| const [filteredPickOrders, setFilteredPickOrders] = useState(pickOrders); | |||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const [isAssigning, setIsAssigning] = useState(false); | |||
| const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | |||
| typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | |||
| ); | |||
| useEffect(() => { | |||
| const onAssigned = () => { | |||
| localStorage.removeItem('hideCompletedUntilNext'); | |||
| setHideCompletedUntilNext(false); | |||
| }; | |||
| window.addEventListener('pickOrderAssigned', onAssigned); | |||
| return () => window.removeEventListener('pickOrderAssigned', onAssigned); | |||
| }, []); | |||
| const [fgPickOrdersData, setFgPickOrdersData] = useState<FGPickOrderResponse[]>([]); | |||
| @@ -231,6 +244,19 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| } | |||
| } | |||
| }, [tabIndex, items.length]); | |||
| useEffect(() => { | |||
| const handleCompletionStatusChange = (event: CustomEvent) => { | |||
| const { allLotsCompleted } = event.detail; | |||
| setPrintButtonsEnabled(allLotsCompleted); | |||
| console.log("Print buttons enabled:", allLotsCompleted); | |||
| }; | |||
| window.addEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||
| return () => { | |||
| window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener); | |||
| }; | |||
| }, []); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| () => { | |||
| @@ -391,6 +417,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| </Typography> | |||
| </Box> | |||
| </Grid> | |||
| </Grid> | |||
| {/* First 4 buttons aligned left */} | |||
| @@ -404,7 +431,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| </Grid> | |||
| {/* Last 2 buttons aligned right */} | |||
| <Grid item xs={6} display="flex" justifyContent="flex-end"> | |||
| <Grid item xs={6} > | |||
| <Stack direction="row" spacing={1}> | |||
| <Button | |||
| variant="contained" | |||
| @@ -422,6 +449,56 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| </Button> | |||
| </Stack> | |||
| </Grid> | |||
| {/* ✅ Updated print buttons with completion status */} | |||
| <Grid item xs={6} display="flex" justifyContent="flex-end"> | |||
| <Stack direction="row" spacing={1}> | |||
| {/* | |||
| <Button | |||
| variant={hideCompletedUntilNext ? "contained" : "outlined"} | |||
| color={hideCompletedUntilNext ? "warning" : "inherit"} | |||
| onClick={() => { | |||
| const next = !hideCompletedUntilNext; | |||
| setHideCompletedUntilNext(next); | |||
| if (next) localStorage.setItem('hideCompletedUntilNext', 'true'); | |||
| else localStorage.removeItem('hideCompletedUntilNext'); | |||
| window.dispatchEvent(new Event('pickOrderAssigned')); // ask detail to re-fetch | |||
| }} | |||
| > | |||
| {hideCompletedUntilNext ? t("Hide Completed: ON") : t("Hide Completed: OFF")} | |||
| </Button> | |||
| */} | |||
| <Button | |||
| variant="contained" | |||
| disabled={!printButtonsEnabled} | |||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | |||
| > | |||
| {t("Print Draft")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| disabled={!printButtonsEnabled} | |||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | |||
| > | |||
| {t("Print Pick Order and DN Label")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| disabled={!printButtonsEnabled} | |||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | |||
| > | |||
| {t("Print Pick Order")} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| disabled={!printButtonsEnabled} | |||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | |||
| > | |||
| {t("Print DN Label")} | |||
| </Button> | |||
| </Stack> | |||
| </Grid> | |||
| </Grid> | |||
| </Stack> | |||
| </Box> | |||
| @@ -433,6 +510,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("Pick Order Detail")} iconPosition="end" /> | |||
| <Tab label={t("Pick Execution Detail")} iconPosition="end" /> | |||
| <Tab label={t("Pick Execution Record")} iconPosition="end" /> | |||
| </Tabs> | |||
| </Box> | |||
| @@ -443,6 +521,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| }}> | |||
| {tabIndex === 0 && <PickExecution filterArgs={filterArgs} />} | |||
| {tabIndex === 1 && <PickExecutionDetail filterArgs={filterArgs} />} | |||
| {tabIndex === 2 && <GoodPickExecutionRecord filterArgs={filterArgs} />} | |||
| </Box> | |||
| </Box> | |||
| ); | |||
| @@ -351,7 +351,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({ | |||
| </DialogContent> | |||
| <DialogActions> | |||
| <Button onClick={handleClose} disabled={loading}> | |||
| {t('cancel')} | |||
| {t('Cancel')} | |||
| </Button> | |||
| <Button | |||
| onClick={handleSubmit} | |||
| @@ -0,0 +1,416 @@ | |||
| "use client"; | |||
| import { | |||
| Box, | |||
| Button, | |||
| Stack, | |||
| TextField, | |||
| Typography, | |||
| Alert, | |||
| CircularProgress, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TableHead, | |||
| TableRow, | |||
| Paper, | |||
| TablePagination, | |||
| Modal, | |||
| Card, | |||
| CardContent, | |||
| CardActions, | |||
| Chip, | |||
| Accordion, | |||
| AccordionSummary, | |||
| AccordionDetails, | |||
| } from "@mui/material"; | |||
| import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||
| import { useCallback, useEffect, useState, useRef, useMemo } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { | |||
| fetchALLPickOrderLineLotDetails, | |||
| updateStockOutLineStatus, | |||
| createStockOutLine, | |||
| recordPickExecutionIssue, | |||
| fetchFGPickOrders, | |||
| FGPickOrderResponse, | |||
| autoAssignAndReleasePickOrder, | |||
| AutoAssignReleaseResponse, | |||
| checkPickOrderCompletion, | |||
| PickOrderCompletionResponse, | |||
| checkAndCompletePickOrderByConsoCode, | |||
| fetchCompletedDoPickOrders, // ✅ 新增:使用新的 API | |||
| CompletedDoPickOrderResponse, | |||
| CompletedDoPickOrderSearchParams // ✅ 修复:导入类型 | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| import { | |||
| FormProvider, | |||
| useForm, | |||
| } from "react-hook-form"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; | |||
| import { updateInventoryLotLineQuantities } from "@/app/api/inventory/actions"; | |||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | |||
| import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider'; | |||
| import { useSession } from "next-auth/react"; | |||
| import { SessionWithTokens } from "@/config/authConfig"; | |||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | |||
| import GoodPickExecutionForm from "./GoodPickExecutionForm"; | |||
| import FGPickOrderCard from "./FGPickOrderCard"; | |||
| interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| // ✅ 新增:已完成的 DO Pick Order 接口 | |||
| interface CompletedDoPickOrder { | |||
| id: number; | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| pickOrderConsoCode: string; | |||
| pickOrderStatus: string; | |||
| deliveryOrderId: number; | |||
| deliveryNo: string; | |||
| deliveryDate: string; | |||
| shopId: number; | |||
| shopCode: string; | |||
| shopName: string; | |||
| shopAddress: string; | |||
| ticketNo: string; | |||
| shopPoNo: string; | |||
| numberOfCartons: number; | |||
| truckNo: string; | |||
| storeId: string; | |||
| completedDate: string; | |||
| fgPickOrders: FGPickOrderResponse[]; | |||
| } | |||
| // ✅ 新增:Pick Order 数据接口 | |||
| interface PickOrderData { | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| pickOrderConsoCode: string; | |||
| pickOrderStatus: string; | |||
| completedDate: string; | |||
| lots: any[]; | |||
| } | |||
| const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const router = useRouter(); | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| // ✅ 新增:已完成 DO Pick Orders 状态 | |||
| const [completedDoPickOrders, setCompletedDoPickOrders] = useState<CompletedDoPickOrder[]>([]); | |||
| const [completedDoPickOrdersLoading, setCompletedDoPickOrdersLoading] = useState(false); | |||
| // ✅ 新增:详情视图状态 | |||
| const [selectedDoPickOrder, setSelectedDoPickOrder] = useState<CompletedDoPickOrder | null>(null); | |||
| const [showDetailView, setShowDetailView] = useState(false); | |||
| const [detailLotData, setDetailLotData] = useState<any[]>([]); | |||
| // ✅ 新增:搜索状态 | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrder[]>([]); | |||
| // ✅ 新增:分页状态 | |||
| const [paginationController, setPaginationController] = useState({ | |||
| pageNum: 0, | |||
| pageSize: 10, | |||
| }); | |||
| const formProps = useForm(); | |||
| const errors = formProps.formState.errors; | |||
| // ✅ 修改:使用新的 API 获取已完成的 DO Pick Orders | |||
| const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => { | |||
| if (!currentUserId) return; | |||
| setCompletedDoPickOrdersLoading(true); | |||
| try { | |||
| console.log("🔍 Fetching completed DO pick orders with params:", searchParams); | |||
| const completedDoPickOrders = await fetchCompletedDoPickOrders(currentUserId, searchParams); | |||
| setCompletedDoPickOrders(completedDoPickOrders); | |||
| setFilteredDoPickOrders(completedDoPickOrders); | |||
| console.log("✅ Fetched completed DO pick orders:", completedDoPickOrders); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching completed DO pick orders:", error); | |||
| setCompletedDoPickOrders([]); | |||
| setFilteredDoPickOrders([]); | |||
| } finally { | |||
| setCompletedDoPickOrdersLoading(false); | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ 初始化时获取数据 | |||
| useEffect(() => { | |||
| if (currentUserId) { | |||
| fetchCompletedDoPickOrdersData(); | |||
| } | |||
| }, [currentUserId, fetchCompletedDoPickOrdersData]); | |||
| // ✅ 修改:搜索功能使用新的 API | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| setSearchQuery({ ...query }); | |||
| console.log("Search query:", query); | |||
| const searchParams: CompletedDoPickOrderSearchParams = { | |||
| pickOrderCode: query.pickOrderCode || undefined, | |||
| shopName: query.shopName || undefined, | |||
| deliveryNo: query.deliveryNo || undefined, | |||
| ticketNo: query.ticketNo || undefined, | |||
| }; | |||
| // 使用新的 API 进行搜索 | |||
| fetchCompletedDoPickOrdersData(searchParams); | |||
| }, [fetchCompletedDoPickOrdersData]); | |||
| // ✅ 修复:重命名函数避免重复声明 | |||
| const handleSearchReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| fetchCompletedDoPickOrdersData(); // 重新获取所有数据 | |||
| }, [fetchCompletedDoPickOrdersData]); | |||
| // ✅ 分页功能 | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| setPaginationController(prev => ({ | |||
| ...prev, | |||
| pageNum: newPage, | |||
| })); | |||
| }, []); | |||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||
| const newPageSize = parseInt(event.target.value, 10); | |||
| setPaginationController({ | |||
| pageNum: 0, | |||
| pageSize: newPageSize, | |||
| }); | |||
| }, []); | |||
| // ✅ 分页数据 | |||
| const paginatedData = useMemo(() => { | |||
| const startIndex = paginationController.pageNum * paginationController.pageSize; | |||
| const endIndex = startIndex + paginationController.pageSize; | |||
| return filteredDoPickOrders.slice(startIndex, endIndex); | |||
| }, [filteredDoPickOrders, paginationController]); | |||
| // ✅ 搜索条件 | |||
| const searchCriteria: Criterion<any>[] = [ | |||
| { | |||
| label: t("Pick Order Code"), | |||
| paramName: "pickOrderCode", | |||
| type: "text", | |||
| }, | |||
| { | |||
| label: t("Shop Name"), | |||
| paramName: "shopName", | |||
| type: "text", | |||
| }, | |||
| { | |||
| label: t("Delivery No"), | |||
| paramName: "deliveryNo", | |||
| type: "text", | |||
| }, | |||
| { | |||
| label: t("Ticket No"), | |||
| paramName: "ticketNo", | |||
| type: "text", | |||
| }, | |||
| ]; | |||
| // ✅ 处理详情点击 - 显示类似 GoodPickExecution 的表格 | |||
| const handleDetailClick = useCallback(async (doPickOrder: CompletedDoPickOrder) => { | |||
| setSelectedDoPickOrder(doPickOrder); | |||
| setShowDetailView(true); | |||
| // 获取该 pick order 的详细 lot 数据 | |||
| try { | |||
| const allLotDetails = await fetchALLPickOrderLineLotDetails(currentUserId!); | |||
| const filteredLots = allLotDetails.filter(lot => | |||
| lot.pickOrderId === doPickOrder.pickOrderId | |||
| ); | |||
| setDetailLotData(filteredLots); | |||
| console.log("✅ Loaded detail lot data for pick order:", doPickOrder.pickOrderCode, filteredLots); | |||
| } catch (error) { | |||
| console.error("❌ Error loading detail lot data:", error); | |||
| setDetailLotData([]); | |||
| } | |||
| }, [currentUserId]); | |||
| // ✅ 返回列表视图 | |||
| const handleBackToList = useCallback(() => { | |||
| setShowDetailView(false); | |||
| setSelectedDoPickOrder(null); | |||
| setDetailLotData([]); | |||
| }, []); | |||
| // ✅ 如果显示详情视图,渲染类似 GoodPickExecution 的表格 | |||
| if (showDetailView && selectedDoPickOrder) { | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| <Box> | |||
| {/* 返回按钮和标题 */} | |||
| <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}> | |||
| <Button variant="outlined" onClick={handleBackToList}> | |||
| {t("Back to List")} | |||
| </Button> | |||
| <Typography variant="h6"> | |||
| {t("Pick Order Details")}: {selectedDoPickOrder.pickOrderCode} | |||
| </Typography> | |||
| </Box> | |||
| {/* FG Pick Orders 信息 */} | |||
| <Box sx={{ mb: 2 }}> | |||
| {selectedDoPickOrder.fgPickOrders.map((fgOrder, index) => ( | |||
| <FGPickOrderCard | |||
| key={index} | |||
| fgOrder={fgOrder} | |||
| onQrCodeClick={() => {}} // 只读模式 | |||
| /> | |||
| ))} | |||
| </Box> | |||
| {/* 类似 GoodPickExecution 的表格 */} | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell>{t("Pick Order Code")}</TableCell> | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell>{t("Lot No")}</TableCell> | |||
| <TableCell>{t("Location")}</TableCell> | |||
| <TableCell>{t("Required Qty")}</TableCell> | |||
| <TableCell>{t("Actual Pick Qty")}</TableCell> | |||
| <TableCell>{t("Submitted Status")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {detailLotData.map((lot, index) => ( | |||
| <TableRow key={index}> | |||
| <TableCell>{lot.pickOrderCode}</TableCell> | |||
| <TableCell>{lot.itemCode}</TableCell> | |||
| <TableCell>{lot.itemName}</TableCell> | |||
| <TableCell>{lot.lotNo}</TableCell> | |||
| <TableCell>{lot.location}</TableCell> | |||
| <TableCell>{lot.requiredQty}</TableCell> | |||
| <TableCell>{lot.actualPickQty}</TableCell> | |||
| <TableCell> | |||
| <Chip | |||
| label={lot.processingStatus} | |||
| color={lot.processingStatus === 'completed' ? 'success' : 'default'} | |||
| size="small" | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| ))} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| </Box> | |||
| </FormProvider> | |||
| ); | |||
| } | |||
| // ✅ 默认列表视图 | |||
| return ( | |||
| <FormProvider {...formProps}> | |||
| <Box> | |||
| {/* 搜索框 */} | |||
| <Box sx={{ mb: 2 }}> | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={handleSearch} | |||
| onReset={handleSearchReset} | |||
| /> | |||
| </Box> | |||
| {/* 加载状态 */} | |||
| {completedDoPickOrdersLoading ? ( | |||
| <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}> | |||
| <CircularProgress /> | |||
| </Box> | |||
| ) : ( | |||
| <Box> | |||
| {/* 结果统计 */} | |||
| <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> | |||
| {t("Total")}: {filteredDoPickOrders.length} {t("completed DO pick orders")} | |||
| </Typography> | |||
| {/* 列表 */} | |||
| {filteredDoPickOrders.length === 0 ? ( | |||
| <Box sx={{ p: 3, textAlign: 'center' }}> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No completed DO pick orders found")} | |||
| </Typography> | |||
| </Box> | |||
| ) : ( | |||
| <Stack spacing={2}> | |||
| {paginatedData.map((doPickOrder) => ( | |||
| <Card key={doPickOrder.id}> | |||
| <CardContent> | |||
| <Stack direction="row" justifyContent="space-between" alignItems="center"> | |||
| <Box> | |||
| <Typography variant="h6"> | |||
| {doPickOrder.pickOrderCode} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {doPickOrder.shopName} - {doPickOrder.deliveryNo} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Completed")}: {new Date(doPickOrder.completedDate).toLocaleString()} | |||
| </Typography> | |||
| </Box> | |||
| <Box> | |||
| <Chip | |||
| label={doPickOrder.pickOrderStatus} | |||
| color={doPickOrder.pickOrderStatus === 'completed' ? 'success' : 'default'} | |||
| size="small" | |||
| sx={{ mb: 1 }} | |||
| /> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {doPickOrder.fgPickOrders.length} {t("FG orders")} | |||
| </Typography> | |||
| </Box> | |||
| </Stack> | |||
| </CardContent> | |||
| <CardActions> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => handleDetailClick(doPickOrder)} | |||
| > | |||
| {t("View Details")} | |||
| </Button> | |||
| </CardActions> | |||
| </Card> | |||
| ))} | |||
| </Stack> | |||
| )} | |||
| {/* 分页 */} | |||
| {filteredDoPickOrders.length > 0 && ( | |||
| <TablePagination | |||
| component="div" | |||
| count={filteredDoPickOrders.length} | |||
| page={paginationController.pageNum} | |||
| rowsPerPage={paginationController.pageSize} | |||
| onPageChange={handlePageChange} | |||
| onRowsPerPageChange={handlePageSizeChange} | |||
| rowsPerPageOptions={[5, 10, 25, 50]} | |||
| /> | |||
| )} | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| </FormProvider> | |||
| ); | |||
| }; | |||
| export default GoodPickExecutionRecord; | |||
| @@ -34,6 +34,7 @@ import { | |||
| autoAssignAndReleasePickOrder, | |||
| AutoAssignReleaseResponse, | |||
| checkPickOrderCompletion, | |||
| fetchAllPickOrderLotsHierarchical, | |||
| PickOrderCompletionResponse, | |||
| checkAndCompletePickOrderByConsoCode, | |||
| updateSuggestedLotLineId, | |||
| @@ -322,7 +323,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | |||
| const currentUserId = session?.id ? parseInt(session.id) : undefined; | |||
| const [allLotsCompleted, setAllLotsCompleted] = useState(false); | |||
| const [combinedLotData, setCombinedLotData] = useState<any[]>([]); | |||
| const [combinedDataLoading, setCombinedDataLoading] = useState(false); | |||
| const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]); | |||
| @@ -366,6 +367,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set()); | |||
| const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); | |||
| const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); | |||
| const fetchFgPickOrdersData = useCallback(async () => { | |||
| if (!currentUserId) return; | |||
| @@ -403,7 +405,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| fetchFgPickOrdersData(); | |||
| } | |||
| }, [combinedLotData, fetchFgPickOrdersData]); | |||
| // ✅ Handle QR code button click | |||
| const handleQrCodeClick = (pickOrderId: number) => { | |||
| console.log(`QR Code clicked for pick order ID: ${pickOrderId}`); | |||
| @@ -416,7 +418,31 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| setScannedLotData(scannedLot); | |||
| setLotConfirmationOpen(true); | |||
| }, []); | |||
| const checkAllLotsCompleted = useCallback((lotData: any[]) => { | |||
| if (lotData.length === 0) { | |||
| setAllLotsCompleted(false); | |||
| return false; | |||
| } | |||
| // Filter out rejected lots | |||
| const nonRejectedLots = lotData.filter(lot => | |||
| lot.lotAvailability !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'rejected' | |||
| ); | |||
| if (nonRejectedLots.length === 0) { | |||
| setAllLotsCompleted(false); | |||
| return false; | |||
| } | |||
| // Check if all non-rejected lots are completed | |||
| const allCompleted = nonRejectedLots.every(lot => | |||
| lot.stockOutLineStatus === 'completed' | |||
| ); | |||
| setAllLotsCompleted(allCompleted); | |||
| return allCompleted; | |||
| }, []); | |||
| const fetchAllCombinedLotData = useCallback(async (userId?: number) => { | |||
| setCombinedDataLoading(true); | |||
| try { | |||
| @@ -428,22 +454,111 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| console.warn("⚠️ No userId available, skipping API call"); | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| setAllLotsCompleted(false); | |||
| return; | |||
| } | |||
| // ✅ Use the non-auto-assign endpoint - this only fetches existing data | |||
| const allLotDetails = await fetchALLPickOrderLineLotDetails(userIdToUse); | |||
| console.log("✅ All combined lot details:", allLotDetails); | |||
| setCombinedLotData(allLotDetails); | |||
| setOriginalCombinedData(allLotDetails); | |||
| // ✅ Use the hierarchical endpoint that includes rejected lots | |||
| const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse); | |||
| console.log("✅ Hierarchical lot details:", hierarchicalData); | |||
| // ✅ Transform hierarchical data to flat structure for the table | |||
| const flatLotData: any[] = []; | |||
| if (hierarchicalData.pickOrder && hierarchicalData.pickOrderLines) { | |||
| hierarchicalData.pickOrderLines.forEach((line: any) => { | |||
| if (line.lots && line.lots.length > 0) { | |||
| line.lots.forEach((lot: any) => { | |||
| flatLotData.push({ | |||
| // Pick order info | |||
| pickOrderId: hierarchicalData.pickOrder.id, | |||
| pickOrderCode: hierarchicalData.pickOrder.code, | |||
| pickOrderConsoCode: hierarchicalData.pickOrder.consoCode, | |||
| pickOrderTargetDate: hierarchicalData.pickOrder.targetDate, | |||
| pickOrderType: hierarchicalData.pickOrder.type, | |||
| pickOrderStatus: hierarchicalData.pickOrder.status, | |||
| pickOrderAssignTo: hierarchicalData.pickOrder.assignTo, | |||
| // Pick order line info | |||
| pickOrderLineId: line.id, | |||
| pickOrderLineRequiredQty: line.requiredQty, | |||
| pickOrderLineStatus: line.status, | |||
| // Item info | |||
| itemId: line.item.id, | |||
| itemCode: line.item.code, | |||
| itemName: line.item.name, | |||
| uomCode: line.item.uomCode, | |||
| uomDesc: line.item.uomDesc, | |||
| // Lot info | |||
| lotId: lot.id, | |||
| lotNo: lot.lotNo, | |||
| expiryDate: lot.expiryDate, | |||
| location: lot.location, | |||
| stockUnit: lot.stockUnit, | |||
| availableQty: lot.availableQty, | |||
| requiredQty: lot.requiredQty, | |||
| actualPickQty: lot.actualPickQty, | |||
| inQty: lot.inQty, | |||
| outQty: lot.outQty, | |||
| holdQty: lot.holdQty, | |||
| lotStatus: lot.lotStatus, | |||
| lotAvailability: lot.lotAvailability, | |||
| processingStatus: lot.processingStatus, | |||
| suggestedPickLotId: lot.suggestedPickLotId, | |||
| stockOutLineId: lot.stockOutLineId, | |||
| stockOutLineStatus: lot.stockOutLineStatus, | |||
| stockOutLineQty: lot.stockOutLineQty, | |||
| // Router info | |||
| routerId: lot.router?.id, | |||
| routerIndex: lot.router?.index, | |||
| routerRoute: lot.router?.route, | |||
| routerArea: lot.router?.area, | |||
| uomShortDesc: lot.router?.uomId | |||
| }); | |||
| }); | |||
| } | |||
| }); | |||
| } | |||
| console.log("✅ Transformed flat lot data:", flatLotData); | |||
| setCombinedLotData(flatLotData); | |||
| setOriginalCombinedData(flatLotData); | |||
| // ✅ Check completion status | |||
| checkAllLotsCompleted(flatLotData); | |||
| } catch (error) { | |||
| console.error("❌ Error fetching combined lot data:", error); | |||
| setCombinedLotData([]); | |||
| setOriginalCombinedData([]); | |||
| setAllLotsCompleted(false); | |||
| } finally { | |||
| setCombinedDataLoading(false); | |||
| } | |||
| }, [currentUserId]); | |||
| }, [currentUserId, checkAllLotsCompleted]); | |||
| // ✅ Add effect to check completion when lot data changes | |||
| useEffect(() => { | |||
| if (combinedLotData.length > 0) { | |||
| checkAllLotsCompleted(combinedLotData); | |||
| } | |||
| }, [combinedLotData, checkAllLotsCompleted]); | |||
| // ✅ Add function to expose completion status to parent | |||
| const getCompletionStatus = useCallback(() => { | |||
| return allLotsCompleted; | |||
| }, [allLotsCompleted]); | |||
| // ✅ Expose completion status to parent component | |||
| useEffect(() => { | |||
| // Dispatch custom event with completion status | |||
| const event = new CustomEvent('pickOrderCompletionStatus', { | |||
| detail: { allLotsCompleted } | |||
| }); | |||
| window.dispatchEvent(event); | |||
| }, [allLotsCompleted]); | |||
| const handleLotConfirmation = useCallback(async () => { | |||
| if (!expectedLotData || !scannedLotData || !selectedLotForQr) return; | |||
| setIsConfirmingLot(true); | |||
| @@ -465,6 +580,15 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| newInventoryLotLineId: newLotLineId | |||
| }); | |||
| setQrScanError(false); | |||
| setQrScanSuccess(false); | |||
| setQrScanInput(''); | |||
| setIsManualScanning(false); | |||
| stopScan(); | |||
| resetScan(); | |||
| setProcessedQrCodes(new Set()); | |||
| setLastProcessedQr(''); | |||
| setLotConfirmationOpen(false); | |||
| setExpectedLotData(null); | |||
| setScannedLotData(null); | |||
| @@ -672,11 +796,25 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| console.error("No item match in expected lots for scanned code"); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| // 2) Check if scanned lot is exactly in expected lots | |||
| const exactLotMatch = sameItemLotsInExpected.find(l => | |||
| // ✅ FIXED: Find the ACTIVE suggested lot (not rejected lots) | |||
| const activeSuggestedLots = sameItemLotsInExpected.filter(lot => | |||
| lot.lotAvailability !== 'rejected' && | |||
| lot.stockOutLineStatus !== 'rejected' && | |||
| lot.processingStatus !== 'rejected' | |||
| ); | |||
| if (activeSuggestedLots.length === 0) { | |||
| console.error("No active suggested lots found for this item"); | |||
| setQrScanError(true); | |||
| setQrScanSuccess(false); | |||
| return; | |||
| } | |||
| // 2) Check if scanned lot is exactly in active suggested lots | |||
| const exactLotMatch = activeSuggestedLots.find(l => | |||
| (scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) || | |||
| (scanned?.lotNo && l.lotNo === scanned.lotNo) | |||
| ); | |||
| @@ -689,7 +827,8 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| } | |||
| // Case 2: Item matches but lot number differs -> open confirmation modal | |||
| const expectedLot = sameItemLotsInExpected[0]; | |||
| // ✅ FIXED: Use the first ACTIVE suggested lot, not just any lot | |||
| const expectedLot = activeSuggestedLots[0]; | |||
| if (!expectedLot) { | |||
| console.error("Could not determine expected lot for confirmation"); | |||
| setQrScanError(true); | |||
| @@ -697,6 +836,14 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| return; | |||
| } | |||
| // ✅ Check if the expected lot is already the scanned lot (after substitution) | |||
| if (expectedLot.lotNo === scanned?.lotNo) { | |||
| console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`); | |||
| handleQrCodeSubmit(scanned.lotNo); | |||
| return; | |||
| } | |||
| console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); | |||
| setSelectedLotForQr(expectedLot); | |||
| handleLotMismatch( | |||
| { | |||
| @@ -720,26 +867,27 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| } | |||
| }, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]); | |||
| // ✅ Update the outside QR scanning effect to use enhanced processing | |||
| useEffect(() => { | |||
| if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { | |||
| return; | |||
| } | |||
| // ✅ Update the outside QR scanning effect to use enhanced processing | |||
| useEffect(() => { | |||
| if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) { | |||
| return; | |||
| } | |||
| const latestQr = qrValues[qrValues.length - 1]; | |||
| const latestQr = qrValues[qrValues.length - 1]; | |||
| if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { | |||
| console.log("QR code already processed, skipping..."); | |||
| return; | |||
| } | |||
| if (latestQr && latestQr !== lastProcessedQr) { | |||
| console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); | |||
| setLastProcessedQr(latestQr); | |||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||
| if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) { | |||
| console.log("QR code already processed, skipping..."); | |||
| return; | |||
| } | |||
| if (latestQr && latestQr !== lastProcessedQr) { | |||
| console.log(`🔍 Processing new QR code with enhanced validation: ${latestQr}`); | |||
| setLastProcessedQr(latestQr); | |||
| setProcessedQrCodes(prev => new Set(prev).add(latestQr)); | |||
| processOutsideQrCode(latestQr); | |||
| } | |||
| }, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode]); | |||
| processOutsideQrCode(latestQr); | |||
| } | |||
| }, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData]); | |||
| // ✅ Only fetch existing data when session is ready, no auto-assignment | |||
| useEffect(() => { | |||
| if (session && currentUserId && !initializationRef.current) { | |||
| @@ -1003,7 +1151,14 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||
| setPickExecutionFormOpen(false); | |||
| setSelectedLotForExecutionForm(null); | |||
| setQrScanError(false); | |||
| setQrScanSuccess(false); | |||
| setQrScanInput(''); | |||
| setIsManualScanning(false); | |||
| stopScan(); | |||
| resetScan(); | |||
| setProcessedQrCodes(new Set()); | |||
| setLastProcessedQr(''); | |||
| await fetchAllCombinedLotData(); | |||
| } catch (error) { | |||
| console.error("Error submitting pick execution form:", error); | |||
| @@ -1315,7 +1470,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| {/* <TableCell>{t("Lot Location")}</TableCell> */} | |||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||
| {/* <TableCell align="right">{t("Original Available Qty")}</TableCell> */} | |||
| <TableCell align="right">{t("Scan Result")}</TableCell> | |||
| <TableCell align="center">{t("Scan Result")}</TableCell> | |||
| <TableCell align="center">{t("Submit Required Pick Qty")}</TableCell> | |||
| {/* <TableCell align="right">{t("Remaining Available Qty")}</TableCell> */} | |||
| @@ -1380,21 +1535,36 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||
| return result.toLocaleString()+'('+lot.uomShortDesc+')'; | |||
| })()} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? ( | |||
| <Checkbox | |||
| checked={lot.stockOutLineStatus?.toLowerCase() !== 'pending'} | |||
| disabled={true} | |||
| readOnly={true} | |||
| sx={{ | |||
| color: lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? 'success.main' : 'grey.400', | |||
| '&.Mui-checked': { | |||
| color: 'success.main', | |||
| }, | |||
| }} | |||
| /> | |||
| ) : null} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| {lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? ( | |||
| <Box sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'center', | |||
| alignItems: 'center', | |||
| width: '100%', | |||
| height: '100%' | |||
| }}> | |||
| <Checkbox | |||
| checked={lot.stockOutLineStatus?.toLowerCase() !== 'pending'} | |||
| disabled={true} | |||
| readOnly={true} | |||
| size="large" | |||
| sx={{ | |||
| color: lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? 'success.main' : 'grey.400', | |||
| '&.Mui-checked': { | |||
| color: 'success.main', | |||
| }, | |||
| transform: 'scale(1.3)', | |||
| '& .MuiSvgIcon-root': { | |||
| fontSize: '1.5rem', | |||
| } | |||
| }} | |||
| /> | |||
| </Box> | |||
| ) : null} | |||
| </TableCell> | |||
| <TableCell align="center"> | |||
| <Box sx={{ display: 'flex', justifyContent: 'center' }}> | |||
| <Stack direction="row" spacing={1} alignItems="center"> | |||
| @@ -92,7 +92,7 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({ | |||
| </Box> | |||
| <Alert severity="info"> | |||
| {t("If you proceed, the system will:")} | |||
| {t("If you confirm, the system will:")} | |||
| <ul style={{ margin: '8px 0 0 16px' }}> | |||
| <li>{t("Update your suggested lot to the this scanned lot")}</li> | |||
| </ul> | |||
| @@ -114,7 +114,7 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({ | |||
| color="warning" | |||
| disabled={isLoading} | |||
| > | |||
| {isLoading ? t("Processing...") : t("Yes, Use This Lot")} | |||
| {isLoading ? t("Processing...") : t("Confirm")} | |||
| </Button> | |||
| </DialogActions> | |||
| </Dialog> | |||
| @@ -22,7 +22,7 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||
| import ExpandLessIcon from '@mui/icons-material/ExpandLess'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { Controller, useFormContext } from 'react-hook-form'; | |||
| import { EscalationInput, ModalFormInput } from '@/app/api/po/actions'; | |||
| import { EscalationInput, ModalFormInput } from '@/app/api/stockIn/actions'; | |||
| import { EscalationCombo } from "@/app/api/user"; | |||
| import { fetchEscalationCombo } from "@/app/api/user/actions"; | |||
| import { FireExtinguisher } from '@mui/icons-material'; | |||
| @@ -1,6 +1,6 @@ | |||
| "use client"; | |||
| import { StockInLineEntry, EscalationInput } from "@/app/api/po/actions"; | |||
| import { StockInLineEntry, EscalationInput } from "@/app/api/stockIn/actions"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| @@ -30,7 +30,7 @@ import TwoLineCell from "./TwoLineCell"; | |||
| import QcSelect from "./QcSelect"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { StockInLine } from "@/app/api/stockIn"; | |||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||
| interface Props { | |||
| @@ -4,7 +4,6 @@ import { | |||
| fetchPoWithStockInLines, | |||
| PoResult, | |||
| PurchaseOrderLine, | |||
| StockInLine, | |||
| } from "@/app/api/po"; | |||
| import { | |||
| Box, | |||
| @@ -44,11 +43,11 @@ import { | |||
| checkPolAndCompletePo, | |||
| fetchPoInClient, | |||
| fetchPoListClient, | |||
| fetchStockInLineInfo, | |||
| PurchaseQcResult, | |||
| startPo, | |||
| createStockInLine | |||
| } from "@/app/api/po/actions"; | |||
| import { | |||
| createStockInLine | |||
| } from "@/app/api/stockIn/actions"; | |||
| import { | |||
| useCallback, | |||
| useContext, | |||
| @@ -59,7 +58,7 @@ import { | |||
| import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; | |||
| import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; | |||
| import PoInputGrid from "./PoInputGrid"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| // import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { useRouter, useSearchParams, usePathname } from "next/navigation"; | |||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||
| import { calculateWeight, dateStringToDayjs, dayjsToDateString, OUTPUT_DATE_FORMAT, outputDateStringToInputDateString, returnWeightUnit } from "@/app/utils/formatUtil"; | |||
| @@ -81,12 +80,13 @@ import LoadingComponent from "../General/LoadingComponent"; | |||
| import { getMailTemplatePdfForStockInLine } from "@/app/api/mailTemplate/actions"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| import { EscalationCombo } from "@/app/api/user"; | |||
| import { StockInLine } from "@/app/api/stockIn"; | |||
| //import { useRouter } from "next/navigation"; | |||
| type Props = { | |||
| po: PoResult; | |||
| qc: QcItemWithChecks[]; | |||
| // qc: QcItemWithChecks[]; | |||
| warehouse: WarehouseResult[]; | |||
| printerCombo: PrinterCombo[]; | |||
| }; | |||
| @@ -191,7 +191,7 @@ interface PolInputResult { | |||
| dnQty: number, | |||
| } | |||
| const PoDetail: React.FC<Props> = ({ po, qc, warehouse, printerCombo }) => { | |||
| const PoDetail: React.FC<Props> = ({ po, warehouse, printerCombo }) => { | |||
| const cameras = useContext(CameraContext); | |||
| // console.log(cameras); | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| @@ -236,7 +236,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse, printerCombo }) => { | |||
| const dnFormProps = useForm({ | |||
| defaultValues: { | |||
| dnNo: '', | |||
| dnDate: dayjsToDateString(dayjs()) | |||
| receiptDate: dayjsToDateString(dayjs()) | |||
| } | |||
| }) | |||
| const fetchPoList = useCallback(async () => { | |||
| @@ -263,7 +263,6 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse, printerCombo }) => { | |||
| const fetchPoDetail = useCallback(async (poId: string) => { | |||
| try { | |||
| const result = await fetchPoInClient(parseInt(poId)); | |||
| console.log(result) | |||
| if (result) { | |||
| console.log("%c Fetched PO:", "color:orange", result); | |||
| setPurchaseOrder(result); | |||
| @@ -420,11 +419,11 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse, printerCombo }) => { | |||
| const postData = { | |||
| dnNo: dnFormProps.watch("dnNo"), | |||
| dnDate: outputDateStringToInputDateString(dnFormProps.watch("dnDate")), | |||
| receiptDate: outputDateStringToInputDateString(dnFormProps.watch("receiptDate")), | |||
| itemId: row.itemId, | |||
| itemNo: row.itemNo, | |||
| itemName: row.itemName, | |||
| purchaseOrderId: row.purchaseOrderId, | |||
| // purchaseOrderId: row.purchaseOrderId, | |||
| purchaseOrderLineId: row.id, | |||
| acceptedQty: acceptedQty, | |||
| productLotNo: polInputList[rowIndex].lotNo || '', | |||
| @@ -765,11 +764,11 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse, printerCombo }) => { | |||
| > | |||
| <Controller | |||
| control={dnFormProps.control} | |||
| name="dnDate" | |||
| name="receiptDate" | |||
| render={({ field }) => ( | |||
| <DatePicker | |||
| label={t("dnDate")} | |||
| label={t("receiptDate")} | |||
| format={`${OUTPUT_DATE_FORMAT}`} | |||
| defaultValue={dateStringToDayjs(field.value)} | |||
| onChange={(newValue: Dayjs | null) => { | |||
| @@ -854,7 +853,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse, printerCombo }) => { | |||
| <TableCell align="right"> | |||
| <Box> | |||
| <PoInputGrid | |||
| qc={qc} | |||
| // qc={qc} | |||
| setRows={setRows} | |||
| stockInLine={stockInLine} | |||
| setStockInLine={setStockInLine} | |||
| @@ -24,18 +24,16 @@ type Props = { | |||
| const PoDetailWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | |||
| const [ | |||
| poWithStockInLine, | |||
| warehouse, | |||
| qc, | |||
| warehouse, | |||
| printerCombo, | |||
| ] = await Promise.all([ | |||
| fetchPoWithStockInLines(id), | |||
| fetchWarehouseList(), | |||
| fetchQcItemCheck(), | |||
| fetchPrinterCombo(), | |||
| ]); | |||
| // const poWithStockInLine = await fetchPoWithStockInLines(id) | |||
| console.log("%c pol:", "color:green", poWithStockInLine); | |||
| return <PoDetail po={poWithStockInLine} qc={qc} warehouse={warehouse} printerCombo={printerCombo} />; | |||
| return <PoDetail po={poWithStockInLine} warehouse={warehouse} printerCombo={printerCombo} />; | |||
| }; | |||
| PoDetailWrapper.Loading = PoDetailLoading; | |||
| @@ -31,10 +31,11 @@ import DeleteIcon from "@mui/icons-material/Delete"; | |||
| import CancelIcon from "@mui/icons-material/Cancel"; | |||
| import FactCheckIcon from "@mui/icons-material/FactCheck"; | |||
| import ShoppingCartIcon from "@mui/icons-material/ShoppingCart"; | |||
| import { QcItemWithChecks } from "src/app/api/qc"; | |||
| // import { QcItemWithChecks } from "src/app/api/qc"; | |||
| import PlayArrowIcon from "@mui/icons-material/PlayArrow"; | |||
| import { PurchaseOrderLine, StockInLine } from "@/app/api/po"; | |||
| import { createStockInLine, PurchaseQcResult } from "@/app/api/po/actions"; | |||
| import { PurchaseOrderLine } from "@/app/api/po"; | |||
| import { StockInLine } from "@/app/api/stockIn"; | |||
| import { createStockInLine, PurchaseQcResult } from "@/app/api/stockIn/actions"; | |||
| import { usePathname, useRouter, useSearchParams } from "next/navigation"; | |||
| import { | |||
| returnWeightUnit, | |||
| @@ -73,7 +74,7 @@ interface ResultWithId { | |||
| } | |||
| interface Props { | |||
| qc: QcItemWithChecks[]; | |||
| // qc: QcItemWithChecks[]; | |||
| setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>; | |||
| setStockInLine: Dispatch<SetStateAction<StockInLine[]>>; | |||
| setProcessedQty: Dispatch<SetStateAction<number>>; | |||
| @@ -114,7 +115,7 @@ class ProcessRowUpdateError extends Error { | |||
| } | |||
| function PoInputGrid({ | |||
| qc, | |||
| // qc, | |||
| setRows, | |||
| setStockInLine, | |||
| setProcessedQty, | |||
| @@ -136,7 +137,7 @@ function PoInputGrid({ | |||
| const [entries, setEntries] = useState<StockInLineRow[]>(stockInLine || []); | |||
| useEffect(() => { | |||
| setEntries(stockInLine) | |||
| setEntries(stockInLine); | |||
| }, [stockInLine]) | |||
| const [modalInfo, setModalInfo] = useState< | |||
| StockInLine & { qcResult?: PurchaseQcResult[] } & { escalationResult?: EscalationResult[] } | |||
| @@ -215,7 +216,7 @@ function PoInputGrid({ | |||
| setRejectOpen(true); | |||
| }, []); | |||
| const handleStart = useCallback( | |||
| const handleStart = useCallback( // NOTE: Seems unused!!!!!!!! | |||
| (id: GridRowId, params: any) => () => { | |||
| setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| @@ -229,7 +230,7 @@ function PoInputGrid({ | |||
| itemId: params.row.itemId, | |||
| itemNo: params.row.itemNo, | |||
| itemName: params.row.itemName, | |||
| purchaseOrderId: params.row.purchaseOrderId, | |||
| // purchaseOrderId: params.row.purchaseOrderId, | |||
| purchaseOrderLineId: params.row.purchaseOrderLineId, | |||
| acceptedQty: params.row.acceptedQty, | |||
| }; | |||
| @@ -263,7 +264,6 @@ function PoInputGrid({ | |||
| [id]: { mode: GridRowModes.View }, | |||
| })); | |||
| const qcResult = await fetchQcDefaultValue(id); | |||
| const escResult = await fetchEscalationLogsByStockInLines([Number(id)]); | |||
| // console.log(params.row); | |||
| console.log("Fetched QC Result:", qcResult); | |||
| @@ -291,9 +291,9 @@ const closeNewModal = useCallback(() => { | |||
| newParams.delete("stockInLineId"); // Remove the parameter | |||
| router.replace(`${pathname}?${newParams.toString()}`); | |||
| fetchPoDetail(itemDetail.purchaseOrderId.toString()); | |||
| setTimeout(() => { | |||
| setNewOpen(false); // Close the modal first | |||
| }, 300); // Add a delay to avoid immediate re-trigger of useEffect | |||
| setNewOpen(false); // Close the modal first | |||
| // setTimeout(() => { | |||
| // }, 300); // Add a delay to avoid immediate re-trigger of useEffect | |||
| }, [searchParams, pathname, router]); | |||
| // Open modal | |||
| @@ -304,31 +304,27 @@ const closeNewModal = useCallback(() => { | |||
| // Button handler to update the URL and open the modal | |||
| const handleNewQC = useCallback( | |||
| (id: GridRowId, params: any) => async() => { | |||
| // console.log(id) | |||
| // setBtnIsLoading(true); | |||
| setRowModesModel((prev) => ({ | |||
| ...prev, | |||
| [id]: { mode: GridRowModes.View }, | |||
| })); | |||
| const qcResult = await fetchQcDefaultValue(id); | |||
| const escResult = await fetchEscalationLogsByStockInLines([Number(id)]); | |||
| // const qcResult = await fetchQcDefaultValue(id); | |||
| // const escResult = await fetchEscalationLogsByStockInLines([Number(id)]); | |||
| setModalInfo(() => ({ | |||
| ...params.row, | |||
| qcResult: qcResult, | |||
| escResult: escResult, | |||
| // qcResult: qcResult, | |||
| // escResult: escResult, | |||
| receivedQty: itemDetail.receivedQty, | |||
| })); | |||
| setTimeout(() => { | |||
| const newParams = new URLSearchParams(searchParams.toString()); | |||
| newParams.set("stockInLineId", id.toString()); // Ensure `set` to avoid duplicates | |||
| router.replace(`${pathname}?${newParams.toString()}`); | |||
| // console.log("hello") | |||
| openNewModal() | |||
| // setBtnIsLoading(false); | |||
| }, 200); | |||
| const newParams = new URLSearchParams(searchParams.toString()); | |||
| newParams.set("stockInLineId", id.toString()); // Ensure `set` to avoid duplicates | |||
| router.replace(`${pathname}?${newParams.toString()}`); | |||
| openNewModal() | |||
| // setTimeout(() => { | |||
| // }, 200); | |||
| }, | |||
| [fetchQcDefaultValue, openNewModal, pathname, router, searchParams] | |||
| ); | |||
| @@ -337,7 +333,6 @@ const closeNewModal = useCallback(() => { | |||
| const [firstCheckForSil, setFirstCheckForSil] = useState(false) | |||
| useEffect(() => { | |||
| if (stockInLineId && itemDetail && !firstCheckForSil) { | |||
| // console.log("heeloo") | |||
| // console.log(stockInLineId) | |||
| // console.log(apiRef.current.getRow(stockInLineId)) | |||
| setFirstCheckForSil(true) | |||
| @@ -492,8 +487,8 @@ const closeNewModal = useCallback(() => { | |||
| // flex: 0.4, | |||
| }, | |||
| { | |||
| field: "dnDate", | |||
| headerName: t("dnDate"), | |||
| field: "receiptDate", | |||
| headerName: t("receiptDate"), | |||
| width: 125, | |||
| renderCell: (params) => { | |||
| // console.log(params.row) | |||
| @@ -952,100 +947,19 @@ const closeNewModal = useCallback(() => { | |||
| footer: { child: footer }, | |||
| }} | |||
| /> | |||
| {modalInfo !== undefined && ( | |||
| {/* {modalInfo !== undefined && ( */} | |||
| <> | |||
| <QcStockInModal | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| session={sessionToken} | |||
| qc={qc} | |||
| warehouse={warehouse} | |||
| open={newOpen} | |||
| onClose={closeNewModal} | |||
| itemDetail={modalInfo} | |||
| handleMailTemplateForStockInLine={handleMailTemplateForStockInLine} | |||
| // itemDetail={modalInfo} | |||
| inputDetail={modalInfo} | |||
| printerCombo={printerCombo} | |||
| /> | |||
| </> | |||
| ) | |||
| } | |||
| {/* {modalInfo !== undefined && ( | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"qc"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| qc={qc} | |||
| open={qcOpen} | |||
| onClose={closeQcModal} | |||
| itemDetail={modalInfo} | |||
| /> | |||
| </> | |||
| )} | |||
| {modalInfo !== undefined && ( | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"escalation"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| // qc={qc} | |||
| open={escalOpen} | |||
| onClose={closeEscalationModal} | |||
| itemDetail={modalInfo} | |||
| /> | |||
| </> | |||
| )} | |||
| {modalInfo !== undefined && ( | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"reject"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| // qc={qc} | |||
| open={rejectOpen} | |||
| onClose={closeRejectModal} | |||
| itemDetail={modalInfo} | |||
| /> | |||
| </> | |||
| )} | |||
| {modalInfo !== undefined && ( | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"stockIn"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| // qc={qc} | |||
| setItemDetail={setModalInfo} | |||
| open={stockInOpen} | |||
| onClose={closeStockInModal} | |||
| itemDetail={modalInfo} | |||
| /> | |||
| </> | |||
| )} | |||
| {modalInfo !== undefined && ( | |||
| <> | |||
| <PoQcStockInModal | |||
| type={"putaway"} | |||
| // setRows={setRows} | |||
| setEntries={setEntries} | |||
| setStockInLine={setStockInLine} | |||
| setItemDetail={setModalInfo} | |||
| open={putAwayOpen} | |||
| warehouse={warehouse} | |||
| onClose={closePutAwayModal} | |||
| itemDetail={modalInfo} | |||
| /> | |||
| </> | |||
| )} */} | |||
| {/* ) | |||
| } */} | |||
| </> | |||
| ); | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| "use client"; | |||
| import { PurchaseQcResult, PutAwayInput, PutAwayLine } from "@/app/api/po/actions"; | |||
| import { PurchaseQcResult, PutAwayInput, PutAwayLine } from "@/app/api/stockIn/actions"; | |||
| import { | |||
| Autocomplete, | |||
| Box, | |||
| @@ -36,7 +36,7 @@ import TwoLineCell from "./TwoLineCell"; | |||
| import QcSelect from "./QcSelect"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { StockInLine } from "@/app/api/stockIn"; | |||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||
| import { | |||
| arrayToDateTimeString, | |||
| @@ -58,7 +58,7 @@ dayjs.extend(arraySupport); | |||
| interface Props { | |||
| itemDetail: StockInLine; | |||
| warehouse: WarehouseResult[]; | |||
| warehouse?: WarehouseResult[]; | |||
| disabled: boolean; | |||
| // qc: QcItemWithChecks[]; | |||
| setRowModesModel: Dispatch<SetStateAction<GridRowModesModel>>; | |||
| @@ -84,7 +84,7 @@ const style = { | |||
| width: "auto", | |||
| }; | |||
| const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowModesModel, setRowSelectionModel }) => { | |||
| const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse=[], disabled, setRowModesModel, setRowSelectionModel }) => { | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const apiRef = useGridApiRef(); | |||
| const { | |||
| @@ -100,7 +100,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowM | |||
| clearErrors, | |||
| } = useFormContext<PutAwayInput>(); | |||
| // const [recordQty, setRecordQty] = useState(0); | |||
| const [warehouseId, setWarehouseId] = useState(itemDetail.defaultWarehouseId); | |||
| const [warehouseId, setWarehouseId] = useState(itemDetail.defaultWarehouseId ?? 1); | |||
| const filteredWarehouse = useMemo(() => { | |||
| // do filtering here if any | |||
| return warehouse; | |||
| @@ -113,11 +113,11 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowM | |||
| }; | |||
| const options = useMemo(() => { | |||
| return [ | |||
| // { | |||
| // value: 0, // think think sin | |||
| // label: t("Select warehouse"), | |||
| // group: "default", | |||
| // }, | |||
| { | |||
| value: 1, | |||
| label: t("W001 - 憶兆 3樓A倉"), | |||
| group: "default", | |||
| }, | |||
| ...filteredWarehouse.map((w) => ({ | |||
| value: w.id, | |||
| label: `${w.code} - ${w.name}`, | |||
| @@ -175,32 +175,10 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowM | |||
| // validateForm(); | |||
| // }, [validateForm]); | |||
| const qrContent = useMemo( | |||
| () => ({ | |||
| stockInLineId: itemDetail.id, | |||
| itemId: itemDetail.itemId, | |||
| lotNo: itemDetail.lotNo, | |||
| // warehouseId: 2 // for testing | |||
| // expiryDate: itemDetail.expiryDate, | |||
| // productionDate: itemDetail.productionDate, | |||
| // supplier: itemDetail.supplier, | |||
| // poCode: itemDetail.poCode, | |||
| }), | |||
| [itemDetail], | |||
| ); | |||
| const [isOpenScanner, setOpenScanner] = useState(false); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| setOpenScanner(false); | |||
| }, | |||
| [], | |||
| ); | |||
| useEffect(() => { | |||
| setValue("status", "received"); | |||
| // setValue("status", "completed"); | |||
| setValue("warehouseId", options[0].value); //TODO: save all warehouse entry? | |||
| // setValue("warehouseId", options[0].value); //TODO: save all warehouse entry? | |||
| }, []); | |||
| useEffect(() => { | |||
| @@ -342,7 +320,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowM | |||
| const defaultMaxQty = Number(itemDetail.demandQty?? itemDetail.acceptedQty)//watch("acceptedQty") | |||
| - watch("putAwayLines").reduce((acc, cur) => acc + cur.qty, 0) | |||
| const defaultWarehouseId = itemDetail.defaultWarehouseId ?? 1 | |||
| const defaultWarehouse = options.find((o) => o.value === defaultWarehouseId)?.label | |||
| const defaultWarehouse = "W001 - 憶兆 3樓A倉"//options.find((o) => o.value === defaultWarehouseId)?.label | |||
| return {qty: defaultMaxQty, warehouseId: defaultWarehouseId, warehouse: defaultWarehouse, printQty: 1, _isNew: true } as Partial<PutAwayLine> | |||
| }, []) | |||
| @@ -360,27 +338,27 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowM | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Grid item xs={12}> | |||
| {/* <Grid item xs={6}> | |||
| <TextField | |||
| label={t("LotNo")} | |||
| label={t("Supplier")} | |||
| fullWidth | |||
| value={itemDetail.lotNo} | |||
| value={itemDetail.supplier} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| </Grid> */} | |||
| {/* <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Supplier")} | |||
| label={t("Po Code")} | |||
| fullWidth | |||
| value={itemDetail.supplier} | |||
| value={itemDetail.poCode} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| </Grid> */} | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("Po Code")} | |||
| label={t("itemNo")} | |||
| fullWidth | |||
| value={itemDetail.poCode} | |||
| value={itemDetail.itemNo} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| @@ -394,44 +372,52 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowM | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("itemNo")} | |||
| label={t("stockLotNo")} | |||
| fullWidth | |||
| value={itemDetail.itemNo} | |||
| value={itemDetail.lotNo} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("qty")} | |||
| label={t("expiryDate")} | |||
| fullWidth | |||
| value={ | |||
| // dayjs(itemDetail.expiryDate) | |||
| dayjs() | |||
| .add(20, "day") | |||
| .format(OUTPUT_DATE_FORMAT)} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| value={itemDetail.acceptedQty} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <Grid item xs={3}> | |||
| <TextField | |||
| label={t("productionDate")} | |||
| label={t("uom")} | |||
| fullWidth | |||
| value={ | |||
| // dayjs(itemDetail.productionDate) | |||
| dayjs() | |||
| // .add(-1, "month") | |||
| .format(OUTPUT_DATE_FORMAT)} | |||
| value={itemDetail.uom?.udfudesc} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| {/* <Grid item xs={6}> | |||
| <TextField | |||
| label={t("expiryDate")} | |||
| label={t("productionDate")} | |||
| fullWidth | |||
| value={ | |||
| // dayjs(itemDetail.expiryDate) | |||
| // dayjs(itemDetail.productionDate) | |||
| dayjs() | |||
| .add(20, "day") | |||
| // .add(-1, "month") | |||
| .format(OUTPUT_DATE_FORMAT)} | |||
| disabled | |||
| /> | |||
| </Grid> | |||
| </Grid> */} | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <Autocomplete | |||
| @@ -530,7 +516,7 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowM | |||
| <Grid | |||
| item | |||
| xs={12} | |||
| style={{ display: "flex", justifyContent: "center" }} | |||
| style={{ display: "flex", justifyContent: "center", marginTop:5 }} | |||
| > | |||
| {/* <QrCode content={qrContent} sx={{ width: 200, height: 200 }} /> */} | |||
| <InputDataGrid<PutAwayInput, PutAwayLine, EntryError> | |||
| @@ -548,24 +534,6 @@ const PutAwayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled, setRowM | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| {/* <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| sx={{ mt: 0.5 }} | |||
| > | |||
| <Button onClick={onOpenScanner}>bind</Button> | |||
| </Grid> */} | |||
| <Modal open={isOpenScanner} onClose={closeHandler}> | |||
| <Box sx={style}> | |||
| <Typography variant="h4"> | |||
| {t("Please scan warehouse qr code.")} | |||
| </Typography> | |||
| {/* <ReactQrCodeScanner scannerConfig={scannerConfig} /> */} | |||
| </Box> | |||
| </Modal> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| @@ -1,6 +1,6 @@ | |||
| "use client"; | |||
| import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/po/actions"; | |||
| import { PurchaseQcResult, PurchaseQCInput } from "@/app/api/stockIn/actions"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| @@ -39,7 +39,7 @@ import { TableRow } from "../InputDataGrid/InputDataGrid"; | |||
| import TwoLineCell from "./TwoLineCell"; | |||
| import QcSelect from "./QcSelect"; | |||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { ModalFormInput, StockInLine } from "@/app/api/stockIn"; | |||
| import { stockInLineStatusMap } from "@/app/utils/formatUtil"; | |||
| import { fetchQcItemCheck, fetchQcResult } from "@/app/api/qc/actions"; | |||
| import { QcItemWithChecks, QcData } from "@/app/api/qc"; | |||
| @@ -49,16 +49,17 @@ import axiosInstance from "@/app/(main)/axios/axiosInstance"; | |||
| import EscalationComponent from "./EscalationComponent"; | |||
| import QcDataGrid from "./QCDatagrid"; | |||
| import { dummyEscalationHistory, dummyQCData } from "./dummyQcTemplate"; | |||
| import { ModalFormInput } from "@/app/api/po/actions"; | |||
| import { escape, min } from "lodash"; | |||
| import { PanoramaSharp } from "@mui/icons-material"; | |||
| import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; | |||
| import { EscalationResult } from "@/app/api/escalation"; | |||
| import { EscalationCombo } from "@/app/api/user"; | |||
| import CollapsibleCard from "../CollapsibleCard/CollapsibleCard"; | |||
| import LoadingComponent from "../General/LoadingComponent"; | |||
| interface Props { | |||
| itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| itemDetail: StockInLine; | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| // qc: QcItemWithChecks[]; | |||
| disabled: boolean; | |||
| // qcItems: QcData[] | |||
| @@ -97,7 +98,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| // const qcResult = useMemo(() => [...watch("qcResult")], [watch("qcResult")]); | |||
| const qcRecord = useMemo(() => { // Need testing | |||
| const value = watch('qcResult'); console.log("%c QC update!", "color:green", value); | |||
| const value = watch('qcResult'); //console.log("%c QC update!", "color:green", value); | |||
| return Array.isArray(value) ? [...value] : []; | |||
| }, [watch('qcResult')]); | |||
| const [qcHistory, setQcHistory] = useState<PurchaseQcResult[]>([]); | |||
| @@ -168,7 +169,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| if (validateFieldFail("acceptQty", isNaN(accQty), t("value must be a number"))) return; | |||
| } | |||
| const qcResultItems = qcResult; console.log("Validating:", qcResultItems); | |||
| const qcResultItems = qcResult; //console.log("Validating:", qcResultItems); | |||
| // Check if failed items have failed quantity | |||
| const failedItemsWithoutQty = qcResultItems.filter(item => | |||
| item.qcPassed === false && (!item.failQty || item.failQty <= 0) | |||
| @@ -194,21 +195,6 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| validateForm(); | |||
| }, [clearErrors, validateForm]); | |||
| const columns = useMemo<GridColDef[]>( | |||
| () => [ | |||
| { | |||
| field: "escalation", | |||
| headerName: t("escalation"), | |||
| flex: 1, | |||
| }, | |||
| { | |||
| field: "supervisor", | |||
| headerName: t("supervisor"), | |||
| flex: 1, | |||
| }, | |||
| ], | |||
| [], | |||
| ); | |||
| /// validate datagrid | |||
| const validation = useCallback( | |||
| (newRow: GridRowModel<QcRow>): EntryError => { | |||
| @@ -220,16 +206,16 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| ); | |||
| function BooleanEditCell(params: GridRenderEditCellParams) { | |||
| const apiRef = useGridApiContext(); | |||
| const { id, field, value } = params; | |||
| const apiRef = useGridApiContext(); | |||
| const { id, field, value } = params; | |||
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||
| apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); | |||
| apiRef.current.stopCellEditMode({ id, field }); // commit immediately | |||
| }; | |||
| const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { | |||
| apiRef.current.setEditCellValue({ id, field, value: e.target.checked }); | |||
| apiRef.current.stopCellEditMode({ id, field }); // commit immediately | |||
| }; | |||
| return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | |||
| } | |||
| return <Checkbox checked={!!value} onChange={handleChange} sx={{ p: 0 }} />; | |||
| } | |||
| const qcDisabled = (row : PurchaseQcResult) => { | |||
| return disabled || isExist(row.escalationLogId); | |||
| @@ -396,12 +382,12 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| }, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]); | |||
| useEffect(() => { | |||
| console.log("%c Qc Record updated:", "color:red", qcRecord); | |||
| console.log("%c Qc Record updated:", "color:green", qcRecord); | |||
| if (qcRecord.length < 1) { // New QC | |||
| const fetchedQcData = dummyQCData; //TODO fetch from DB | |||
| setValue("qcResult", fetchedQcData); | |||
| } else { | |||
| if (itemDetail.status == "escalated") { // Copy the previous QC data for editing | |||
| if (itemDetail?.status == "escalated") { // Copy the previous QC data for editing | |||
| if (qcRecord.find((qc) => !isExist(qc.escalationLogId)) === undefined) { | |||
| const copiedQcData = qcRecord.map(qc => ({ ...qc, escalationLogId: undefined })); | |||
| const mutableQcData = [...qcRecord, ...copiedQcData]; | |||
| @@ -442,7 +428,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| useEffect(() => { | |||
| console.log("%c QC ItemDetail updated:", "color: gold", itemDetail); | |||
| // console.log("%c QC ItemDetail updated:", "color: gold", itemDetail); | |||
| }, [itemDetail]); | |||
| @@ -482,6 +468,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| return ( | |||
| <> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| {itemDetail ? ( | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| @@ -577,127 +564,129 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| </> | |||
| )} | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Qc Decision")} | |||
| </Typography> | |||
| <FormControl> | |||
| <Controller | |||
| name="qcDecision" | |||
| // name="qcAccept" | |||
| control={control} | |||
| defaultValue={setDefaultQcDecision(itemDetail?.status)} | |||
| // defaultValue={true} | |||
| render={({ field }) => ( | |||
| <> | |||
| {/* <Typography sx={{color:"red"}}> | |||
| {errors.qcDecision?.message} | |||
| </Typography> */} | |||
| <RadioGroup | |||
| row | |||
| aria-labelledby="demo-radio-buttons-group-label" | |||
| {...field} | |||
| value={field.value} | |||
| // value={field.value?.toString() || "true"} | |||
| onChange={(e) => { | |||
| const value = e.target.value.toString();// === 'true'; | |||
| const input = document.getElementById('accQty') as HTMLInputElement; //TODO improve | |||
| console.log("%c AccQty Error", "color:pink", errors.acceptQty); | |||
| if (input) { // Selected Reject in new flow with Error | |||
| if (value == "1") { // Selected Accept | |||
| input.value = Number(accQty).toString(); | |||
| } else { | |||
| if (Boolean(errors.acceptQty)) { | |||
| setValue("acceptQty", 0); | |||
| } | |||
| input.value = '0'; | |||
| } | |||
| } | |||
| // setValue("acceptQty", itemDetail.acceptedQty ?? 0); | |||
| // clearErrors("acceptQty"); | |||
| // } | |||
| field.onChange(value); | |||
| }} | |||
| > | |||
| <FormControlLabel disabled={disabled} | |||
| value="1" control={<Radio />} label="接受來貨" /> | |||
| {(itemDetail.status == "escalated"|| (disabled && accQty != itemDetail.acceptedQty && qcDecision == 1)) && ( //TODO Improve | |||
| <Box sx={{mr:2}}> | |||
| <TextField | |||
| // type="number" | |||
| id="accQty" | |||
| label={t("acceptQty")} | |||
| sx={{ width: '150px' }} | |||
| // value={Number(accQty)} | |||
| defaultValue={Number(accQty)} | |||
| // defaultValue={(qcDecision == 1)? Number(accQty) : 0} | |||
| // value={(qcDecision == 1)? Number(accQty) : undefined } | |||
| // value={qcAccept? accQty : 0 } | |||
| disabled={qcDecision != 1 || disabled} | |||
| // disabled={!qcAccept || disabled} | |||
| onBlur={(e) => { | |||
| const value = e.target.value; | |||
| const input = document.getElementById('accQty') as HTMLInputElement; | |||
| input.value = Number(value).toString() | |||
| setValue(`acceptQty`, Number(value)); | |||
| }} | |||
| onInput={(e: React.ChangeEvent<HTMLInputElement>) => { | |||
| const input = e.target.value; | |||
| const numReg = /^[0-9]+$/ | |||
| let r = ''; | |||
| if (!numReg.test(input)) { | |||
| const result = input.replace(/\D/g, ""); | |||
| r = (result === '' ? result : Number(result)).toString(); | |||
| <Card sx={{p:2}}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Qc Decision")} | |||
| </Typography> | |||
| <FormControl> | |||
| <Controller | |||
| name="qcDecision" | |||
| // name="qcAccept" | |||
| control={control} | |||
| defaultValue={setDefaultQcDecision(itemDetail?.status)} | |||
| // defaultValue={true} | |||
| render={({ field }) => ( | |||
| <> | |||
| {/* <Typography sx={{color:"red"}}> | |||
| {errors.qcDecision?.message} | |||
| </Typography> */} | |||
| <RadioGroup | |||
| row | |||
| aria-labelledby="demo-radio-buttons-group-label" | |||
| {...field} | |||
| value={field.value} | |||
| // value={field.value?.toString() || "true"} | |||
| onChange={(e) => { | |||
| const value = e.target.value.toString();// === 'true'; | |||
| const input = document.getElementById('accQty') as HTMLInputElement; //TODO improve | |||
| // console.log("%c AccQty Error", "color:red", errors.acceptQty); | |||
| if (input) { // Selected Reject in new flow with Error | |||
| if (value == "1") { // Selected Accept | |||
| input.value = Number(accQty).toString(); | |||
| } else { | |||
| r = Number(input).toString() | |||
| if (Boolean(errors.acceptQty)) { | |||
| setValue("acceptQty", 0); | |||
| } | |||
| input.value = '0'; | |||
| } | |||
| e.target.value = r; | |||
| }} | |||
| inputProps={{ min: 1, max:itemDetail.acceptedQty }} | |||
| // onChange={(e) => { | |||
| // const inputValue = e.target.value; | |||
| // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) { | |||
| // setValue("acceptQty", Number(inputValue === '' ? null : parseInt(inputValue, 10))); | |||
| // } | |||
| // }} | |||
| // {...register("acceptQty", { | |||
| // required: "acceptQty required!", | |||
| // })} | |||
| error={Boolean(errors.acceptQty)} | |||
| helperText={errors.acceptQty?.message} | |||
| /> | |||
| <TextField | |||
| type="number" | |||
| label={t("rejectQty")} | |||
| sx={{ width: '150px' }} | |||
| value={ | |||
| (!Boolean(errors.acceptQty)) ? | |||
| (qcDecision == 1 ? itemDetail.acceptedQty - accQty : itemDetail.acceptedQty) | |||
| : "" | |||
| } | |||
| error={Boolean(errors.acceptQty)} | |||
| disabled={true} | |||
| /> | |||
| </Box>)} | |||
| <FormControlLabel disabled={disabled} | |||
| value="2" control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "red"}}} | |||
| label= {itemDetail.status == "escalated" ? "全部拒絕並退貨" : "不接受並退貨"} /> | |||
| // setValue("acceptQty", itemDetail.acceptedQty ?? 0); | |||
| // clearErrors("acceptQty"); | |||
| // } | |||
| field.onChange(value); | |||
| }} | |||
| > | |||
| <FormControlLabel disabled={disabled} | |||
| value="1" control={<Radio />} label="接受來貨" /> | |||
| {(itemDetail.status == "pending" || disabled) && (<> | |||
| <FormControlLabel disabled={disabled} | |||
| value="3" control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "blue"}}} | |||
| label="上報品檢結果" /> | |||
| </>)} | |||
| </RadioGroup> | |||
| </> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| {(itemDetail.status == "escalated"|| (disabled && accQty != itemDetail.acceptedQty && qcDecision == 1)) && ( //TODO Improve | |||
| <Box sx={{mr:2}}> | |||
| <TextField | |||
| // type="number" | |||
| id="accQty" | |||
| label={t("acceptQty")} | |||
| sx={{ width: '150px' }} | |||
| // value={Number(accQty)} | |||
| defaultValue={Number(accQty)} | |||
| // defaultValue={(qcDecision == 1)? Number(accQty) : 0} | |||
| // value={(qcDecision == 1)? Number(accQty) : undefined } | |||
| // value={qcAccept? accQty : 0 } | |||
| disabled={qcDecision != 1 || disabled} | |||
| // disabled={!qcAccept || disabled} | |||
| onBlur={(e) => { | |||
| const value = e.target.value; | |||
| const input = document.getElementById('accQty') as HTMLInputElement; | |||
| input.value = Number(value).toString() | |||
| setValue(`acceptQty`, Number(value)); | |||
| }} | |||
| onInput={(e: React.ChangeEvent<HTMLInputElement>) => { | |||
| const input = e.target.value; | |||
| const numReg = /^[0-9]+$/ | |||
| let r = ''; | |||
| if (!numReg.test(input)) { | |||
| const result = input.replace(/\D/g, ""); | |||
| r = (result === '' ? result : Number(result)).toString(); | |||
| } else { | |||
| r = Number(input).toString() | |||
| } | |||
| e.target.value = r; | |||
| }} | |||
| inputProps={{ min: 1, max:itemDetail.acceptedQty }} | |||
| // onChange={(e) => { | |||
| // const inputValue = e.target.value; | |||
| // if (inputValue === '' || /^[0-9]*$/.test(inputValue)) { | |||
| // setValue("acceptQty", Number(inputValue === '' ? null : parseInt(inputValue, 10))); | |||
| // } | |||
| // }} | |||
| // {...register("acceptQty", { | |||
| // required: "acceptQty required!", | |||
| // })} | |||
| error={Boolean(errors.acceptQty)} | |||
| helperText={errors.acceptQty?.message} | |||
| /> | |||
| <TextField | |||
| type="number" | |||
| label={t("rejectQty")} | |||
| sx={{ width: '150px' }} | |||
| value={ | |||
| (!Boolean(errors.acceptQty)) ? | |||
| (qcDecision == 1 ? itemDetail.acceptedQty - accQty : itemDetail.acceptedQty) | |||
| : "" | |||
| } | |||
| error={Boolean(errors.acceptQty)} | |||
| disabled={true} | |||
| /> | |||
| </Box>)} | |||
| <FormControlLabel disabled={disabled} | |||
| value="2" control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "red"}}} | |||
| label= {itemDetail.status == "escalated" ? "全部拒絕並退貨" : "不接受並退貨"} /> | |||
| {(itemDetail.status == "pending" || disabled) && (<> | |||
| <FormControlLabel disabled={disabled} | |||
| value="3" control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "blue"}}} | |||
| label="上報品檢結果" /> | |||
| </>)} | |||
| </RadioGroup> | |||
| </> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Card> | |||
| </Grid> | |||
| {qcDecision == 3 && ( | |||
| // {!qcAccept && ( | |||
| @@ -721,6 +710,7 @@ const QcComponent: React.FC<Props> = ({ itemDetail, disabled = false }) => { | |||
| /> | |||
| </Grid>} */} | |||
| </Grid> | |||
| ) : <LoadingComponent/>} | |||
| </Grid> | |||
| </> | |||
| ); | |||
| @@ -1,6 +1,4 @@ | |||
| "use client"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { ModalFormInput, PurchaseQcResult, StockInLineEntry, updateStockInLine, PurchaseQCInput, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/po/actions"; | |||
| import { QcItemWithChecks, QcData } from "@/app/api/qc"; | |||
| import { | |||
| Autocomplete, | |||
| @@ -38,6 +36,12 @@ import { GridRowModesModel } from "@mui/x-data-grid"; | |||
| import { isEmpty } from "lodash"; | |||
| import { EscalationCombo } from "@/app/api/user"; | |||
| import { truncateSync } from "fs"; | |||
| import { ModalFormInput, StockInLineInput, StockInLine } from "@/app/api/stockIn"; | |||
| import { PurchaseQcResult, StockInLineEntry, updateStockInLine, printQrCodeForSil, PrintQrCodeForSilRequest } from "@/app/api/stockIn/actions"; | |||
| import { fetchStockInLineInfo } from "@/app/api/stockIn/actions"; | |||
| import { fetchQcResult } from "@/app/api/qc/actions"; | |||
| import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions"; | |||
| import LoadingComponent from "../General/LoadingComponent"; | |||
| const style = { | |||
| @@ -54,42 +58,23 @@ const style = { | |||
| height: { xs: "90%", sm: "90%", md: "90%" }, | |||
| }; | |||
| interface CommonProps extends Omit<ModalProps, "children"> { | |||
| // setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>; | |||
| setEntries?: Dispatch<SetStateAction<StockInLineRow[]>>; | |||
| setStockInLine?: Dispatch<SetStateAction<StockInLine[]>>; | |||
| itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| setItemDetail: Dispatch< | |||
| SetStateAction< | |||
| | (StockInLine & { | |||
| warehouseId?: number; | |||
| }) | |||
| | undefined | |||
| > | |||
| >; | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] } | undefined; | |||
| inputDetail: StockInLineInput | undefined; | |||
| session: SessionWithTokens | null; | |||
| qc?: QcItemWithChecks[]; | |||
| warehouse?: any[]; | |||
| // type: "qc" | "stockIn" | "escalation" | "putaway" | "reject"; | |||
| handleMailTemplateForStockInLine: (stockInLineId: number) => void; | |||
| printerCombo: PrinterCombo[]; | |||
| onClose: () => void; | |||
| } | |||
| interface Props extends CommonProps { | |||
| itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| // itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
| } | |||
| const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
| // type, | |||
| // setRows, | |||
| setEntries, | |||
| setStockInLine, | |||
| open, | |||
| onClose, | |||
| itemDetail, | |||
| setItemDetail, | |||
| // itemDetail, | |||
| inputDetail, | |||
| session, | |||
| qc, | |||
| warehouse, | |||
| handleMailTemplateForStockInLine, | |||
| printerCombo, | |||
| }) => { | |||
| const { | |||
| @@ -97,6 +82,10 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
| i18n: { language }, | |||
| } = useTranslation("purchaseOrder"); | |||
| const [stockInLineInfo, setStockInLineInfo] = useState<StockInLine>(); | |||
| const [isLoading, setIsLoading] = useState<Boolean>(false); | |||
| // const [viewOnly, setViewOnly] = useState(false); | |||
| // Select Printer | |||
| const [selectedPrinter, setSelectedPrinter] = useState(printerCombo[0]); | |||
| const [printQty, setPrintQty] = useState(1); | |||
| @@ -109,28 +98,108 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
| [], | |||
| ); | |||
| const defaultNewValue = useMemo(() => { | |||
| return ( | |||
| { | |||
| ...itemDetail, | |||
| status: itemDetail.status ?? "pending", | |||
| dnDate: arrayToDateString(itemDetail.dnDate, "input")?? dayjsToInputDateString(dayjs()), | |||
| // putAwayLines: dummyPutAwayLine, | |||
| // putAwayLines: itemDetail.putAwayLines.map((line) => (return {...line, printQty: 1})) ?? [], | |||
| putAwayLines: itemDetail.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false, _disableDelete: true})) ?? [], | |||
| // qcResult: (itemDetail.qcResult && itemDetail.qcResult?.length > 0) ? itemDetail.qcResult : [],//[...dummyQCData], | |||
| escResult: (itemDetail.escResult && itemDetail.escResult?.length > 0) ? itemDetail.escResult : [], | |||
| productionDate: itemDetail.productionDate ? arrayToDateString(itemDetail.productionDate, "input") : undefined, | |||
| expiryDate: itemDetail.expiryDate ? arrayToDateString(itemDetail.expiryDate, "input") : undefined, | |||
| receiptDate: itemDetail.receiptDate ? arrayToDateString(itemDetail.receiptDate, "input") | |||
| : dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | |||
| acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, | |||
| warehouseId: itemDetail.defaultWarehouseId ?? 1, | |||
| const fetchStockInLineData = useCallback( | |||
| async (stockInLineId: number) => { | |||
| try { | |||
| const res = await fetchStockInLineInfo(stockInLineId); | |||
| if (res) { | |||
| console.log("%c Fetched Stock In Line: ", "color:orange", res); | |||
| setStockInLineInfo({...inputDetail, ...res}); | |||
| fetchQcResultData(stockInLineId); | |||
| } else throw("Result is undefined"); | |||
| } catch (e) { | |||
| console.log("%c Error when fetching Stock In Line: ", "color:red", e); | |||
| } | |||
| },[fetchStockInLineInfo, inputDetail] | |||
| ); | |||
| const fetchQcResultData = useCallback( // TODO: put this inside QC Component | |||
| async (stockInLineId: number) => { | |||
| try { | |||
| const res = await fetchQcResult(stockInLineId); | |||
| if (res.length > 0) { | |||
| console.log("%c Fetched Qc Result: ", "color:orange", res); | |||
| setStockInLineInfo((prev) => ({...prev, qcResult: res} as StockInLine)); | |||
| formProps.setValue("qcResult", res); | |||
| fetchEscalationLogData(stockInLineId); | |||
| } else {setStockInLineInfo((prev) => ({...prev, qcResult: []} as StockInLine));} | |||
| // } else throw("Result is undefined"); | |||
| } catch (e) { | |||
| console.log("%c Error when fetching Qc Result: ", "color:red", e); | |||
| } | |||
| },[fetchQcResult] | |||
| ); | |||
| const fetchEscalationLogData = useCallback( | |||
| async (stockInLineId: number) => { | |||
| try { | |||
| const res = await fetchEscalationLogsByStockInLines([stockInLineId]); | |||
| if (res.length > 0) { | |||
| console.log("%c Fetched Escalation Log: ", "color:orange", res[0]); | |||
| setStockInLineInfo((prev) => ({...prev, escResult: res} as StockInLine)); | |||
| // formProps.setValue("escalationLog", res[0]); | |||
| } else throw("Result is undefined"); | |||
| } catch (e) { | |||
| console.log("%c Error when fetching EscalationLog: ", "color:red", e); | |||
| } | |||
| },[fetchEscalationLogsByStockInLines] | |||
| ); | |||
| // Fetch info if id is input | |||
| useEffect(() => { | |||
| setIsLoading(true); | |||
| if (inputDetail && open) { | |||
| console.log("%c Opened Modal with input:", "color:yellow", inputDetail); | |||
| if (inputDetail.id) { | |||
| const id = inputDetail.id; | |||
| fetchStockInLineData(id); | |||
| } | |||
| } | |||
| ) | |||
| },[itemDetail]) | |||
| }, [open]); | |||
| const [qcItems, setQcItems] = useState(dummyQCData) | |||
| // Make sure stock in line info is fetched | |||
| useEffect(() => { | |||
| if (stockInLineInfo) { | |||
| if (stockInLineInfo.id) { | |||
| if (isLoading) { | |||
| formProps.reset({ | |||
| ...defaultNewValue | |||
| }); | |||
| console.log("%c Modal loaded successfully", "color:lime"); | |||
| setIsLoading(false); | |||
| } | |||
| } | |||
| } | |||
| }, [stockInLineInfo]); | |||
| const defaultNewValue = useMemo(() => { | |||
| const d = stockInLineInfo; | |||
| if (d !== undefined) { | |||
| // console.log("%c sil info", "color:yellow", d ) | |||
| return ( | |||
| { | |||
| ...d, | |||
| // status: d.status ?? "pending", | |||
| productionDate: d.productionDate ? arrayToDateString(d.productionDate, "input") : undefined, | |||
| expiryDate: d.expiryDate ? arrayToDateString(d.expiryDate, "input") : undefined, | |||
| receiptDate: d.receiptDate ? arrayToDateString(d.receiptDate, "input") | |||
| : dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | |||
| acceptQty: d.demandQty?? d.acceptedQty, | |||
| // escResult: (d.escResult && d.escResult?.length > 0) ? d.escResult : [], | |||
| // qcResult: (d.qcResult && d.qcResult?.length > 0) ? d.qcResult : [],//[...dummyQCData], | |||
| warehouseId: d.defaultWarehouseId ?? 1, | |||
| putAwayLines: d.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false, _disableDelete: true})) ?? [], | |||
| } as ModalFormInput | |||
| ) | |||
| } return undefined | |||
| }, [stockInLineInfo]) | |||
| // const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const formProps = useForm<ModalFormInput>({ | |||
| defaultValues: { | |||
| ...defaultNewValue, | |||
| @@ -139,41 +208,45 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| () => { | |||
| setStockInLineInfo(undefined); | |||
| formProps.reset({}); | |||
| onClose?.(); | |||
| // reset(); | |||
| }, | |||
| [onClose], | |||
| ); | |||
| const isPutaway = () => { | |||
| if (itemDetail) { | |||
| const status = itemDetail.status; | |||
| if (stockInLineInfo) { | |||
| const status = stockInLineInfo.status; | |||
| return status == "received"; | |||
| } else return false; | |||
| }; | |||
| const [viewOnly, setViewOnly] = useState(false); | |||
| useEffect(() => { | |||
| if (itemDetail && itemDetail.status) { | |||
| const isViewOnly = itemDetail.status.toLowerCase() == "completed" | |||
| || itemDetail.status.toLowerCase() == "partially_completed" // TODO update DB | |||
| || itemDetail.status.toLowerCase() == "rejected" | |||
| || (itemDetail.status.toLowerCase() == "escalated" && session?.id != itemDetail.handlerId) | |||
| setViewOnly(isViewOnly) | |||
| } | |||
| console.log("Modal ItemDetail updated:", itemDetail); | |||
| if (showPutaway) { setTabIndex(1); } else { setTabIndex(0); } | |||
| }, [itemDetail]); | |||
| useEffect(() => { | |||
| formProps.reset({ | |||
| ...defaultNewValue | |||
| }) | |||
| setQcItems(dummyQCData); | |||
| // setOpenPutaway(isPutaway); | |||
| }, [open]) | |||
| // Get show putaway | |||
| const showPutaway = useMemo(() => { | |||
| if (stockInLineInfo) { | |||
| const status = stockInLineInfo.status; | |||
| return status !== "pending" && status !== "escalated" && status !== "rejected"; | |||
| } | |||
| return false; | |||
| }, [stockInLineInfo]); | |||
| // Get is view only | |||
| const viewOnly = useMemo(() => { | |||
| if (stockInLineInfo) { | |||
| if (stockInLineInfo.status) { | |||
| const status = stockInLineInfo.status; | |||
| const isViewOnly = status.toLowerCase() == "completed" | |||
| || status.toLowerCase() == "partially_completed" // TODO update DB | |||
| || status.toLowerCase() == "rejected" | |||
| || (status.toLowerCase() == "escalated" && session?.id != stockInLineInfo.handlerId) | |||
| if (showPutaway) { setTabIndex(1); } else { setTabIndex(0); } | |||
| return isViewOnly; | |||
| } | |||
| } | |||
| return true; | |||
| }, [stockInLineInfo]) | |||
| const [openPutaway, setOpenPutaway] = useState(false); | |||
| const onOpenPutaway = useCallback(() => { | |||
| @@ -269,7 +342,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| alert("請輸入到期日!"); | |||
| return; | |||
| } | |||
| if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && itemDetail.status != "escalated") { //TODO: fix it please! | |||
| if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && stockInLineInfo?.status != "escalated") { //TODO: fix it please! | |||
| validationErrors.push("有不合格檢查項目,無法收貨!"); | |||
| // submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?", | |||
| // confirmButtonText: t("confirm putaway"), html: ""}); | |||
| @@ -279,7 +352,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| // Check if all QC items have results | |||
| const itemsWithoutResult = qcResults.filter(item => item.qcPassed === undefined); | |||
| if (itemsWithoutResult.length > 0 && itemDetail.status != "escalated") { //TODO: fix it please! | |||
| if (itemsWithoutResult.length > 0 && stockInLineInfo?.status != "escalated") { //TODO: fix it please! | |||
| validationErrors.push(`${t("QC items without result")}`); | |||
| // validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`); | |||
| } | |||
| @@ -292,7 +365,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const qcData = { | |||
| dnNo : data.dnNo? data.dnNo : "DN00000", | |||
| dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | |||
| // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | |||
| productionDate : arrayToDateString(data.productionDate, "input"), | |||
| expiryDate : arrayToDateString(data.expiryDate, "input"), | |||
| receiptDate : arrayToDateString(data.receiptDate, "input"), | |||
| @@ -345,69 +418,36 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| return ; | |||
| }, | |||
| [onOpenPutaway, qcItems, formProps.formState.errors], | |||
| [onOpenPutaway, formProps.formState.errors], | |||
| ); | |||
| const postStockInLine = useCallback(async (args: ModalFormInput) => { | |||
| const submitData = { | |||
| ...itemDetail, ...args | |||
| ...stockInLineInfo, ...args | |||
| } as StockInLineEntry & ModalFormInput; | |||
| console.log("Submitting", submitData); | |||
| const res = await updateStockInLine(submitData); | |||
| return res; | |||
| },[itemDetail]) | |||
| // Email supplier handler | |||
| const onSubmitEmailSupplier = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| console.log("Email Supplier Submission:", event!.nativeEvent); | |||
| // Extract only email supplier related fields | |||
| const emailData = { | |||
| // supplierEmail: data.supplierEmail, | |||
| // issueDescription: data.issueDescription, | |||
| // qcComments: data.qcComments, | |||
| // defectNotes: data.defectNotes, | |||
| // attachments: data.attachments, | |||
| // escalationReason: data.escalationReason, | |||
| data: data, | |||
| // Add other email-specific fields | |||
| }; | |||
| console.log("Email Supplier Data:", emailData); | |||
| // Handle email supplier logic here | |||
| // e.g., send email to supplier, log escalation, etc. | |||
| }, | |||
| [], | |||
| ); | |||
| }, [stockInLineInfo]) | |||
| // Put away model | |||
| const [pafRowModesModel, setPafRowModesModel] = useState<GridRowModesModel>({}) | |||
| const [pafRowSelectionModel, setPafRowSelectionModel] = useState<GridRowSelectionModel>([]) | |||
| const pafSubmitDisable = useMemo(() => { | |||
| // console.log("%c mode: ", "background:#90EE90; color:red", Object.entries(pafRowModesModel)) | |||
| // console.log("%c mode: ", "background:pink; color:#87CEEB", Object.entries(pafRowModesModel)) | |||
| return Object.entries(pafRowModesModel).length > 0 || Object.entries(pafRowModesModel).some(([key, value], index) => value.mode === GridRowModes.Edit) | |||
| }, [pafRowModesModel]) | |||
| // Putaway submission handler | |||
| const onSubmitPutaway = useCallback<SubmitHandler<ModalFormInput>>( | |||
| async (data, event) => { | |||
| // console.log("Putaway Submission:", event!.nativeEvent); | |||
| // console.log(data.putAwayLines) | |||
| // console.log(data.putAwayLines?.filter((line) => line._isNew !== false)) | |||
| // Extract only putaway related fields | |||
| const putawayData = { | |||
| // putawayLine: data.putawayLine, | |||
| // putawayLocation: data.putawayLocation, | |||
| // binLocation: data.binLocation, | |||
| // putawayQuantity: data.putawayQuantity, | |||
| // putawayNotes: data.putawayNotes, | |||
| acceptQty: Number(data.acceptQty?? (itemDetail.demandQty?? (itemDetail.acceptedQty))), //TODO improve | |||
| acceptQty: Number(data.acceptQty?? (stockInLineInfo?.demandQty?? (stockInLineInfo?.acceptedQty))), //TODO improve | |||
| warehouseId: data.warehouseId, | |||
| status: data.status, //TODO Fix it! | |||
| // ...data, | |||
| dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | |||
| // dnDate : data.dnDate? arrayToDateString(data.dnDate, "input") : dayjsToInputDateString(dayjs()), | |||
| productionDate : arrayToDateString(data.productionDate, "input"), | |||
| expiryDate : arrayToDateString(data.expiryDate, "input"), | |||
| receiptDate : arrayToDateString(data.receiptDate, "input"), | |||
| @@ -464,7 +504,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| // const printQty = printList.reduce((acc, cur) => acc + cur.printQty, 0) | |||
| // console.log(printQty) | |||
| const data: PrintQrCodeForSilRequest = { | |||
| stockInLineId: itemDetail.id, | |||
| stockInLineId: stockInLineInfo?.id ?? 0, | |||
| printerId: selectedPrinter.id, | |||
| printQty: printQty | |||
| } | |||
| @@ -475,32 +515,22 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| } finally { | |||
| setIsPrinting(() => false) | |||
| } | |||
| }, [itemDetail.id, pafRowSelectionModel, printQty, selectedPrinter]); | |||
| // }, [pafRowSelectionModel, printQty, selectedPrinter]); | |||
| }, [stockInLineInfo?.id, pafRowSelectionModel, printQty, selectedPrinter]); | |||
| const acceptQty = formProps.watch("acceptedQty") | |||
| const showPutaway = useMemo(() => { | |||
| const status = itemDetail.status; | |||
| return status !== "pending" && status !== "escalated" && status !== "rejected"; | |||
| }, [itemDetail]); | |||
| const checkQcIsPassed = useCallback((qcItems: PurchaseQcResult[]) => { | |||
| const isPassed = qcItems.every((qc) => qc.qcPassed); | |||
| console.log(isPassed) | |||
| if (isPassed) { | |||
| formProps.setValue("passingQty", acceptQty) | |||
| } else { | |||
| formProps.setValue("passingQty", 0) | |||
| } | |||
| return isPassed | |||
| }, [acceptQty, formProps]) | |||
| // useEffect(() => { | |||
| // // maybe check if submitted before | |||
| // console.log("Modal QC Items updated:", qcItems); | |||
| // // checkQcIsPassed(qcItems) | |||
| // }, [qcItems, checkQcIsPassed]) | |||
| // const checkQcIsPassed = useCallback((qcItems: PurchaseQcResult[]) => { | |||
| // const isPassed = qcItems.every((qc) => qc.qcPassed); | |||
| // console.log(isPassed) | |||
| // if (isPassed) { | |||
| // formProps.setValue("passingQty", acceptQty) | |||
| // } else { | |||
| // formProps.setValue("passingQty", 0) | |||
| // } | |||
| // return isPassed | |||
| // }, [acceptQty, formProps]) | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| @@ -514,8 +544,11 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| marginLeft: 3, | |||
| marginRight: 3, | |||
| // overflow: "hidden", | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| }} | |||
| > | |||
| {(!isLoading && stockInLineInfo) ? (<> | |||
| <Box sx={{ position: 'sticky', top: 0, bgcolor: 'background.paper', | |||
| zIndex: 5, borderBottom: 2, borderColor: 'divider', width: "100%"}}> | |||
| <Tabs | |||
| @@ -537,124 +570,92 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| sx={{padding: 2}} | |||
| > | |||
| <Grid item xs={12}> | |||
| {tabIndex === 0 && <> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Delivery Detail")} | |||
| </Typography> | |||
| </Grid> | |||
| <StockInForm itemDetail={itemDetail} disabled={viewOnly || showPutaway} /> | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| > | |||
| <QcComponent | |||
| // qc={qc!} | |||
| itemDetail={itemDetail} | |||
| disabled={viewOnly || showPutaway} | |||
| // qcItems={qcItems} | |||
| // setQcItems={setQcItems} | |||
| /> | |||
| </Grid> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| {(!viewOnly && !showPutaway) && (<Button | |||
| id="qcSubmit" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ mt: 1 }} | |||
| onClick={formProps.handleSubmit(onSubmitQc, onSubmitErrorQc)} | |||
| > | |||
| {t("confirm qc result")} | |||
| </Button>)} | |||
| </Stack> | |||
| </>} | |||
| {tabIndex === 0 && | |||
| <Box> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| {t("Delivery Detail")} | |||
| </Typography> | |||
| </Grid> | |||
| <StockInForm itemDetail={stockInLineInfo} disabled={viewOnly || showPutaway} /> | |||
| {stockInLineInfo.qcResult ? | |||
| <QcComponent | |||
| itemDetail={stockInLineInfo} | |||
| disabled={viewOnly || showPutaway} | |||
| /> : <LoadingComponent/> | |||
| } | |||
| <Stack direction="row" justifyContent="flex-end" gap={1} sx={{pt:2}}> | |||
| {(!viewOnly && !showPutaway) && (<Button | |||
| id="Submit" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ mt: 1 }} | |||
| onClick={formProps.handleSubmit(onSubmitQc, onSubmitErrorQc)} | |||
| > | |||
| {t("confirm qc result")} | |||
| </Button>)} | |||
| </Stack> | |||
| </Box> | |||
| } | |||
| {tabIndex === 1 && | |||
| <Box | |||
| // component="form" | |||
| // onSubmit={formProps.handleSubmit(onSubmitPutaway)} | |||
| > | |||
| <Grid | |||
| container | |||
| justifyContent="flex-start" | |||
| alignItems="flex-start" | |||
| spacing={2} | |||
| > | |||
| <Grid item xs={12}> | |||
| <PutAwayForm | |||
| itemDetail={itemDetail} | |||
| warehouse={warehouse!} | |||
| disabled={viewOnly} | |||
| setRowModesModel={setPafRowModesModel} | |||
| setRowSelectionModel={setPafRowSelectionModel} | |||
| /> | |||
| </Grid> | |||
| {/* <PutAwayGrid | |||
| itemDetail={itemDetail} | |||
| warehouse={warehouse!} | |||
| disabled={viewOnly} | |||
| /> */} | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Autocomplete | |||
| disableClearable | |||
| options={printerCombo} | |||
| defaultValue={selectedPrinter} | |||
| onChange={(event, value) => { | |||
| setSelectedPrinter(value) | |||
| }} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| variant="outlined" | |||
| label={t("Printer")} | |||
| sx={{ width: 300}} | |||
| /> | |||
| )} | |||
| /> | |||
| <TextField | |||
| variant="outlined" | |||
| label={t("Print Qty")} | |||
| defaultValue={printQty} | |||
| onChange={(event) => { | |||
| event.target.value = event.target.value.replace(/[^0-9]/g, '') | |||
| setPrintQty(Number(event.target.value)) | |||
| }} | |||
| sx={{ width: 300}} | |||
| /> | |||
| <Button | |||
| id="printButton" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ mt: 1 }} | |||
| onClick={handlePrint} | |||
| disabled={isPrinting || printerCombo.length <= 0 || pafSubmitDisable} | |||
| > | |||
| {isPrinting ? t("Printing") : t("print")} | |||
| </Button> | |||
| {/* <Button | |||
| id="putawaySubmit" | |||
| type="submit" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ mt: 1 }} | |||
| onClick={formProps.handleSubmit(onSubmitPutaway)} | |||
| disabled={pafSubmitDisable} | |||
| > | |||
| {t("confirm putaway")} | |||
| </Button> */} | |||
| </Stack> | |||
| </Grid> | |||
| </Grid> | |||
| <Box> | |||
| <PutAwayForm | |||
| itemDetail={stockInLineInfo} | |||
| warehouse={warehouse!} | |||
| disabled={viewOnly} | |||
| setRowModesModel={setPafRowModesModel} | |||
| setRowSelectionModel={setPafRowSelectionModel} | |||
| /> | |||
| </Box> | |||
| } | |||
| </Grid> | |||
| </Grid> | |||
| {tabIndex == 1 && ( | |||
| <Stack direction="row" justifyContent="flex-end" gap={1} sx={{m:3, mt:"auto"}}> | |||
| <Autocomplete | |||
| disableClearable | |||
| options={printerCombo} | |||
| defaultValue={selectedPrinter} | |||
| onChange={(event, value) => { | |||
| setSelectedPrinter(value) | |||
| }} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| variant="outlined" | |||
| label={t("Printer")} | |||
| sx={{ width: 300}} | |||
| /> | |||
| )} | |||
| /> | |||
| <TextField | |||
| variant="outlined" | |||
| label={t("Print Qty")} | |||
| defaultValue={printQty} | |||
| onChange={(event) => { | |||
| event.target.value = event.target.value.replace(/[^0-9]/g, '') | |||
| setPrintQty(Number(event.target.value)) | |||
| }} | |||
| sx={{ width: 300}} | |||
| /> | |||
| <Button | |||
| id="printButton" | |||
| type="button" | |||
| variant="contained" | |||
| color="primary" | |||
| sx={{ mt: 1 }} | |||
| onClick={handlePrint} | |||
| disabled={isPrinting || printerCombo.length <= 0 || pafSubmitDisable} | |||
| > | |||
| {isPrinting ? t("Printing") : t("print")} | |||
| </Button> | |||
| </Stack> | |||
| )} | |||
| </>) : <LoadingComponent/>} | |||
| </Box> | |||
| </Modal> | |||
| </FormProvider> | |||
| @@ -4,7 +4,7 @@ import { | |||
| PurchaseQcResult, | |||
| PurchaseQCInput, | |||
| StockInInput, | |||
| } from "@/app/api/po/actions"; | |||
| } from "@/app/api/stockIn/actions"; | |||
| import { | |||
| Box, | |||
| Card, | |||
| @@ -34,7 +34,7 @@ import TwoLineCell from "./TwoLineCell"; | |||
| import QcSelect from "./QcSelect"; | |||
| import { QcItemWithChecks } from "@/app/api/qc"; | |||
| import { GridEditInputCell } from "@mui/x-data-grid"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { StockInLine } from "@/app/api/stockIn"; | |||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| @@ -249,6 +249,19 @@ const StockInForm: React.FC<Props> = ({ | |||
| </> | |||
| )} | |||
| <Grid item xs={6}> | |||
| {putawayMode ? ( | |||
| <TextField | |||
| label={t("stockLotNo")} | |||
| fullWidth | |||
| {...register("lotNo", { | |||
| // required: "productLotNo required!", | |||
| })} | |||
| sx={textfieldSx} | |||
| disabled={disabled} | |||
| error={Boolean(errors.productLotNo)} | |||
| helperText={errors.productLotNo?.message} | |||
| />) : ( | |||
| <TextField | |||
| label={t("productLotNo")} | |||
| fullWidth | |||
| @@ -259,7 +272,7 @@ const StockInForm: React.FC<Props> = ({ | |||
| disabled={disabled} | |||
| error={Boolean(errors.productLotNo)} | |||
| helperText={errors.productLotNo?.message} | |||
| /> | |||
| />)} | |||
| </Grid> | |||
| {putawayMode || (<> | |||
| <Grid item xs={6}> | |||
| @@ -350,8 +363,20 @@ const StockInForm: React.FC<Props> = ({ | |||
| }} | |||
| /> | |||
| </Grid> | |||
| {putawayMode || ( | |||
| <Grid item xs={6}> | |||
| <Grid item xs={6}> | |||
| {putawayMode ? ( | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| sx={textfieldSx} | |||
| disabled={true} | |||
| value={itemDetail.acceptedQty} | |||
| // disabled={true} | |||
| // disabled={disabled} | |||
| // error={Boolean(errors.acceptedQty)} | |||
| // helperText={errors.acceptedQty?.message} | |||
| /> | |||
| ) : ( | |||
| <TextField | |||
| label={t("receivedQty")} | |||
| fullWidth | |||
| @@ -361,8 +386,8 @@ const StockInForm: React.FC<Props> = ({ | |||
| sx={textfieldSx} | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| )} | |||
| )} | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("uom")} | |||
| @@ -375,21 +400,8 @@ const StockInForm: React.FC<Props> = ({ | |||
| disabled={true} | |||
| /> | |||
| </Grid> | |||
| {putawayMode ? (<> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| sx={textfieldSx} | |||
| disabled={true} | |||
| value={itemDetail.acceptedQty} | |||
| // disabled={true} | |||
| // disabled={disabled} | |||
| // error={Boolean(errors.acceptedQty)} | |||
| // helperText={errors.acceptedQty?.message} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <Grid item xs={6}> | |||
| {putawayMode ? ( | |||
| <TextField | |||
| label={t("processedQty")} | |||
| fullWidth | |||
| @@ -401,9 +413,7 @@ const StockInForm: React.FC<Props> = ({ | |||
| // error={Boolean(errors.acceptedQty)} | |||
| // helperText={errors.acceptedQty?.message} | |||
| /> | |||
| </Grid></> | |||
| ) : ( | |||
| <Grid item xs={6}> | |||
| ) : ( | |||
| <TextField | |||
| label={t("acceptedQty")} | |||
| fullWidth | |||
| @@ -417,9 +427,8 @@ const StockInForm: React.FC<Props> = ({ | |||
| // error={Boolean(errors.acceptedQty)} | |||
| // helperText={errors.acceptedQty?.message} | |||
| /> | |||
| </Grid> | |||
| ) | |||
| } | |||
| )} | |||
| </Grid> | |||
| {/* <Grid item xs={4}> | |||
| <TextField | |||
| label={t("acceptedWeight")} | |||
| @@ -15,7 +15,7 @@ import { useSession } from "next-auth/react"; | |||
| import { defaultPagingController } from "../SearchResults/SearchResults"; | |||
| import { fetchPoListClient, testing } from "@/app/api/po/actions"; | |||
| import dayjs from "dayjs"; | |||
| import { arrayToDateString, OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { arrayToDateString, dayjsToInputDateStringFIX } from "@/app/utils/formatUtil"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import { Checkbox, Box } from "@mui/material"; | |||
| dayjs.extend(arraySupport); | |||
| @@ -38,7 +38,7 @@ const PoSearch: React.FC<Props> = ({ | |||
| const [selectedPoIds, setSelectedPoIds] = useState<number[]>([]); | |||
| const [selectAll, setSelectAll] = useState(false); | |||
| const [filteredPo, setFilteredPo] = useState<PoResult[]>(po); | |||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({}); | |||
| const [filterArgs, setFilterArgs] = useState<Record<string, any>>({estimatedArrivalDate : dayjsToInputDateStringFIX(dayjs())}); | |||
| const { t } = useTranslation("purchaseOrder"); | |||
| const router = useRouter(); | |||
| const [pagingController, setPagingController] = useState( | |||
| @@ -66,7 +66,8 @@ const PoSearch: React.FC<Props> = ({ | |||
| { label: t(`completed`), value: `completed` }, | |||
| ], | |||
| }, | |||
| { label: t("ETA"), label2: t("ETA To"), paramName: "estimatedArrivalDate", type: "dateRange" }, | |||
| { label: t("ETA"), label2: t("ETA To"), paramName: "estimatedArrivalDate", type: "dateRange", | |||
| preFilledValue: dayjsToInputDateStringFIX(dayjs()) }, | |||
| ]; | |||
| return searchCriteria; | |||
| @@ -19,11 +19,10 @@ import ReactQrCodeScanner, { | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { | |||
| fetchStockInLineInfo, | |||
| ModalFormInput, | |||
| StockInLineEntry, | |||
| updateStockInLine, | |||
| } from "@/app/api/po/actions"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| } from "@/app/api/stockIn/actions"; | |||
| import { ModalFormInput, StockInLine } from "@/app/api/stockIn"; | |||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||
| // import { QrCodeInfo } from "@/app/api/qrcde"; | |||
| import { Check, QrCode, ErrorOutline, CheckCircle } from "@mui/icons-material"; | |||
| @@ -94,7 +93,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId | |||
| { | |||
| ...itemDetail, | |||
| // status: itemDetail.status ?? "pending", | |||
| dnDate: arrayToDateString(itemDetail?.dnDate, "input")?? undefined, | |||
| // dnDate: arrayToDateString(itemDetail?.dnDate, "input")?? undefined, | |||
| // // putAwayLines: dummyPutAwayLine, | |||
| // // putAwayLines: itemDetail.putAwayLines.map((line) => (return {...line, printQty: 1})) ?? [], | |||
| // putAwayLines: itemDetail.putAwayLines?.map((line) => ({...line, printQty: 1, _isNew: false})) ?? [], | |||
| @@ -105,7 +104,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId | |||
| receiptDate: itemDetail?.receiptDate ? arrayToDateString(itemDetail?.receiptDate, "input") : undefined, | |||
| // acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, | |||
| defaultWarehouseId: itemDetail?.defaultWarehouseId ?? 1, | |||
| } | |||
| } as ModalFormInput | |||
| ) | |||
| }, [itemDetail]) | |||
| @@ -218,7 +217,7 @@ const PutAwayModal: React.FC<Props> = ({ open, onClose, warehouse, stockInLineId | |||
| useEffect(() => { | |||
| if (stockInLineId) { fetchStockInLine(stockInLineId); } | |||
| }, [stockInLineId]); | |||
| }, [stockInLineId]); | |||
| const validateQty = useCallback((qty : number = putQty) => { | |||
| // if (isNaN(putQty) || putQty === undefined || putQty === null || typeof(putQty) != "number") { | |||
| @@ -15,13 +15,7 @@ import { | |||
| ScannerConfig, | |||
| } from "../ReactQrCodeScanner/ReactQrCodeScanner"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { | |||
| fetchStockInLineInfo, | |||
| ModalFormInput, | |||
| StockInLineEntry, | |||
| updateStockInLine, | |||
| } from "@/app/api/po/actions"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { StockInLine } from "@/app/api/stockIn"; | |||
| import { WarehouseResult } from "@/app/api/warehouse"; | |||
| import { QrCodeInfo } from "@/app/api/qrcode"; | |||
| import { Check, QrCodeScanner, Warehouse } from "@mui/icons-material"; | |||
| @@ -36,6 +36,8 @@ interface BaseCriterion<T extends string> { | |||
| paramName: T; | |||
| paramName2?: T; | |||
| // options?: T[] | string[]; | |||
| defaultValue?: string; | |||
| preFilledValue?: string; | |||
| filterObj?: T; | |||
| handleSelectionChange?: (selectedOptions: T[]) => void; | |||
| } | |||
| @@ -136,18 +138,31 @@ function SearchBox<T extends string>({ | |||
| if (c.type === "dateRange") { | |||
| tempCriteria = { | |||
| ...tempCriteria, | |||
| [c.paramName]: "", | |||
| [c.paramName]: c.defaultValue ?? "", | |||
| [`${c.paramName}To`]: "", | |||
| }; | |||
| } | |||
| return tempCriteria; | |||
| }, | |||
| {} as Record<T | `${T}To`, string>, | |||
| ), | |||
| [criteria], | |||
| ); | |||
| const [inputs, setInputs] = useState(defaultInputs); | |||
| const preFilledInputs = useMemo(() => { | |||
| const preFilledCriteria = criteria.reduce<Record<T | `${T}To`, string>>( | |||
| (acc, c) => { | |||
| if (c.preFilledValue !== undefined) { | |||
| return { | |||
| ...acc, | |||
| [c.paramName]: c.preFilledValue, | |||
| }; | |||
| } else return acc; | |||
| }, | |||
| {} as Record<T | `${T}To`, string>,); | |||
| return {...defaultInputs, ...preFilledCriteria} | |||
| }, [defaultInputs]) | |||
| const [inputs, setInputs] = useState(preFilledInputs); | |||
| const [isReset, setIsReset] = useState(false); | |||
| const makeInputChangeHandler = useCallback( | |||
| @@ -43,5 +43,6 @@ | |||
| "Back": "返回", | |||
| "Batch Release": "批量放單", | |||
| "Batch release completed successfully.": "已完成批量放單", | |||
| "Edit Delivery Order Detail": "編輯交貨單詳情" | |||
| "Edit Delivery Order Detail": "編輯交貨單詳情", | |||
| "DO released successfully! Pick orders created.": "交貨單放單成功!提料單已建立。" | |||
| } | |||
| @@ -192,7 +192,7 @@ | |||
| "Finished Good Order": "成品出倉", | |||
| "Assign and Release": "分派並放單", | |||
| "Original Available Qty": "原可用數", | |||
| "Remaining Available Qty": "剩餘", | |||
| "Remaining Available Qty": "剩餘可用數", | |||
| "Please submit pick order.": "請提交提料單。", | |||
| "Please finish QR code scan and pick order.": "請完成 QR 碼掃描和提料。", | |||
| "Please finish QR code scanand pick order.": "請完成 QR 碼掃描和提料。", | |||
| @@ -273,6 +273,17 @@ | |||
| "Print Draft":"列印草稿", | |||
| "Print Pick Order and DN Label":"列印提料單和送貨單標貼", | |||
| "Print Pick Order":"列印提料單", | |||
| "Print DN Label":"列印送貨單標貼", | |||
| "If you confirm, the system will:":"如果您確認,系統將:", | |||
| "QR code verified.":"QR 碼驗證成功。", | |||
| "Order Finished":"訂單完成", | |||
| "Submitted Status":"提交狀態", | |||
| "Pick Execution Record":"提料執行記錄", | |||
| "Delivery No.":"送貨單編號", | |||
| "Total":"總數", | |||
| "completed DO pick orders":"已完成送貨單提料單", | |||
| "No completed DO pick orders found":"沒有已完成送貨單提料單", | |||
| "Print DN Label":"列印送貨單標貼", | |||
| "Enter the number of cartons: ": "請輸入總箱數", | |||
| "Number of cartons": "箱數" | |||
| @@ -44,7 +44,7 @@ | |||
| "price": "訂單貨值", | |||
| "processedQty": "已上架數量", | |||
| "expiryDate": "到期日", | |||
| "acceptedQty": "是次來貨數量", | |||
| "acceptedQty": "本批收貨數量", | |||
| "putawayQty": "上架數量", | |||
| "acceptQty": "揀收數量", | |||
| "printQty": "列印數量", | |||
| @@ -91,7 +91,7 @@ | |||
| "to be processed": "待處理", | |||
| "supervisor": "管理層", | |||
| "Stock In Detail": "入庫詳情", | |||
| "productLotNo": "貨品批號", | |||
| "productLotNo": "來貨批號", | |||
| "receiptDate": "收貨日期", | |||
| "acceptedWeight": "接受重量", | |||
| "productionDate": "生產日期", | |||
| @@ -100,7 +100,7 @@ | |||
| "Select warehouse": "選擇倉庫", | |||
| "Putaway Detail": "上架詳情", | |||
| "Delivery Detail": "來貨詳情", | |||
| "LotNo": "批號", | |||
| "stockLotNo": "入倉批號", | |||
| "Po Code": "採購訂單編號", | |||
| "No Warehouse": "沒有倉庫", | |||
| "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", | |||