Przeglądaj źródła

Merge branch 'master' of https://git.2fi-solutions.com/derek/FPSMS-frontend

# Conflicts:
#	src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
#	src/i18n/zh/pickOrder.json
master
kelvin.yau 2 miesięcy temu
rodzic
commit
5a31516cff
30 zmienionych plików z 1837 dodań i 733 usunięć
  1. +100
    -1
      src/app/api/pickOrder/actions.ts
  2. +20
    -3
      src/app/api/po/actions.ts
  3. +3
    -1
      src/app/api/po/index.ts
  4. +8
    -0
      src/app/api/qc/index.ts
  5. +243
    -0
      src/app/api/stockIn/actions.ts
  6. +168
    -0
      src/app/api/stockIn/index.ts
  7. +13
    -2
      src/app/utils/formatUtil.ts
  8. +1
    -1
      src/components/DoDetail/DoDetail.tsx
  9. +1
    -10
      src/components/FinishedGoodSearch/FGPickOrderCard.tsx
  10. +80
    -1
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  11. +1
    -1
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  12. +416
    -0
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  13. +217
    -47
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  14. +2
    -2
      src/components/FinishedGoodSearch/LotConfirmationModal.tsx
  15. +1
    -1
      src/components/PoDetail/EscalationComponent.tsx
  16. +2
    -2
      src/components/PoDetail/EscalationForm.tsx
  17. +13
    -14
      src/components/PoDetail/PoDetail.tsx
  18. +2
    -4
      src/components/PoDetail/PoDetailWrapper.tsx
  19. +31
    -117
      src/components/PoDetail/PoInputGrid.tsx
  20. +46
    -78
      src/components/PoDetail/PutAwayForm.tsx
  21. +139
    -149
      src/components/PoDetail/QcComponent.tsx
  22. +248
    -247
      src/components/PoDetail/QcStockInModal.tsx
  23. +37
    -28
      src/components/PoDetail/StockInForm.tsx
  24. +4
    -3
      src/components/PoSearch/PoSearch.tsx
  25. +5
    -6
      src/components/PutAwayScan/PutAwayModal.tsx
  26. +1
    -7
      src/components/PutAwayScan/PutAwayScan.tsx
  27. +18
    -3
      src/components/SearchBox/SearchBox.tsx
  28. +2
    -1
      src/i18n/zh/do.json
  29. +12
    -1
      src/i18n/zh/pickOrder.json
  30. +3
    -3
      src/i18n/zh/purchaseOrder.json

+ 100
- 1
src/app/api/pickOrder/actions.ts Wyświetl plik

@@ -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 {


+ 20
- 3
src/app/api/po/actions.ts Wyświetl plik

@@ -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}`,


+ 3
- 1
src/app/api/po/index.ts Wyświetl plik

@@ -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;


+ 8
- 0
src/app/api/qc/index.ts Wyświetl plik

@@ -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,


+ 243
- 0
src/app/api/stockIn/actions.ts Wyświetl plik

@@ -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"],
},
},
)
})

+ 168
- 0
src/app/api/stockIn/index.ts Wyświetl plik

@@ -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>
};

+ 13
- 2
src/app/utils/formatUtil.ts Wyświetl plik

@@ -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))
}


+ 1
- 1
src/components/DoDetail/DoDetail.tsx Wyświetl plik

@@ -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) {


+ 1
- 10
src/components/FinishedGoodSearch/FGPickOrderCard.tsx Wyświetl plik

@@ -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>


+ 80
- 1
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx Wyświetl plik

@@ -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>
);


+ 1
- 1
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx Wyświetl plik

@@ -351,7 +351,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={loading}>
{t('cancel')}
{t('Cancel')}
</Button>
<Button
onClick={handleSubmit}


+ 416
- 0
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx Wyświetl plik

@@ -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;

+ 217
- 47
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx Wyświetl plik

@@ -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">


+ 2
- 2
src/components/FinishedGoodSearch/LotConfirmationModal.tsx Wyświetl plik

@@ -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>


+ 1
- 1
src/components/PoDetail/EscalationComponent.tsx Wyświetl plik

@@ -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';


+ 2
- 2
src/components/PoDetail/EscalationForm.tsx Wyświetl plik

@@ -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 {


+ 13
- 14
src/components/PoDetail/PoDetail.tsx Wyświetl plik

@@ -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}


+ 2
- 4
src/components/PoDetail/PoDetailWrapper.tsx Wyświetl plik

@@ -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
- 117
src/components/PoDetail/PoInputGrid.tsx Wyświetl plik

@@ -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}
/>
</>
)} */}
{/* )
} */}
</>
);
}


+ 46
- 78
src/components/PoDetail/PutAwayForm.tsx Wyświetl plik

@@ -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>
);
};


+ 139
- 149
src/components/PoDetail/QcComponent.tsx Wyświetl plik

@@ -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>
</>
);


+ 248
- 247
src/components/PoDetail/QcStockInModal.tsx Wyświetl plik

@@ -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>


+ 37
- 28
src/components/PoDetail/StockInForm.tsx Wyświetl plik

@@ -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")}


+ 4
- 3
src/components/PoSearch/PoSearch.tsx Wyświetl plik

@@ -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;


+ 5
- 6
src/components/PutAwayScan/PutAwayModal.tsx Wyświetl plik

@@ -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") {


+ 1
- 7
src/components/PutAwayScan/PutAwayScan.tsx Wyświetl plik

@@ -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";


+ 18
- 3
src/components/SearchBox/SearchBox.tsx Wyświetl plik

@@ -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(


+ 2
- 1
src/i18n/zh/do.json Wyświetl plik

@@ -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.": "交貨單放單成功!提料單已建立。"
}

+ 12
- 1
src/i18n/zh/pickOrder.json Wyświetl plik

@@ -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": "箱數"


+ 3
- 3
src/i18n/zh/purchaseOrder.json Wyświetl plik

@@ -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 碼。",


Ładowanie…
Anuluj
Zapisz