Browse Source

update stock in modal

master
kelvinsuen 1 week ago
parent
commit
f7b35c13a1
16 changed files with 244 additions and 105 deletions
  1. +12
    -10
      src/app/api/dashboard/actions.ts
  2. +8
    -5
      src/app/api/po/actions.ts
  3. +4
    -0
      src/app/utils/formatUtil.ts
  4. +4
    -4
      src/components/DashboardPage/QC/SupervisorQcApproval.tsx
  5. +1
    -0
      src/components/PoDetail/PoDetail.tsx
  6. +35
    -18
      src/components/PoDetail/PoInputGrid.tsx
  7. +3
    -3
      src/components/PoDetail/PoQcStockInModal.tsx
  8. +3
    -2
      src/components/PoDetail/PutawayForm.tsx
  9. +26
    -15
      src/components/PoDetail/QcFormVer2.tsx
  10. +120
    -31
      src/components/PoDetail/QcStockInModalVer2.tsx
  11. +6
    -5
      src/components/PoDetail/StockInFormVer2.tsx
  12. +6
    -6
      src/components/PoDetail/dummyQcTemplate.tsx
  13. +1
    -0
      src/components/Swal/CustomAlerts.tsx
  14. +2
    -2
      src/i18n/index.tsx
  15. +4
    -0
      src/i18n/zh/dashboard.json
  16. +9
    -4
      src/i18n/zh/purchaseOrder.json

+ 12
- 10
src/app/api/dashboard/actions.ts View File

@@ -11,7 +11,7 @@ import { QcItemResult } from "../settings/qcItem";
import { RecordsRes } from "../utils"; import { RecordsRes } from "../utils";
// import { BASE_API_URL } from "@/config/api"; // import { BASE_API_URL } from "@/config/api";


