Bläddra i källkod

update stock in modal & po

master
kelvinsuen 2 månader sedan
förälder
incheckning
b0fa2ac4d5
20 ändrade filer med 1005 tillägg och 668 borttagningar
  1. +20
    -3
      src/app/api/po/actions.ts
  2. +3
    -1
      src/app/api/po/index.ts
  3. +8
    -0
      src/app/api/qc/index.ts
  4. +243
    -0
      src/app/api/stockIn/actions.ts
  5. +168
    -0
      src/app/api/stockIn/index.ts
  6. +13
    -2
      src/app/utils/formatUtil.ts
  7. +1
    -1
      src/components/PoDetail/EscalationComponent.tsx
  8. +2
    -2
      src/components/PoDetail/EscalationForm.tsx
  9. +13
    -14
      src/components/PoDetail/PoDetail.tsx
  10. +2
    -4
      src/components/PoDetail/PoDetailWrapper.tsx
  11. +31
    -117
      src/components/PoDetail/PoInputGrid.tsx
  12. +46
    -78
      src/components/PoDetail/PutAwayForm.tsx
  13. +139
    -149
      src/components/PoDetail/QcComponent.tsx
  14. +248
    -247
      src/components/PoDetail/QcStockInModal.tsx
  15. +37
    -28
      src/components/PoDetail/StockInForm.tsx
  16. +4
    -3
      src/components/PoSearch/PoSearch.tsx
  17. +5
    -6
      src/components/PutAwayScan/PutAwayModal.tsx
  18. +1
    -7
      src/components/PutAwayScan/PutAwayScan.tsx
  19. +18
    -3
      src/components/SearchBox/SearchBox.tsx
  20. +3
    -3
      src/i18n/zh/purchaseOrder.json

+ 20
- 3
src/app/api/po/actions.ts Visa fil

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

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

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

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

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

@@ -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/PoDetail/EscalationComponent.tsx Visa fil

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

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

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

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

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

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

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

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

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

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

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

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

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


+ 3
- 3
src/i18n/zh/purchaseOrder.json Visa fil

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


Laddar…
Avbryt
Spara