export interface PostStockInLiineResponse<T> {
export interface PostStockInLineResponse<T> {
id: number | null; id: number | null;
name: string; name: string;
code: string; code: string;
@@ -38,6 +38,7 @@ export interface StockInLineEntry {
export interface PurchaseQcResult { export interface PurchaseQcResult {
qcItemId: number; qcItemId: number;
failQty: number; failQty: number;
isPassed: boolean;
} }
export interface StockInInput { export interface StockInInput {
status: string; status: string;
@@ -49,11 +50,12 @@ export interface StockInInput {
expiryDate: string; expiryDate: string;
} }
export interface PurchaseQCInput { export interface PurchaseQCInput {
status: string;
acceptedQty: number;
sampleRate: number;
sampleWeight: number;
totalWeight: number;
status?: string;
qcAccept: boolean;
acceptQty: number;
sampleRate?: number;
sampleWeight?: number;
totalWeight?: number;
qcResult: PurchaseQcResult[]; qcResult: PurchaseQcResult[];
} }
export interface EscalationInput { export interface EscalationInput {
@@ -92,7 +94,7 @@ export const fetchStockInLineInfo = cache(async (stockInLineId: number) => {
export const createStockInLine = async (data: StockInLineEntry) => { export const createStockInLine = async (data: StockInLineEntry) => {
console.log(data) console.log(data)
const stockInLine = await serverFetchJson< const stockInLine = await serverFetchJson<
PostStockInLiineResponse<StockInLineEntry>
PostStockInLineResponse<StockInLineEntry>
>(`${BASE_API_URL}/stockInLine/create`, { >(`${BASE_API_URL}/stockInLine/create`, {
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -106,7 +108,7 @@ export const updateStockInLine = async (
data: StockInLineEntry & ModalFormInput, data: StockInLineEntry & ModalFormInput,
) => { ) => {
const stockInLine = await serverFetchJson< const stockInLine = await serverFetchJson<
PostStockInLiineResponse<StockInLineEntry & ModalFormInput>
PostStockInLineResponse<StockInLineEntry & ModalFormInput>
>(`${BASE_API_URL}/stockInLine/update`, { >(`${BASE_API_URL}/stockInLine/update`, {
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -117,7 +119,7 @@ export const updateStockInLine = async (
}; };


export const startPo = async (poId: number) => { export const startPo = async (poId: number) => {
const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(
const po = await serverFetchJson<PostStockInLineResponse<PoResult>>(
`${BASE_API_URL}/po/start/${poId}`, `${BASE_API_URL}/po/start/${poId}`,
{ {
method: "POST", method: "POST",
@@ -130,7 +132,7 @@ export const startPo = async (poId: number) => {
}; };


export const checkPolAndCompletePo = async (poId: number) => { export const checkPolAndCompletePo = async (poId: number) => {
const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(
const po = await serverFetchJson<PostStockInLineResponse<PoResult>>(
`${BASE_API_URL}/po/check/${poId}`, `${BASE_API_URL}/po/check/${poId}`,
{ {
method: "POST", method: "POST",


+ 8
- 5
src/app/api/po/actions.ts View File

@@ -12,7 +12,7 @@ import { RecordsRes } from "../utils";
import { Uom } from "../settings/uom"; import { Uom } from "../settings/uom";
// import { BASE_API_URL } from "@/config/api"; // import { BASE_API_URL } from "@/config/api";


export interface PostStockInLiineResponse<T> {
export interface PostStockInLineResponse<T> {
id: number | null; id: number | null;
name: string; name: string;
code: string; code: string;
@@ -35,13 +35,16 @@ export interface StockInLineEntry {


export interface PurchaseQcResult { export interface PurchaseQcResult {
qcItemId: number; qcItemId: number;
isPassed: boolean,
failQty: number; failQty: number;
remarks?: string
} }
export interface StockInInput { export interface StockInInput {
status: string; status: string;
poCode: string; poCode: string;
productLotNo?: string; productLotNo?: string;
dnNo?: string; dnNo?: string;
dnDate?: string;
itemName: string; itemName: string;
invoiceNo?: string; invoiceNo?: string;
receiptDate: string; receiptDate: string;
@@ -107,7 +110,7 @@ export const fetchStockInLineInfo = cache(async (stockInLineId: number) => {


export const createStockInLine = async (data: StockInLineEntry) => { export const createStockInLine = async (data: StockInLineEntry) => {
const stockInLine = await serverFetchJson< const stockInLine = await serverFetchJson<
PostStockInLiineResponse<StockInLineEntry>
PostStockInLineResponse<StockInLineEntry>
>(`${BASE_API_URL}/stockInLine/create`, { >(`${BASE_API_URL}/stockInLine/create`, {
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -121,7 +124,7 @@ export const updateStockInLine = async (
data: StockInLineEntry & ModalFormInput, data: StockInLineEntry & ModalFormInput,
) => { ) => {
const stockInLine = await serverFetchJson< const stockInLine = await serverFetchJson<
PostStockInLiineResponse<StockInLineEntry & ModalFormInput>
PostStockInLineResponse<StockInLineEntry & ModalFormInput>
>(`${BASE_API_URL}/stockInLine/update`, { >(`${BASE_API_URL}/stockInLine/update`, {
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -132,7 +135,7 @@ export const updateStockInLine = async (
}; };


export const startPo = async (poId: number) => { export const startPo = async (poId: number) => {
const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(
const po = await serverFetchJson<PostStockInLineResponse<PoResult>>(
`${BASE_API_URL}/po/start/${poId}`, `${BASE_API_URL}/po/start/${poId}`,
{ {
method: "POST", method: "POST",
@@ -145,7 +148,7 @@ export const startPo = async (poId: number) => {
}; };


export const checkPolAndCompletePo = async (poId: number) => { export const checkPolAndCompletePo = async (poId: number) => {
const po = await serverFetchJson<PostStockInLiineResponse<PoResult>>(
const po = await serverFetchJson<PostStockInLineResponse<PoResult>>(
`${BASE_API_URL}/po/check/${poId}`, `${BASE_API_URL}/po/check/${poId}`,
{ {
method: "POST", method: "POST",


+ 4
- 0
src/app/utils/formatUtil.ts View File

@@ -54,6 +54,10 @@ export const arrayToDateString = (arr: ConfigType | (number | undefined)[]) => {
return arrayToDayjs(arr).format(OUTPUT_DATE_FORMAT); return arrayToDayjs(arr).format(OUTPUT_DATE_FORMAT);
}; };


export const arrayToInputDateString = (arr: ConfigType | (number | undefined)[]) => {
return arrayToDayjs(arr).format(INPUT_DATE_FORMAT);
};

export const dateStringToDayjs = (date: string) => { export const dateStringToDayjs = (date: string) => {
// Format: YYYY/MM/DD // Format: YYYY/MM/DD
return dayjs(date, OUTPUT_DATE_FORMAT); return dayjs(date, OUTPUT_DATE_FORMAT);


+ 4
- 4
src/components/DashboardPage/QC/SupervisorQcApproval.tsx View File

@@ -55,10 +55,10 @@ const navigateTo = useCallback(
<Table aria-label="Two column navigable table" size="small"> <Table aria-label="Two column navigable table" size="small">
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>{t("purchase order code")}</TableCell>
<TableCell>{t("item name")}</TableCell>
<TableCell>{t("escalation level")}</TableCell>
<TableCell>{t("reason")}</TableCell>
<TableCell>{t("Purchase Order Code")}</TableCell>
<TableCell>{t("Item Name")}</TableCell>
<TableCell>{t("Escalation Level")}</TableCell>
<TableCell>{t("Reason")}</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>


+ 1
- 0
src/components/PoDetail/PoDetail.tsx View File

@@ -823,6 +823,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => {
setProcessedQty={setProcessedQty} setProcessedQty={setProcessedQty}
itemDetail={selectedRow} itemDetail={selectedRow}
warehouse={warehouse} warehouse={warehouse}
fetchPoDetail={fetchPoDetail}
/> />
</Box> </Box>
</TableCell> </TableCell>


+ 35
- 18
src/components/PoDetail/PoInputGrid.tsx View File

@@ -73,6 +73,7 @@ interface Props {
itemDetail: PurchaseOrderLine; itemDetail: PurchaseOrderLine;
stockInLine: StockInLine[]; stockInLine: StockInLine[];
warehouse: WarehouseResult[]; warehouse: WarehouseResult[];
fetchPoDetail: (poId: string) => void;
} }


export type StockInLineEntryError = { export type StockInLineEntryError = {
@@ -111,6 +112,7 @@ function PoInputGrid({
itemDetail, itemDetail,
stockInLine, stockInLine,
warehouse, warehouse,
fetchPoDetail
}: Props) { }: Props) {
console.log(itemDetail); console.log(itemDetail);
const { t } = useTranslation("purchaseOrder"); const { t } = useTranslation("purchaseOrder");
@@ -272,6 +274,7 @@ const closeNewModal = useCallback(() => {
const newParams = new URLSearchParams(searchParams.toString()); const newParams = new URLSearchParams(searchParams.toString());
newParams.delete("stockInLineId"); // Remove the parameter newParams.delete("stockInLineId"); // Remove the parameter
router.replace(`${pathname}?${newParams.toString()}`); router.replace(`${pathname}?${newParams.toString()}`);
fetchPoDetail(itemDetail.purchaseOrderId.toString());
setTimeout(() => { setTimeout(() => {
setNewOpen(false); // Close the modal first setNewOpen(false); // Close the modal first
}, 300); // Add a delay to avoid immediate re-trigger of useEffect }, 300); // Add a delay to avoid immediate re-trigger of useEffect
@@ -413,6 +416,17 @@ const closeNewModal = useCallback(() => {
[], [],
); );


const getButtonSx = (status : string) => {
let btnSx = {label:"", color:""};
switch (status) {
case "received": btnSx = {label: t("putaway processing"), color:"secondary.main"}; break;
case "rejected":
case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break;
default: btnSx = {label: t("qc processing"), color:"success.main"};
}
return btnSx
};

// const handleQrCode = useCallback( // const handleQrCode = useCallback(
// (id: GridRowId, params: any) => () => { // (id: GridRowId, params: any) => () => {
// setRowModesModel((prev) => ({ // setRowModesModel((prev) => ({
@@ -532,7 +546,7 @@ const closeNewModal = useCallback(() => {
{ {
field: "status", field: "status",
headerName: t("Status"), headerName: t("Status"),
width: 70,
width: 140,
// flex: 0.5, // flex: 0.5,
renderCell: (params) => { renderCell: (params) => {
return t(`${params.row.status}`); return t(`${params.row.status}`);
@@ -546,20 +560,22 @@ const closeNewModal = useCallback(() => {
// )} | ${t("putaway")} | ${t("delete")}`, // )} | ${t("putaway")} | ${t("delete")}`,
headerName: "動作", headerName: "動作",
// headerName: "start | qc | escalation | stock in | putaway | delete", // headerName: "start | qc | escalation | stock in | putaway | delete",
width: 300,
width: 200,
// flex: 2, // flex: 2,
cellClassName: "actions", cellClassName: "actions",
getActions: (params) => { getActions: (params) => {
// console.log(params.row.status); // console.log(params.row.status);
const status = params.row.status.toLowerCase(); const status = params.row.status.toLowerCase();
const btnSx = getButtonSx(status);
// console.log(stockInLineStatusMap[status]); // console.log(stockInLineStatusMap[status]);
// console.log(session?.user?.abilities?.includes("APPROVAL")); // console.log(session?.user?.abilities?.includes("APPROVAL"));
return [ return [
<GridActionsCellItem <GridActionsCellItem
icon={<Button variant="contained">{t("qc processing")}</Button>}
icon={<Button variant="contained" sx={{ width: '150px', backgroundColor: btnSx.color }}>
{btnSx.label}</Button>}
label="start" label="start"
sx={{ sx={{
color: "primary.main",
// color: "primary.main",
// marginRight: 1, // marginRight: 1,
}} }}
// disabled={!(stockInLineStatusMap[status] === 0)} // disabled={!(stockInLineStatusMap[status] === 0)}
@@ -569,20 +585,21 @@ const closeNewModal = useCallback(() => {
color="inherit" color="inherit"
key="edit" key="edit"
/>, />,
<GridActionsCellItem
icon={<Button variant="contained">{t("putawayBtn")}</Button>}
label="start"
sx={{
color: "primary.main",
// marginRight: 1,
}}
// disabled={!(stockInLineStatusMap[status] === 0)}
// set _isNew to false after posting
// or check status
onClick={handleStart(params.row.id, params)}
color="inherit"
key="edit"
/>,
// <GridActionsCellItem
// icon={<Button variant="contained">{t("putawayBtn")}</Button>}
// label="start"
// sx={{
// color: "primary.main",
// // marginRight: 1,
// }}
// // disabled={!(stockInLineStatusMap[status] === 0)}
// // set _isNew to false after posting
// // or check status
// onClick={handleStart(params.row.id, params)}
// color="inherit"
// key="edit"
// />,

// <GridActionsCellItem // <GridActionsCellItem
// icon={<Button variant="contained">{t("qc processing")}</Button>} // icon={<Button variant="contained">{t("qc processing")}</Button>}
// label="start" // label="start"


+ 3
- 3
src/components/PoDetail/PoQcStockInModal.tsx View File

@@ -121,9 +121,9 @@ const PoQcStockInModal: React.FC<Props> = ({
const params = useSearchParams(); const params = useSearchParams();
const [btnIsLoading, setBtnIsLoading] = useState(false); const [btnIsLoading, setBtnIsLoading] = useState(false);


console.log(params.get("id"));
console.log(itemDetail);
console.log(itemDetail.qcResult);
// console.log(params.get("id"));
// console.log(itemDetail);
// console.log(itemDetail.qcResult);
const formProps = useForm<ModalFormInput>({ const formProps = useForm<ModalFormInput>({
defaultValues: { defaultValues: {
...itemDetail, ...itemDetail,


+ 3
- 2
src/components/PoDetail/PutawayForm.tsx View File

@@ -245,8 +245,9 @@ const PutawayForm: React.FC<Props> = ({ itemDetail, warehouse, disabled }) => {
}, [scanner.values]); }, [scanner.values]);


useEffect(() => { useEffect(() => {
setValue("status", "completed");
setValue("warehouseId", options[0].value);
setValue("status", "received");
// setValue("status", "completed");
setValue("warehouseId", options[0].value); //TODO: save all warehouse entry?
}, []); }, []);


useEffect(() => { useEffect(() => {


+ 26
- 15
src/components/PoDetail/QcFormVer2.tsx View File

@@ -51,6 +51,7 @@ import StockInFormVer2 from "./StockInFormVer2";
import { dummyEscalationHistory, dummyQCData, QcData } from "./dummyQcTemplate"; import { dummyEscalationHistory, dummyQCData, QcData } from "./dummyQcTemplate";
import { ModalFormInput } from "@/app/api/dashboard/actions"; import { ModalFormInput } from "@/app/api/dashboard/actions";
import { escape } from "lodash"; import { escape } from "lodash";
import { PanoramaSharp } from "@mui/icons-material";


interface Props { interface Props {
itemDetail: StockInLine; itemDetail: StockInLine;
@@ -216,7 +217,8 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
<FormControlLabel <FormControlLabel
value="true" value="true"
control={<Radio />} control={<Radio />}
label="合格"
label="合格"
disabled={itemDetail.status.toLowerCase() == "completed"}
sx={{ sx={{
color: currentValue === true ? "green" : "inherit", color: currentValue === true ? "green" : "inherit",
"& .Mui-checked": {color: "green"} "& .Mui-checked": {color: "green"}
@@ -226,6 +228,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
value="false" value="false"
control={<Radio />} control={<Radio />}
label="不合格" label="不合格"
disabled={itemDetail.status.toLowerCase() == "completed"}
sx={{ sx={{
color: currentValue === false ? "red" : "inherit", color: currentValue === false ? "red" : "inherit",
"& .Mui-checked": {color: "red"} "& .Mui-checked": {color: "red"}
@@ -237,7 +240,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
}, },
}, },
{ {
field: "failedQty",
field: "failQty",
headerName: t("failedQty"), headerName: t("failedQty"),
flex: 1, flex: 1,
// editable: true, // editable: true,
@@ -246,13 +249,13 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
type="number" type="number"
size="small" size="small"
value={!params.row.isPassed? (params.value ?? '') : '0'} value={!params.row.isPassed? (params.value ?? '') : '0'}
disabled={params.row.isPassed}
disabled={params.row.isPassed || itemDetail.status.toLowerCase() == "completed"}
onChange={(e) => { onChange={(e) => {
const v = e.target.value; const v = e.target.value;
const next = v === '' ? undefined : Number(v); const next = v === '' ? undefined : Number(v);
if (Number.isNaN(next)) return; if (Number.isNaN(next)) return;
setQcItems((prev) => setQcItems((prev) =>
prev.map((r) => (r.id === params.id ? { ...r, failedQty: next } : r))
prev.map((r) => (r.id === params.id ? { ...r, failQty: next } : r))
); );
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
@@ -271,6 +274,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
<TextField <TextField
size="small" size="small"
value={params.value ?? ''} value={params.value ?? ''}
disabled={itemDetail.status.toLowerCase() == "completed"}
onChange={(e) => { onChange={(e) => {
const remarks = e.target.value; const remarks = e.target.value;
// const next = v === '' ? undefined : Number(v); // const next = v === '' ? undefined : Number(v);
@@ -296,10 +300,12 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI


// Set initial value for acceptQty // Set initial value for acceptQty
useEffect(() => { useEffect(() => {
if (itemDetail?.acceptedQty !== undefined) {
setValue("acceptQty", itemDetail.acceptedQty);
if (itemDetail?.demandQty > 0) { //!== undefined) {
setValue("acceptQty", itemDetail.demandQty); // THIS NEED TO UPDATE TO NOT USE DEMAND QTY
} else {
setValue("acceptQty", itemDetail?.acceptedQty);
} }
}, [itemDetail?.acceptedQty, setValue]);
}, [itemDetail?.demandQty, itemDetail?.acceptedQty, setValue]);


// const [openCollapse, setOpenCollapse] = useState(false) // const [openCollapse, setOpenCollapse] = useState(false)
const [isCollapsed, setIsCollapsed] = useState<boolean>(false); const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
@@ -365,14 +371,14 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
/> />
</Grid> </Grid>
{/* <Grid item xs={12}>
{!qcAccept && (
<Grid item xs={12}>
<EscalationComponent <EscalationComponent
forSupervisor={false} forSupervisor={false}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed} setIsCollapsed={setIsCollapsed}
/> />
</Grid> */}
</Grid>)}
</> </>
)} )}
{tabIndex == 1 && ( {tabIndex == 1 && (
@@ -419,14 +425,16 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
field.onChange(value); field.onChange(value);
}} }}
> >
<FormControlLabel value="true" control={<Radio />} label="接受" />
<FormControlLabel disabled={itemDetail.status.toLowerCase() == "completed"}
value="true" control={<Radio />} label="接受" />
<Box sx={{mr:2}}> <Box sx={{mr:2}}>
<TextField <TextField
type="number" type="number"
label={t("acceptQty")} label={t("acceptQty")}
sx={{ width: '150px' }} sx={{ width: '150px' }}
value={qcAccept? accQty : 0 }
defaultValue={accQty} defaultValue={accQty}
disabled={!qcAccept}
disabled={!qcAccept || itemDetail.status.toLowerCase() == "completed"}
{...register("acceptQty", { {...register("acceptQty", {
required: "acceptQty required!", required: "acceptQty required!",
})} })}
@@ -434,13 +442,16 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
helperText={errors.acceptQty?.message} helperText={errors.acceptQty?.message}
/> />
</Box> </Box>
<FormControlLabel value="false" control={<Radio />} label="不接受及上報" />
<FormControlLabel disabled={itemDetail.status.toLowerCase() == "completed"}
value="false" control={<Radio />}
sx={{"& .Mui-checked": {color: "red"}}}
label="不接受及上報" />
</RadioGroup> </RadioGroup>
)} )}
/> />
</FormControl> </FormControl>
</Grid> </Grid>
{/* <Grid item xs={12}>
{/* {qcAccept && <Grid item xs={12}>
<Typography variant="h6" display="block" marginBlockEnd={1}> <Typography variant="h6" display="block" marginBlockEnd={1}>
{t("Escalation Result")} {t("Escalation Result")}
</Typography> </Typography>
@@ -451,7 +462,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed} setIsCollapsed={setIsCollapsed}
/> />
</Grid> */}
</Grid>} */}
</Grid> </Grid>
</Grid> </Grid>
</> </>


+ 120
- 31
src/components/PoDetail/QcStockInModalVer2.tsx View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { StockInLine } from "@/app/api/po"; import { StockInLine } from "@/app/api/po";
import { ModalFormInput, PurchaseQcResult } from "@/app/api/po/actions";
import { ModalFormInput, PurchaseQcResult, StockInLineEntry, updateStockInLine } from "@/app/api/po/actions";
import { QcItemWithChecks } from "@/app/api/qc"; import { QcItemWithChecks } from "@/app/api/qc";
import { import {
Box, Box,
@@ -22,6 +22,9 @@ import PutawayForm from "./PutawayForm";
import { dummyPutawayLine, dummyQCData, QcData } from "./dummyQcTemplate"; import { dummyPutawayLine, dummyQCData, QcData } from "./dummyQcTemplate";
import { useGridApiRef } from "@mui/x-data-grid"; import { useGridApiRef } from "@mui/x-data-grid";
import {submitDialogWithWarning} from "../Swal/CustomAlerts"; import {submitDialogWithWarning} from "../Swal/CustomAlerts";
import { PurchaseQCInput, PutawayInput } from "@/app/api/dashboard/actions";
import { arrayToDateString, arrayToInputDateString, dayjsToInputDateString } from "@/app/utils/formatUtil";
import dayjs from "dayjs";


const style = { const style = {
position: "absolute", position: "absolute",
@@ -73,15 +76,18 @@ const PoQcStockInModalVer2: React.FC<Props> = ({
t, t,
i18n: { language }, i18n: { language },
} = useTranslation("purchaseOrder"); } = useTranslation("purchaseOrder");

const [qcItems, setQcItems] = useState(dummyQCData) const [qcItems, setQcItems] = useState(dummyQCData)
const formProps = useForm<ModalFormInput>({ const formProps = useForm<ModalFormInput>({
defaultValues: { defaultValues: {
...itemDetail, ...itemDetail,
dnDate: dayjsToInputDateString(dayjs()),
putawayLine: dummyPutawayLine, putawayLine: dummyPutawayLine,
// receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT), // receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT),
// warehouseId: itemDetail.defaultWarehouseId || 0 // warehouseId: itemDetail.defaultWarehouseId || 0
}, },
}); });

const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>(
(...args) => { (...args) => {
onClose?.(...args); onClose?.(...args);
@@ -89,13 +95,34 @@ const [qcItems, setQcItems] = useState(dummyQCData)
}, },
[onClose], [onClose],
); );
const isPutaway = () => {
if (itemDetail) {
const status = itemDetail.status;
return status == "received";

} else return false;
};

useEffect(() => {
formProps.reset({
...itemDetail,
dnDate: dayjsToInputDateString(dayjs()),
putawayLine: dummyPutawayLine,
})
setOpenPutaway(isPutaway);
}, [open])

const [openPutaway, setOpenPutaway] = useState(false); const [openPutaway, setOpenPutaway] = useState(false);
const onOpenPutaway = useCallback(() => { const onOpenPutaway = useCallback(() => {
setOpenPutaway(true); setOpenPutaway(true);
}, []); }, []);
const onClosePutaway = useCallback(() => { const onClosePutaway = useCallback(() => {
setOpenPutaway(false); setOpenPutaway(false);
}, []); }, []);

// Stock In submission handler // Stock In submission handler
const onSubmitStockIn = useCallback<SubmitHandler<ModalFormInput>>( const onSubmitStockIn = useCallback<SubmitHandler<ModalFormInput>>(
async (data, event) => { async (data, event) => {
@@ -118,14 +145,16 @@ const [qcItems, setQcItems] = useState(dummyQCData)
}, },
[], [],
); );

// QC submission handler // QC submission handler
const onSubmitQc = useCallback<SubmitHandler<ModalFormInput>>( const onSubmitQc = useCallback<SubmitHandler<ModalFormInput>>(
async (data, event) => { async (data, event) => {
console.log("QC Submission:", event!.nativeEvent); console.log("QC Submission:", event!.nativeEvent);
// TODO: Move validation into QC page
// Get QC data from the shared form context // Get QC data from the shared form context
const qcAccept = data.qcAccept; const qcAccept = data.qcAccept;
const acceptQty = data.acceptQty;
const acceptQty = data.acceptQty as number;
// Validate QC data // Validate QC data
const validationErrors : string[] = []; const validationErrors : string[] = [];
@@ -137,7 +166,7 @@ const [qcItems, setQcItems] = useState(dummyQCData)


// Check if failed items have failed quantity // Check if failed items have failed quantity
const failedItemsWithoutQty = qcItems.filter(item => const failedItemsWithoutQty = qcItems.filter(item =>
item.isPassed === false && (!item.failedQty || item.failedQty <= 0)
item.isPassed === false && (!item.failQty || item.failQty <= 0)
); );
if (failedItemsWithoutQty.length > 0) { if (failedItemsWithoutQty.length > 0) {
validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`); validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`);
@@ -153,6 +182,14 @@ const [qcItems, setQcItems] = useState(dummyQCData)
validationErrors.push("Accept quantity must be greater than 0"); validationErrors.push("Accept quantity must be greater than 0");
} }


// Check if dates are input
if (data.productionDate === undefined || data.productionDate == null) {
validationErrors.push("Production Date cannot be null!");
}
if (data.expiryDate === undefined || data.expiryDate == null) {
validationErrors.push("Expiry Date cannot be null!");
}

if (validationErrors.length > 0) { if (validationErrors.length > 0) {
console.error("QC Validation failed:", validationErrors); console.error("QC Validation failed:", validationErrors);
alert(`未完成品檢: ${validationErrors}`); alert(`未完成品檢: ${validationErrors}`);
@@ -160,35 +197,75 @@ const [qcItems, setQcItems] = useState(dummyQCData)
} }


const qcData = { const qcData = {
qcAccept: qcAccept,
acceptQty: acceptQty,
qcItems: qcItems.map(item => ({
id: item.id,
qcItem: item.qcItem,
qcDescription: item.qcDescription,
isPassed: item.isPassed,
failedQty: (item.failedQty && !item.isPassed) || 0,
dnNo : data.dnNo? data.dnNo : "DN00000",
dnDate : data.dnDate? arrayToInputDateString(data.dnDate) : dayjsToInputDateString(dayjs()),
productionDate : arrayToInputDateString(data.productionDate),
expiryDate : arrayToInputDateString(data.expiryDate),
receiptDate : arrayToInputDateString(data.receiptDate),
qcAccept: qcAccept? qcAccept : false,
acceptQty: acceptQty? acceptQty : 0,
qcResult: qcItems.map(item => ({
qcItemId: item.id,
// qcItem: item.qcItem,
// qcDescription: item.qcDescription,
isPassed: item.isPassed? item.isPassed : false,
failQty: (item.failQty && !item.isPassed) ? item.failQty : 0,
// failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0,
remarks: item.remarks || '' remarks: item.remarks || ''
})) }))
}; };
// const qcData = data; // const qcData = data;


console.log("QC Data for submission:", qcData); console.log("QC Data for submission:", qcData);
// await submitQcData(qcData);
if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) {
submitDialogWithWarning(onOpenPutaway, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""});
if (!qcData.qcResult.every((qc) => qc.isPassed) && qcData.qcAccept) {
submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?",
confirmButtonText: t("confirm putaway"), html: ""});
return; return;
} }

if (qcData.qcAccept) {
onOpenPutaway();
} else {
onClose();
}
await postStockInLineWithQc(qcData);
// return;
}, },
[onOpenPutaway, qcItems], [onOpenPutaway, qcItems],
); );
const postStockInLineWithQc = useCallback(async (qcData: PurchaseQCInput) => {
const args = {
...qcData
// id: itemDetail.id,
// purchaseOrderId: itemDetail.purchaseOrderId,
// purchaseOrderLineId: itemDetail.purchaseOrderLineId,
// itemId: itemDetail.itemId,
// ...data,
// productionDate: productionDate,
// expiryDate: expiryDate,
// receiptDate: receiptDate,
} as ModalFormInput;

await postStockInLine(args);

if (qcData.qcAccept) {
// submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?",
// confirmButtonText: t("confirm putaway"), html: ""});
onOpenPutaway();
} else {
closeHandler({}, "backdropClick");
}
return ;
},[onOpenPutaway,closeHandler]);

const postStockInLine = useCallback(async (args: ModalFormInput) => {
const submitData = {
...itemDetail, ...args
} as StockInLineEntry & ModalFormInput;
console.log(submitData);

const res = await updateStockInLine(submitData);
console.log("result ", res);
return res;
},[])

// Email supplier handler // Email supplier handler
const onSubmitEmailSupplier = useCallback<SubmitHandler<ModalFormInput>>( const onSubmitEmailSupplier = useCallback<SubmitHandler<ModalFormInput>>(
async (data, event) => { async (data, event) => {
@@ -222,12 +299,22 @@ const [qcItems, setQcItems] = useState(dummyQCData)
// binLocation: data.binLocation, // binLocation: data.binLocation,
// putawayQuantity: data.putawayQuantity, // putawayQuantity: data.putawayQuantity,
// putawayNotes: data.putawayNotes, // putawayNotes: data.putawayNotes,
data: data,
acceptQty: itemDetail.demandQty? itemDetail.demandQty : itemDetail.acceptedQty,
...data,
dnDate : data.dnDate? arrayToInputDateString(data.dnDate) : dayjsToInputDateString(dayjs()),
productionDate : arrayToInputDateString(data.productionDate),
expiryDate : arrayToInputDateString(data.expiryDate),
receiptDate : arrayToInputDateString(data.receiptDate),


// Add other putaway specific fields // Add other putaway specific fields
};
} as ModalFormInput;
console.log("Putaway Data:", putawayData); console.log("Putaway Data:", putawayData);

// Handle putaway submission logic here // Handle putaway submission logic here
const res = await postStockInLine(putawayData);
console.log("result ", res);
// Close modal after successful putaway // Close modal after successful putaway
closeHandler({}, "backdropClick"); closeHandler({}, "backdropClick");
}, },
@@ -239,6 +326,7 @@ const [qcItems, setQcItems] = useState(dummyQCData)
// Handle print logic here // Handle print logic here
window.print(); window.print();
}, []); }, []);
const acceptQty = formProps.watch("acceptedQty") const acceptQty = formProps.watch("acceptedQty")


const checkQcIsPassed = useCallback((qcItems: QcData[]) => { const checkQcIsPassed = useCallback((qcItems: QcData[]) => {
@@ -272,7 +360,7 @@ const [qcItems, setQcItems] = useState(dummyQCData)
marginRight: 3, marginRight: 3,
}} }}
> >
{openPutaway ? (
{openPutaway ? (
<Box <Box
component="form" component="form"
onSubmit={formProps.handleSubmit(onSubmitPutaway)} onSubmit={formProps.handleSubmit(onSubmitPutaway)}
@@ -299,6 +387,7 @@ const [qcItems, setQcItems] = useState(dummyQCData)
variant="contained" variant="contained"
color="primary" color="primary"
sx={{ mt: 1 }} sx={{ mt: 1 }}
onClick={formProps.handleSubmit(onSubmitPutaway)}
> >
{t("confirm putaway")} {t("confirm putaway")}
</Button> </Button>
@@ -320,7 +409,7 @@ const [qcItems, setQcItems] = useState(dummyQCData)
<StockInFormVer2 itemDetail={itemDetail} disabled={false} /> <StockInFormVer2 itemDetail={itemDetail} disabled={false} />
</Grid> </Grid>
</Grid> </Grid>
<Stack direction="row" justifyContent="flex-end" gap={1}>
{/* <Stack direction="row" justifyContent="flex-end" gap={1}>
<Button <Button
id="stockInSubmit" id="stockInSubmit"
type="button" type="button"
@@ -330,7 +419,7 @@ const [qcItems, setQcItems] = useState(dummyQCData)
> >
{t("submitStockIn")} {t("submitStockIn")}
</Button> </Button>
</Stack>
</Stack> */}
<Grid <Grid
container container
justifyContent="flex-start" justifyContent="flex-start"
@@ -345,7 +434,7 @@ const [qcItems, setQcItems] = useState(dummyQCData)
/> />
</Grid> </Grid>
<Stack direction="row" justifyContent="flex-end" gap={1}> <Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
{itemDetail.status.toLowerCase() == "rejected" && (<Button
id="emailSupplier" id="emailSupplier"
type="button" type="button"
variant="contained" variant="contained"
@@ -354,8 +443,8 @@ const [qcItems, setQcItems] = useState(dummyQCData)
onClick={formProps.handleSubmit(onSubmitEmailSupplier)} onClick={formProps.handleSubmit(onSubmitEmailSupplier)}
> >
{t("email supplier")} {t("email supplier")}
</Button>
<Button
</Button>)}
{itemDetail.status.toLowerCase() != "completed" && (<Button
id="qcSubmit" id="qcSubmit"
type="button" type="button"
variant="contained" variant="contained"
@@ -363,8 +452,8 @@ const [qcItems, setQcItems] = useState(dummyQCData)
sx={{ mt: 1 }} sx={{ mt: 1 }}
onClick={formProps.handleSubmit(onSubmitQc)} onClick={formProps.handleSubmit(onSubmitQc)}
> >
{t("confirm putaway")}
</Button>
{t("confirm qc result")}
</Button>)}
</Stack> </Stack>
</> </>
)} )}


+ 6
- 5
src/components/PoDetail/StockInFormVer2.tsx View File

@@ -123,7 +123,7 @@ const StockInFormVer2: React.FC<Props> = ({
{...register("dnNo", { {...register("dnNo", {
// required: "productLotNo required!", // required: "productLotNo required!",
})} })}
disabled={true}
disabled={itemDetail.status.toLowerCase() == "completed"}
// error={Boolean(errors.productLotNo)} // error={Boolean(errors.productLotNo)}
// helperText={errors.productLotNo?.message} // helperText={errors.productLotNo?.message}
/> />
@@ -205,7 +205,7 @@ const StockInFormVer2: React.FC<Props> = ({
{...register("productLotNo", { {...register("productLotNo", {
// required: "productLotNo required!", // required: "productLotNo required!",
})} })}
disabled={disabled}
disabled={itemDetail.status.toLowerCase() == "completed"}
error={Boolean(errors.productLotNo)} error={Boolean(errors.productLotNo)}
helperText={errors.productLotNo?.message} helperText={errors.productLotNo?.message}
/> />
@@ -226,7 +226,7 @@ const StockInFormVer2: React.FC<Props> = ({
sx={{ width: "100%" }} sx={{ width: "100%" }}
label={t("productionDate")} label={t("productionDate")}
value={productionDate ? dayjs(productionDate) : undefined} value={productionDate ? dayjs(productionDate) : undefined}
disabled={disabled}
disabled={itemDetail.status.toLowerCase() == "completed"}
onChange={(date) => { onChange={(date) => {
if (!date) return; if (!date) return;
setValue( setValue(
@@ -275,7 +275,7 @@ const StockInFormVer2: React.FC<Props> = ({
sx={{ width: "100%" }} sx={{ width: "100%" }}
label={t("expiryDate")} label={t("expiryDate")}
value={expiryDate ? dayjs(expiryDate) : undefined} value={expiryDate ? dayjs(expiryDate) : undefined}
disabled={disabled}
disabled={itemDetail.status.toLowerCase() == "completed"}
onChange={(date) => { onChange={(date) => {
console.log(date); console.log(date);
if (!date) return; if (!date) return;
@@ -322,10 +322,11 @@ const StockInFormVer2: React.FC<Props> = ({
<TextField <TextField
label={t("acceptedQty")} label={t("acceptedQty")}
fullWidth fullWidth
disabled={itemDetail.status.toLowerCase() == "completed"}
{...register("acceptedQty", { {...register("acceptedQty", {
required: "acceptedQty required!", required: "acceptedQty required!",
})} })}
disabled={true}
// disabled={true}
// disabled={disabled} // disabled={disabled}
// error={Boolean(errors.acceptedQty)} // error={Boolean(errors.acceptedQty)}
// helperText={errors.acceptedQty?.message} // helperText={errors.acceptedQty?.message}


+ 6
- 6
src/components/PoDetail/dummyQcTemplate.tsx View File

@@ -5,7 +5,7 @@ export interface QcData {
qcItem: string, qcItem: string,
qcDescription: string, qcDescription: string,
isPassed: boolean | undefined isPassed: boolean | undefined
failedQty: number | undefined
failQty: number | undefined
remarks: string | undefined remarks: string | undefined
} }


@@ -15,7 +15,7 @@ export const dummyQCData: QcData[] = [
qcItem: "包裝", qcItem: "包裝",
qcDescription: "有破爛、污糟、脹袋、積水、與實物不符等任何一種情況,則不合格", qcDescription: "有破爛、污糟、脹袋、積水、與實物不符等任何一種情況,則不合格",
isPassed: undefined, isPassed: undefined,
failedQty: undefined,
failQty: undefined,
remarks: undefined, remarks: undefined,
}, },
{ {
@@ -23,7 +23,7 @@ export const dummyQCData: QcData[] = [
qcItem: "肉質", qcItem: "肉質",
qcDescription: "肉質鬆散,則不合格", qcDescription: "肉質鬆散,則不合格",
isPassed: undefined, isPassed: undefined,
failedQty: undefined,
failQty: undefined,
remarks: undefined, remarks: undefined,
}, },
{ {
@@ -31,7 +31,7 @@ export const dummyQCData: QcData[] = [
qcItem: "顔色", qcItem: "顔色",
qcDescription: "不是食材應有的顔色、顔色不均匀、出現其他顔色、腌料/醬顔色不均匀,油脂部分變綠色、黃色,", qcDescription: "不是食材應有的顔色、顔色不均匀、出現其他顔色、腌料/醬顔色不均匀,油脂部分變綠色、黃色,",
isPassed: undefined, isPassed: undefined,
failedQty: undefined,
failQty: undefined,
remarks: undefined, remarks: undefined,
}, },
{ {
@@ -39,7 +39,7 @@ export const dummyQCData: QcData[] = [
qcItem: "狀態", qcItem: "狀態",
qcDescription: "有結晶、結霜、解凍跡象、發霉、散發異味等任何一種情況,則不合格", qcDescription: "有結晶、結霜、解凍跡象、發霉、散發異味等任何一種情況,則不合格",
isPassed: undefined, isPassed: undefined,
failedQty: undefined,
failQty: undefined,
remarks: undefined, remarks: undefined,
}, },
{ {
@@ -47,7 +47,7 @@ export const dummyQCData: QcData[] = [
qcItem: "異物", qcItem: "異物",
qcDescription: "有不屬於本食材的雜質,則不合格", qcDescription: "有不屬於本食材的雜質,則不合格",
isPassed: undefined, isPassed: undefined,
failedQty: undefined,
failQty: undefined,
remarks: undefined, remarks: undefined,
}, },
] ]


+ 1
- 0
src/components/Swal/CustomAlerts.tsx View File

@@ -126,6 +126,7 @@ export const submitDialogWithWarning = async (
title: t("Do you want to submit?") as SweetAlertTitle, title: t("Do you want to submit?") as SweetAlertTitle,
html: t("Warning!") as SweetAlertHtml, html: t("Warning!") as SweetAlertHtml,
confirmButtonText: t("Submit") as SweetAlertConfirmButtonText, confirmButtonText: t("Submit") as SweetAlertConfirmButtonText,
// cancelButtonText: t("Cancel") as SweetAlertConfirmButtonText,
}, },
) => { ) => {
// console.log(props) // console.log(props)


+ 2
- 2
src/i18n/index.tsx View File

@@ -6,8 +6,8 @@ import { authOptions } from "@/config/authConfig";
import I18nClientProvider from "./I18nClientProvider"; import I18nClientProvider from "./I18nClientProvider";
import universalLanguageDetect from "@unly/universal-language-detector"; import universalLanguageDetect from "@unly/universal-language-detector";


const FALLBACK_LANG = "en";
const SUPPORTED_LANGUAGES = ["en", "zh"];
const FALLBACK_LANG = "zh";
const SUPPORTED_LANGUAGES = ["zh"];


export const detectLanguage = async (): Promise<string> => { export const detectLanguage = async (): Promise<string> => {
// Logic to get language preference from cookies/headers/session // Logic to get language preference from cookies/headers/session


+ 4
- 0
src/i18n/zh/dashboard.json View File

@@ -14,6 +14,10 @@
"Humidity status": "濕度狀態", "Humidity status": "濕度狀態",
"Warehouse status": "倉庫狀態", "Warehouse status": "倉庫狀態",
"Progress chart": "進度圖表", "Progress chart": "進度圖表",
"Purchase Order Code": "採購單號",
"Item Name": "貨品名稱",
"Escalation Level": "上報等級",
"Reason": "原因",
"Order completion": "訂單完成度", "Order completion": "訂單完成度",
"Raw material": "原料", "Raw material": "原料",
"Consumable": "消耗品", "Consumable": "消耗品",


+ 9
- 4
src/i18n/zh/purchaseOrder.json View File

@@ -19,6 +19,7 @@
"Start Fail": "開始失敗", "Start Fail": "開始失敗",
"Start PO": "開始採購訂單", "Start PO": "開始採購訂單",
"Do you want to complete?": "確定完成嗎?", "Do you want to complete?": "確定完成嗎?",
"Cancel": "取消",
"Complete": "完成", "Complete": "完成",
"Complete Success": "完成成功", "Complete Success": "完成成功",
"Complete Fail": "完成失敗", "Complete Fail": "完成失敗",
@@ -64,9 +65,9 @@
"determine2": "上報2", "determine2": "上報2",
"determine3": "上報3", "determine3": "上報3",
"receiving": "收貨中", "receiving": "收貨中",
"received": "已收",
"completed": "已完成",
"rejected": "已拒絕",
"received": "已收",
"completed": "已上架",
"rejected": "已拒絕及上報",
"status": "狀態", "status": "狀態",


"acceptedQty must not greater than": "接受數量不得大於", "acceptedQty must not greater than": "接受數量不得大於",
@@ -107,6 +108,9 @@


"Accept submit": "接受來貨", "Accept submit": "接受來貨",
"qc processing": "處理來貨及品檢", "qc processing": "處理來貨及品檢",
"putaway processing": "處理來貨及上架",
"view stockin": "查看收貨及品檢",
"putaway processing": "處理來貨及上架",
"putawayBtn": "上架", "putawayBtn": "上架",
"dnNo": "送貨單編號", "dnNo": "送貨單編號",
"dnDate": "送貨單日期", "dnDate": "送貨單日期",
@@ -120,9 +124,10 @@
"update qc info": "更新品檢資料", "update qc info": "更新品檢資料",
"email supplier": "電郵供應商", "email supplier": "電郵供應商",
"confirm putaway": "確定及上架", "confirm putaway": "確定及上架",
"confirm qc result": "確定品檢結果",
"warehouse": "倉庫", "warehouse": "倉庫",
"qcItem": "檢項目",
"qcItem": "檢項目",
"passed": "接受", "passed": "接受",
"failed": "不接受", "failed": "不接受",
"failedQty": "不合格數", "failedQty": "不合格數",


Loading…
Cancel
Save