@@ -29,7 +29,7 @@ const PoEdit: React.FC<Props> = async ({ searchParams }) => { | |||
return ( | |||
<> | |||
{/* <Typography variant="h4">{t("Create Material")}</Typography> */} | |||
<I18nProvider namespaces={[type]}> | |||
<I18nProvider namespaces={[type, "dashboard"]}> | |||
<Suspense fallback={<PoDetail.Loading />}> | |||
<PoDetail id={id} /> | |||
</Suspense> | |||
@@ -0,0 +1,21 @@ | |||
"use server" | |||
import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | |||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
import { cache } from "react"; | |||
import { EscalationResult } from "."; | |||
export const fetchEscalationLogsByStockInLines = cache(async(stockInLineIds: number[]) => { | |||
const searchParams = convertObjToURLSearchParams({stockInLineIds: stockInLineIds}) | |||
return serverFetchJson<EscalationResult[]>(`${BASE_API_URL}/escalationLog/stockInLines?${searchParams}`, | |||
{ | |||
method: "GET", | |||
headers: { "Content-Type": "application/json" }, | |||
next: { | |||
tags: ["escalationLogs"], | |||
}, | |||
}, | |||
); | |||
}); |
@@ -32,19 +32,6 @@ export interface EscalationResult { | |||
dnNo?: string; | |||
} | |||
export const fetchEscalationLogsByStockInLines = cache(async(stockInLineIds: number[]) => { | |||
const searchParams = convertObjToURLSearchParams({stockInLineIds: stockInLineIds}) | |||
return serverFetchJson<EscalationResult[]>(`${BASE_API_URL}/escalationLog/stockInLines?${searchParams}`, | |||
{ | |||
method: "GET", | |||
headers: { "Content-Type": "application/json" }, | |||
next: { | |||
tags: ["escalationLogs"], | |||
}, | |||
}, | |||
); | |||
}); | |||
export const fetchEscalationLogsByUser = cache(async() => { | |||
return serverFetchJson<EscalationResult[]>(`${BASE_API_URL}/escalationLog/user`, | |||
{ | |||
@@ -38,8 +38,8 @@ export interface StockInLineEntry { | |||
export interface PurchaseQcResult{ | |||
qcItemId: number; | |||
qcPassed: boolean; | |||
failQty: number; | |||
qcPassed?: boolean; | |||
failQty?: number; | |||
remarks?: string; | |||
} | |||
@@ -69,14 +69,16 @@ export interface PurchaseQCInput { | |||
sampleWeight: number; | |||
totalWeight: number; | |||
qcAccept: boolean; | |||
qcDecision?: number; | |||
qcResult: PurchaseQcResult[]; | |||
} | |||
export interface EscalationInput { | |||
status: string; | |||
remarks?: string; | |||
handler: string; | |||
productLotNo: string; | |||
acceptedQty: number; // this is the qty to be escalated | |||
reason?: string; | |||
handlerId: number; | |||
productLotNo?: string; | |||
acceptedQty?: number; // this is the qty to be escalated | |||
// escalationQty: number | |||
} | |||
export interface PutawayLine { | |||
@@ -94,8 +96,10 @@ export interface PutawayInput { | |||
} | |||
export type ModalFormInput = Partial< | |||
PurchaseQCInput & StockInInput & EscalationInput & PutawayInput | |||
>; | |||
PurchaseQCInput & StockInInput & PutawayInput | |||
> & { | |||
escalationLog? : Partial<EscalationInput> | |||
}; | |||
export const testFetch = cache(async (id: number) => { | |||
return serverFetchJson<PoResult>(`${BASE_API_URL}/po/detail/${id}`, { | |||
@@ -81,6 +81,7 @@ export interface StockInLine { | |||
dnNo?: string; | |||
dnDate?: number[]; | |||
stockQty?: number; | |||
handlerId?: number; | |||
} | |||
export const fetchPoList = cache(async (queryParams?: Record<string, any>) => { | |||
@@ -32,6 +32,8 @@ export const INPUT_DATE_FORMAT = "YYYY-MM-DD"; | |||
export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD"; | |||
export const INPUT_TIME_FORMAT = "HH:mm:ss"; | |||
export const OUTPUT_TIME_FORMAT = "HH:mm:ss"; | |||
export const arrayToDayjs = (arr: ConfigType | (number | undefined)[]) => { | |||
@@ -73,6 +75,10 @@ export const dayjsToDateString = (date: Dayjs) => { | |||
}; | |||
export const dayjsToInputDateString = (date: Dayjs) => { | |||
return date.format(INPUT_DATE_FORMAT + "T" + INPUT_TIME_FORMAT); | |||
}; | |||
export const dayjsToInputDatetimeString = (date: Dayjs) => { | |||
return date.format(INPUT_DATE_FORMAT); | |||
}; | |||
@@ -62,40 +62,57 @@ const EscalationLogTable: React.FC<Props> = ({ | |||
() => [ | |||
{ | |||
name: "handler", | |||
label: t("Responsible for handling colleagues") | |||
label: t("Responsible for handling colleagues"), | |||
sx: { width: "20%", minWidth: 200, maxWidth: 500 }, | |||
}, | |||
{ | |||
name: "acceptedQty", | |||
label: t("Received Qty"), | |||
align: "right", | |||
headerAlign: "right" | |||
headerAlign: "right", | |||
sx: { width: "10%", minWidth: 100 }, | |||
}, | |||
{ | |||
name: "purchaseUomDesc", | |||
label: t("Purchase UoM") | |||
label: t("Purchase UoM"), | |||
sx: { width: "15%", minWidth: 120 }, | |||
}, | |||
{ | |||
name: "dnDate", | |||
label: t("DN Date"), | |||
sx: { width: "10%", minWidth: 120 }, | |||
renderCell: (params) => { | |||
return params.dnDate ? arrayToDateString(params.dnDate) : "N/A" | |||
} | |||
}, | |||
{ | |||
name: "qcTotalCount", | |||
label: t("QC Completed Count"), | |||
align: "right", | |||
headerAlign: "right" | |||
}, | |||
{ | |||
name: "qcFailCount", | |||
label: t("QC Fail Count"), | |||
align: "right", | |||
headerAlign: "right" | |||
headerAlign: "right", | |||
sx: { width: "15%", minWidth: 120 }, | |||
renderCell: (params) => { | |||
return `${params.qcFailCount} / ${params.qcTotalCount}` | |||
} | |||
}, | |||
// { | |||
// name: "qcTotalCount", | |||
// label: t("QC Completed Count"), | |||
// align: "right", | |||
// headerAlign: "right" | |||
// flex: 1, | |||
// }, | |||
// { | |||
// name: "qcFailCount", | |||
// label: t("QC Fail Count"), | |||
// align: "right", | |||
// headerAlign: "right" | |||
// flex: 1, | |||
// }, | |||
{ | |||
name: "reason", | |||
label: t("Reason"), | |||
sx: { width: "30%", minWidth: 150 }, | |||
}, | |||
], []) | |||
@@ -21,6 +21,8 @@ import { SelectChangeEvent } from '@mui/material/Select'; | |||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||
import ExpandLessIcon from '@mui/icons-material/ExpandLess'; | |||
import { useTranslation } from 'react-i18next'; | |||
import { useFormContext } from 'react-hook-form'; | |||
import { EscalationInput, ModalFormInput } from '@/app/api/po/actions'; | |||
interface NameOption { | |||
value: string; | |||
@@ -38,6 +40,7 @@ interface Props { | |||
isCollapsed: boolean | |||
setIsCollapsed: Dispatch<React.SetStateAction<boolean>> | |||
} | |||
const EscalationComponent: React.FC<Props> = ({ | |||
forSupervisor, | |||
isCollapsed, | |||
@@ -60,6 +63,19 @@ const EscalationComponent: React.FC<Props> = ({ | |||
{ value: 'david', label: '林建國' }, | |||
]; | |||
const { | |||
register, | |||
formState: { errors, defaultValues, touchedFields }, | |||
watch, | |||
control, | |||
setValue, | |||
getValues, | |||
reset, | |||
resetField, | |||
setError, | |||
clearErrors, | |||
} = useFormContext<ModalFormInput>(); | |||
const handleInputChange = ( | |||
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | SelectChangeEvent<string> | |||
): void => { | |||
@@ -70,7 +86,7 @@ const EscalationComponent: React.FC<Props> = ({ | |||
})); | |||
}; | |||
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => { | |||
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {console.log("called this?"); | |||
e.preventDefault(); | |||
console.log('表單已提交:', formData); | |||
// 處理表單提交 | |||
@@ -118,9 +134,12 @@ const EscalationComponent: React.FC<Props> = ({ | |||
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> | |||
<FormControl fullWidth> | |||
<select | |||
id="name" | |||
name="name" | |||
value={formData.name} | |||
id="handlerId" | |||
// name="name" | |||
// value={formData.name} | |||
{...register("escalationLog.handlerId", { | |||
required: "handler required!", | |||
})} | |||
onChange={handleInputChange} | |||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white" | |||
> | |||
@@ -169,12 +188,15 @@ const EscalationComponent: React.FC<Props> = ({ | |||
<TextField | |||
fullWidth | |||
id="message" | |||
name="message" | |||
id="reason" | |||
// name="reason" | |||
{...register("escalationLog.reason", { | |||
required: "reason required!", | |||
})} | |||
label="上報原因" | |||
multiline | |||
rows={4} | |||
value={formData.message} | |||
// value={formData.reason} | |||
onChange={handleInputChange} | |||
placeholder="請輸入上報原因" | |||
/> | |||
@@ -9,6 +9,7 @@ import { | |||
GridRowModes, | |||
GridRowModesModel, | |||
GridToolbarContainer, | |||
GridValidRowModel, | |||
useGridApiRef, | |||
} from "@mui/x-data-grid"; | |||
import { | |||
@@ -58,8 +59,12 @@ import { fetchQcResult } from "@/app/api/qc/actions"; | |||
import PoQcStockInModal from "./PoQcStockInModal"; | |||
import DoDisturbIcon from "@mui/icons-material/DoDisturb"; | |||
import { useSession } from "next-auth/react"; | |||
// import { SessionWithTokens } from "src/config/authConfig"; | |||
import PoQcStockInModalVer2 from "./QcStockInModalVer2"; | |||
import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | |||
import { EscalationResult } from "@/app/api/escalation"; | |||
import { fetchEscalationLogsByStockInLines } from "@/app/api/escalation/actions"; | |||
import { SessionWithTokens } from "@/config/authConfig"; | |||
interface ResultWithId { | |||
id: number; | |||
@@ -130,7 +135,7 @@ function PoInputGrid({ | |||
setEntries(stockInLine) | |||
}, [stockInLine]) | |||
const [modalInfo, setModalInfo] = useState< | |||
StockInLine & { qcResult?: PurchaseQcResult[] } | |||
StockInLine & { qcResult?: PurchaseQcResult[] } & { escalationResult?: EscalationResult[] } | |||
>(); | |||
const pathname = usePathname() | |||
const router = useRouter(); | |||
@@ -151,6 +156,7 @@ function PoInputGrid({ | |||
}); | |||
const { data: session } = useSession(); | |||
const sessionToken = session as SessionWithTokens | null; | |||
useEffect(() => { | |||
const completedList = entries.filter( | |||
@@ -245,7 +251,7 @@ function PoInputGrid({ | |||
return await fetchQcResult(stockInLineId as number); | |||
}, []); | |||
const handleQC = useCallback( | |||
const handleQC = useCallback( // UNUSED NOW! | |||
(id: GridRowId, params: any) => async () => { | |||
setBtnIsLoading(true); | |||
setRowModesModel((prev) => ({ | |||
@@ -253,8 +259,10 @@ function PoInputGrid({ | |||
[id]: { mode: GridRowModes.View }, | |||
})); | |||
const qcResult = await fetchQcDefaultValue(id); | |||
const escResult = await fetchEscalationLogsByStockInLines([Number(id)]); | |||
console.log(params.row); | |||
console.log(qcResult); | |||
setModalInfo({ | |||
...params.row, | |||
qcResult: qcResult, | |||
@@ -273,6 +281,7 @@ function PoInputGrid({ | |||
const [newOpen, setNewOpen] = useState(false); | |||
const stockInLineId = searchParams.get("stockInLineId"); | |||
const poLineId = searchParams.get("poLineId"); | |||
const closeNewModal = useCallback(() => { | |||
const newParams = new URLSearchParams(searchParams.toString()); | |||
newParams.delete("stockInLineId"); // Remove the parameter | |||
@@ -300,9 +309,12 @@ const closeNewModal = useCallback(() => { | |||
})); | |||
const qcResult = await fetchQcDefaultValue(id); | |||
const escResult = await fetchEscalationLogsByStockInLines([Number(id)]); | |||
setModalInfo(() => ({ | |||
...params.row, | |||
qcResult: qcResult, | |||
escResult: escResult, | |||
receivedQty: itemDetail.receivedQty, | |||
})); | |||
@@ -424,10 +436,14 @@ const closeNewModal = useCallback(() => { | |||
[], | |||
); | |||
const getButtonSx = (status : string) => { | |||
const getButtonSx = (sil : StockInLine) => { | |||
const status = sil?.status?.toLowerCase(); | |||
let btnSx = {label:"", color:""}; | |||
switch (status) { | |||
case "received": btnSx = {label: t("putaway processing"), color:"secondary.main"}; break; | |||
case "escalated": if (sessionToken?.id == sil?.handlerId) { | |||
btnSx = {label: t("escalation processing"), color:"warning.main"}; | |||
break;} | |||
case "rejected": | |||
case "completed": btnSx = {label: t("view stockin"), color:"info.main"}; break; | |||
default: btnSx = {label: t("qc processing"), color:"success.main"}; | |||
@@ -476,7 +492,7 @@ const closeNewModal = useCallback(() => { | |||
headerName: t("dnDate"), | |||
width: 125, | |||
renderCell: (params) => { | |||
console.log(params.row) | |||
// console.log(params.row) | |||
// return <>07/08/2025</> | |||
return arrayToDateString(params.value) | |||
} | |||
@@ -511,7 +527,7 @@ const closeNewModal = useCallback(() => { | |||
width: 120, | |||
// flex: 0.5, | |||
renderCell: (params) => { | |||
return params.row.uom.code; | |||
return params.row.uom?.code; | |||
}, | |||
}, | |||
{ | |||
@@ -557,7 +573,12 @@ const closeNewModal = useCallback(() => { | |||
width: 140, | |||
// flex: 0.5, | |||
renderCell: (params) => { | |||
return t(`${params.row.status}`); | |||
const handlerId = params.row.handlerId | |||
const status = params.row.status | |||
return (<span style={{ | |||
color: (status == "escalated")? "red":"inherit"}}> | |||
{t(`${params.row.status}`)} | |||
</span>); | |||
}, | |||
}, | |||
{ | |||
@@ -572,9 +593,9 @@ const closeNewModal = useCallback(() => { | |||
// flex: 2, | |||
cellClassName: "actions", | |||
getActions: (params) => { | |||
const data = params.row; | |||
// console.log(params.row.status); | |||
const status = params.row.status.toLowerCase(); | |||
const btnSx = getButtonSx(status); | |||
const btnSx = getButtonSx(data); | |||
// console.log(stockInLineStatusMap[status]); | |||
// console.log(session?.user?.abilities?.includes("APPROVAL")); | |||
return [ | |||
@@ -754,7 +775,7 @@ const closeNewModal = useCallback(() => { | |||
}, | |||
}, | |||
], | |||
[t, handleStart, handleQC, handleEscalation, session?.user?.abilities, handleStockIn, handlePutAway, handleDelete, handleReject, itemDetail], | |||
[t, handleStart, handleQC, handleEscalation, handleStockIn, handlePutAway, handleDelete, handleReject, itemDetail], | |||
); | |||
const addRow = useCallback(() => { | |||
@@ -911,6 +932,7 @@ const closeNewModal = useCallback(() => { | |||
setEntries={setEntries} | |||
setStockInLine={setStockInLine} | |||
setItemDetail={setModalInfo} | |||
session={sessionToken} | |||
qc={qc} | |||
warehouse={warehouse} | |||
open={newOpen} | |||
@@ -921,7 +943,7 @@ const closeNewModal = useCallback(() => { | |||
</> | |||
) | |||
} | |||
{modalInfo !== undefined && ( | |||
{/* {modalInfo !== undefined && ( | |||
<> | |||
<PoQcStockInModal | |||
type={"qc"} | |||
@@ -995,7 +1017,7 @@ const closeNewModal = useCallback(() => { | |||
itemDetail={modalInfo} | |||
/> | |||
</> | |||
)} | |||
)} */} | |||
</> | |||
); | |||
} | |||
@@ -52,9 +52,11 @@ import { dummyEscalationHistory, dummyQCData } from "./dummyQcTemplate"; | |||
import { ModalFormInput } from "@/app/api/po/actions"; | |||
import { escape } from "lodash"; | |||
import { PanoramaSharp } from "@mui/icons-material"; | |||
import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; | |||
import { EscalationResult } from "@/app/api/escalation"; | |||
interface Props { | |||
itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] }; | |||
itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
qc: QcItemWithChecks[]; | |||
disabled: boolean; | |||
qcItems: QcData[] | |||
@@ -90,6 +92,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
const [escalationHistory, setEscalationHistory] = useState(dummyEscalationHistory); | |||
// const [qcResult, setQcResult] = useState(); | |||
const qcAccept = watch("qcAccept"); | |||
const qcDecision = watch("qcDecision"); //WIP | |||
const qcResult = watch("qcResult"); | |||
console.log(qcResult); | |||
// const [qcAccept, setQcAccept] = useState(true); | |||
@@ -119,28 +122,29 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
//// validate form | |||
const accQty = watch("acceptQty"); | |||
const validateForm = useCallback(() => { | |||
console.log(accQty); | |||
if (accQty > itemDetail.acceptedQty) { | |||
setError("acceptQty", { | |||
message: `${t("acceptQty must not greater than")} ${ | |||
itemDetail.acceptedQty | |||
}`, | |||
type: "required", | |||
}); | |||
if (qcDecision == 1) { | |||
if (accQty > itemDetail.acceptedQty) { | |||
setError("acceptQty", { | |||
message: `${t("acceptQty must not greater than")} ${ | |||
itemDetail.acceptedQty | |||
}`, | |||
type: "required", | |||
}); | |||
} | |||
if (accQty < 1) { | |||
setError("acceptQty", { | |||
message: t("minimal value is 1"), | |||
type: "required", | |||
}); | |||
} | |||
if (isNaN(accQty)) { | |||
setError("acceptQty", { | |||
message: t("value must be a number"), | |||
type: "required", | |||
}); | |||
} | |||
} | |||
if (accQty < 1) { | |||
setError("acceptQty", { | |||
message: t("minimal value is 1"), | |||
type: "required", | |||
}); | |||
} | |||
if (isNaN(accQty)) { | |||
setError("acceptQty", { | |||
message: t("value must be a number"), | |||
type: "required", | |||
}); | |||
} | |||
}, [accQty]); | |||
}, [accQty, qcDecision]); | |||
useEffect(() => { | |||
clearErrors(); | |||
@@ -191,7 +195,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
flex: 2, | |||
renderCell: (params) => ( | |||
<Box> | |||
<b>{params.value}</b><br/> | |||
<b>{`${params.api.getRowIndexRelativeToVisibleRows(params.id) + 1}. ${params.value}`}</b><br/> | |||
{params.row.name}<br/> | |||
</Box> | |||
), | |||
@@ -202,7 +206,8 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
flex: 1.5, | |||
renderCell: (params) => { | |||
const currentValue = params.row; | |||
console.log(currentValue.row); | |||
const index = params.api.getRowIndexRelativeToVisibleRows(params.id); | |||
// console.log(currentValue.row); | |||
return ( | |||
<FormControl> | |||
<RadioGroup | |||
@@ -215,6 +220,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
setQcItems((prev) => | |||
prev.map((r): QcData => (r.id === params.id ? { ...r, qcPassed: value === "true" } : r)) | |||
); | |||
// setValue(`qcResult.${index}.qcPassed`, value == "true"); | |||
}} | |||
name={`qcPassed-${params.id}`} | |||
> | |||
@@ -222,7 +228,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
value="true" | |||
control={<Radio />} | |||
label="合格" | |||
disabled={disabled} | |||
disabled={disabled || itemDetail.status == "escalated"} | |||
sx={{ | |||
color: currentValue.qcPassed === true ? "green" : "inherit", | |||
"& .Mui-checked": {color: "green"} | |||
@@ -232,7 +238,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
value="false" | |||
control={<Radio />} | |||
label="不合格" | |||
disabled={disabled} | |||
disabled={disabled || itemDetail.status == "escalated"} | |||
sx={{ | |||
color: currentValue.qcPassed === false ? "red" : "inherit", | |||
"& .Mui-checked": {color: "red"} | |||
@@ -253,7 +259,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
type="number" | |||
size="small" | |||
value={!params.row.qcPassed? (params.value ?? '') : '0'} | |||
disabled={params.row.qcPassed || disabled} | |||
disabled={params.row.qcPassed || disabled || itemDetail.status == "escalated"} | |||
onChange={(e) => { | |||
const v = e.target.value; | |||
const next = v === '' ? undefined : Number(v); | |||
@@ -279,7 +285,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
<TextField | |||
size="small" | |||
value={params.value ?? ''} | |||
disabled={disabled} | |||
disabled={disabled || itemDetail.status == "escalated"} | |||
onChange={(e) => { | |||
const remarks = e.target.value; | |||
// const next = v === '' ? undefined : Number(v); | |||
@@ -331,13 +337,29 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
useEffect(() => { | |||
console.log("ItemDetail in QC:", itemDetail); | |||
console.log("Qc Result in QC:", qcResult); | |||
}, [itemDetail]); | |||
const setQcDecision = (status : string | undefined) => { | |||
const param = status?.toLowerCase(); | |||
if (param !== undefined && param !== null) { | |||
if (param == "completed") { | |||
return 1; | |||
} else if (param == "rejected") { | |||
return 2; | |||
} else if (param == "escalated") { | |||
return 3; | |||
} else { return undefined; } | |||
} else { | |||
return undefined; | |||
} | |||
} | |||
useEffect(() => { | |||
// onFailedOpenCollapse(qcItems) // This function is no longer needed | |||
}, [qcItems]); // Removed onFailedOpenCollapse from dependency array | |||
}, [qcItems]); | |||
return ( | |||
<> | |||
@@ -362,6 +384,19 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
{tabIndex == 0 && ( | |||
<> | |||
<Grid item xs={12}> | |||
<Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}> | |||
<Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}> | |||
Group A - 急凍貨類 (QCA1-MEAT01) | |||
</Typography> | |||
<Typography variant="subtitle1" sx={{ color: '#666' }}> | |||
<b>品檢類型</b>:IQC | |||
</Typography> | |||
<Typography variant="subtitle2" sx={{ color: '#666' }}> | |||
記錄探測溫度的時間,請在1小時内完成卸貨盤點入庫,以保障食品安全<br/> | |||
監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標 | |||
</Typography> | |||
</Box> | |||
{/* <QcDataGrid<ModalFormInput, QcData, EntryError> | |||
apiRef={apiRef} | |||
columns={qcColumns} | |||
@@ -370,7 +405,8 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
/> */} | |||
<StyledDataGrid | |||
columns={qcColumns} | |||
rows={disabled? qcResult:qcItems} | |||
rows={qcResult && qcResult.length > 0 ? qcResult : qcItems} | |||
// rows={disabled? qcResult:qcItems} | |||
autoHeight | |||
/> | |||
</Grid> | |||
@@ -384,52 +420,58 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
disabled={false} | |||
/> | |||
</Grid> */} | |||
<Grid item xs={12}> | |||
{/* <Grid item xs={12}> | |||
<Typography variant="h6" display="block" marginBlockEnd={1}> | |||
{t("Escalation Info")} | |||
</Typography> | |||
</Grid> | |||
</Grid> */} | |||
<Grid item xs={12}> | |||
<StyledDataGrid | |||
<EscalationLogTable items={itemDetail.escResult || []}/> | |||
{/* <StyledDataGrid | |||
rows={escalationHistory} | |||
columns={columns} | |||
onRowSelectionModelChange={(newRowSelectionModel) => { | |||
setRowSelectionModel(newRowSelectionModel); | |||
}} | |||
/> | |||
/> */} | |||
</Grid> | |||
</> | |||
)} | |||
<Grid item xs={12}> | |||
<FormControl> | |||
<Controller | |||
name="qcAccept" | |||
name="qcDecision" | |||
// name="qcAccept" | |||
control={control} | |||
defaultValue={true} | |||
defaultValue={setQcDecision(itemDetail?.status)} | |||
// defaultValue={true} | |||
render={({ field }) => ( | |||
<RadioGroup | |||
row | |||
aria-labelledby="demo-radio-buttons-group-label" | |||
{...field} | |||
value={field.value?.toString() || "true"} | |||
value={field.value} | |||
// value={field.value?.toString() || "true"} | |||
onChange={(e) => { | |||
const value = e.target.value === 'true'; | |||
if (!value && Boolean(errors.acceptQty)) { | |||
setValue("acceptQty", itemDetail.acceptedQty); | |||
const value = e.target.value.toString();// === 'true'; | |||
if (value != "1" && Boolean(errors.acceptQty)) { | |||
// if (!value && Boolean(errors.acceptQty)) { | |||
setValue("acceptQty", itemDetail.acceptedQty ?? 0); | |||
} | |||
field.onChange(value); | |||
}} | |||
> | |||
<FormControlLabel disabled={disabled} | |||
value="true" control={<Radio />} label="接受" /> | |||
value="1" control={<Radio />} label="接受" /> | |||
<Box sx={{mr:2}}> | |||
<TextField | |||
type="number" | |||
label={t("acceptQty")} | |||
sx={{ width: '150px' }} | |||
value={qcAccept? accQty : 0 } | |||
defaultValue={accQty} | |||
disabled={!qcAccept || disabled} | |||
value={(qcDecision == 1)? accQty : 0 } | |||
// value={qcAccept? accQty : 0 } | |||
disabled={qcDecision != 1 || disabled} | |||
// disabled={!qcAccept || disabled} | |||
{...register("acceptQty", { | |||
required: "acceptQty required!", | |||
})} | |||
@@ -438,11 +480,11 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
/> | |||
</Box> | |||
<FormControlLabel disabled={disabled} | |||
value="false" control={<Radio />} | |||
value="2" control={<Radio />} | |||
sx={{"& .Mui-checked": {color: "red"}}} | |||
label="不接受" /> | |||
<FormControlLabel disabled={disabled} | |||
value="false" control={<Radio />} | |||
value="3" control={<Radio />} | |||
sx={{"& .Mui-checked": {color: "blue"}}} | |||
label="上報品檢結果" /> | |||
</RadioGroup> | |||
@@ -450,7 +492,8 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
/> | |||
</FormControl> | |||
</Grid> | |||
{!qcAccept && ( | |||
{qcDecision == 3 && ( | |||
// {!qcAccept && ( | |||
<Grid item xs={12}> | |||
<EscalationComponent | |||
forSupervisor={false} | |||
@@ -11,8 +11,8 @@ import { | |||
Stack, | |||
Typography, | |||
} from "@mui/material"; | |||
import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; | |||
import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||
import { StockInLineRow } from "./PoInputGrid"; | |||
import { useTranslation } from "react-i18next"; | |||
import StockInForm from "./StockInForm"; | |||
@@ -22,8 +22,11 @@ import PutawayForm from "./PutawayForm"; | |||
import { dummyPutawayLine, dummyQCData } from "./dummyQcTemplate"; | |||
import { useGridApiRef } from "@mui/x-data-grid"; | |||
import {submitDialogWithWarning} from "../Swal/CustomAlerts"; | |||
import { arrayToDateString, arrayToInputDateString, dayjsToInputDateString } from "@/app/utils/formatUtil"; | |||
import { arrayToDateString, arrayToInputDateString, dayjsToInputDateString, INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
import dayjs from "dayjs"; | |||
import { watch } from "fs"; | |||
import { EscalationResult } from "@/app/api/escalation"; | |||
import { SessionWithTokens } from "@/config/authConfig"; | |||
const style = { | |||
position: "absolute", | |||
@@ -42,7 +45,7 @@ interface CommonProps extends Omit<ModalProps, "children"> { | |||
// setRows: Dispatch<SetStateAction<PurchaseOrderLine[]>>; | |||
setEntries?: Dispatch<SetStateAction<StockInLineRow[]>>; | |||
setStockInLine?: Dispatch<SetStateAction<StockInLine[]>>; | |||
itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] }; | |||
itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
setItemDetail: Dispatch< | |||
SetStateAction< | |||
| (StockInLine & { | |||
@@ -51,13 +54,15 @@ interface CommonProps extends Omit<ModalProps, "children"> { | |||
| undefined | |||
> | |||
>; | |||
session: SessionWithTokens | null; | |||
qc?: QcItemWithChecks[]; | |||
warehouse?: any[]; | |||
// type: "qc" | "stockIn" | "escalation" | "putaway" | "reject"; | |||
handleMailTemplateForStockInLine: (stockInLineId: number) => void; | |||
onClose: () => void; | |||
} | |||
interface Props extends CommonProps { | |||
itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] }; | |||
itemDetail: StockInLine & { qcResult?: PurchaseQcResult[] } & { escResult?: EscalationResult[] }; | |||
} | |||
const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
// type, | |||
@@ -68,6 +73,7 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
onClose, | |||
itemDetail, | |||
setItemDetail, | |||
session, | |||
qc, | |||
warehouse, | |||
handleMailTemplateForStockInLine, | |||
@@ -77,20 +83,32 @@ const PoQcStockInModalVer2: React.FC<Props> = ({ | |||
i18n: { language }, | |||
} = useTranslation("purchaseOrder"); | |||
const defaultNewValue = useMemo(() => { | |||
return ( | |||
{ | |||
...itemDetail, | |||
status: itemDetail.status ?? "pending", | |||
dnDate: arrayToInputDateString(itemDetail.dnDate)?? dayjsToInputDateString(dayjs()), | |||
putawayLine: dummyPutawayLine, | |||
qcResult: (itemDetail.qcResult && itemDetail.qcResult?.length > 0) ? itemDetail.qcResult : [],//[...dummyQCData], | |||
escResult: (itemDetail.escResult && itemDetail.escResult?.length > 0) ? itemDetail.escResult : [], | |||
receiptDate: itemDetail.receiptDate ?? dayjs().add(0, "month").format(INPUT_DATE_FORMAT), | |||
acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, | |||
warehouseId: itemDetail.defaultWarehouseId ?? 1 | |||
} | |||
) | |||
},[itemDetail]) | |||
const [qcItems, setQcItems] = useState(dummyQCData) | |||
const formProps = useForm<ModalFormInput>({ | |||
defaultValues: { | |||
...itemDetail, | |||
dnDate: dayjsToInputDateString(dayjs()), | |||
putawayLine: dummyPutawayLine, | |||
// receiptDate: itemDetail.receiptDate || dayjs().add(-1, "month").format(INPUT_DATE_FORMAT), | |||
// warehouseId: itemDetail.defaultWarehouseId || 0 | |||
...defaultNewValue, | |||
}, | |||
}); | |||
const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
(...args) => { | |||
onClose?.(...args); | |||
() => { | |||
onClose?.(); | |||
// reset(); | |||
}, | |||
[onClose], | |||
@@ -104,25 +122,31 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
} else return false; | |||
}; | |||
useEffect(() => { | |||
formProps.reset({ | |||
...itemDetail, | |||
dnDate: dayjsToInputDateString(dayjs()), | |||
putawayLine: dummyPutawayLine, | |||
}) | |||
setOpenPutaway(isPutaway); | |||
}, [open]) | |||
const [viewOnly, setViewOnly] = useState(false); | |||
useEffect(() => { | |||
if (itemDetail && itemDetail.status) { | |||
const isViewOnly = itemDetail.status.toLowerCase() == "completed" || itemDetail.status.toLowerCase() == "rejected" | |||
const isViewOnly = itemDetail.status.toLowerCase() == "completed" | |||
|| itemDetail.status.toLowerCase() == "rejected" | |||
|| (itemDetail.status.toLowerCase() == "escalated" && session?.id != itemDetail.handlerId) | |||
setViewOnly(isViewOnly) | |||
} | |||
console.log("ITEM", itemDetail); | |||
}, [itemDetail]); | |||
useEffect(() => { | |||
const qcRes = itemDetail?.qcResult; | |||
// if (!qcRes || qcRes?.length <= 0) { | |||
// itemDetail.qcResult = dummyQCData; | |||
// } | |||
formProps.reset({ | |||
...defaultNewValue | |||
}) | |||
setQcItems(dummyQCData); | |||
setOpenPutaway(isPutaway); | |||
}, [open, defaultNewValue]) | |||
const [openPutaway, setOpenPutaway] = useState(false); | |||
const onOpenPutaway = useCallback(() => { | |||
@@ -155,6 +179,13 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
[], | |||
); | |||
// QC submission handler | |||
const onSubmitErrorQc = useCallback<SubmitErrorHandler<ModalFormInput>>( | |||
async (data, event) => { | |||
console.log("Error", data); | |||
}, [] | |||
); | |||
// QC submission handler | |||
const onSubmitQc = useCallback<SubmitHandler<ModalFormInput>>( | |||
async (data, event) => { | |||
@@ -162,19 +193,16 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
// TODO: Move validation into QC page | |||
// Get QC data from the shared form context | |||
const qcAccept = data.qcAccept; | |||
const qcAccept = data.qcDecision == 1; | |||
// const qcAccept = data.qcAccept; | |||
const acceptQty = data.acceptQty as number; | |||
const qcResults = qcItems; | |||
const qcResults = qcItems; // qcItems; | |||
// const qcResults = data.qcResult as PurchaseQcResult[]; // qcItems; | |||
// const qcResults = viewOnly? data.qcResult as PurchaseQcResult[] : qcItems; | |||
// Validate QC data | |||
const validationErrors : string[] = []; | |||
// Check if all QC items have results | |||
const itemsWithoutResult = qcResults.filter(item => item.qcPassed === undefined); | |||
if (itemsWithoutResult.length > 0) { | |||
validationErrors.push(`${t("QC items without result")}`); | |||
// validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`); | |||
} | |||
// Check if failed items have failed quantity | |||
const failedItemsWithoutQty = qcResults.filter(item => | |||
@@ -202,13 +230,21 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
if (data.expiryDate === undefined || data.expiryDate == null) { | |||
validationErrors.push("請輸入到期日!"); | |||
} | |||
if (!qcResults.every((qc) => qc.qcPassed) && qcAccept) { | |||
if (!qcResults.every((qc) => qc.qcPassed) && qcAccept && itemDetail.status != "escalated") { //TODO: fix it please! | |||
validationErrors.push("有不合格檢查項目,無法收貨!"); | |||
// submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?", | |||
// confirmButtonText: t("confirm putaway"), html: ""}); | |||
// return; | |||
} | |||
// 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! | |||
// validationErrors.push(`${t("QC items without result")}`); | |||
// // validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(', ')}`); | |||
// } | |||
if (validationErrors.length > 0) { | |||
console.error("QC Validation failed:", validationErrors); | |||
alert(`未完成品檢: ${validationErrors}`); | |||
@@ -224,7 +260,8 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
qcAccept: qcAccept? qcAccept : false, | |||
acceptQty: acceptQty? acceptQty : 0, | |||
qcResult: qcResults.map(item => ({ | |||
qcResult: itemDetail.status != "escalated" ? qcResults.map(item => ({ | |||
// id: item.id, | |||
qcItemId: item.id, | |||
// code: item.code, | |||
// qcDescription: item.qcDescription, | |||
@@ -232,13 +269,27 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0, | |||
// failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | |||
remarks: item.remarks || '' | |||
})) | |||
})) : [], | |||
}; | |||
// const qcData = data; | |||
console.log("QC Data for submission:", qcData); | |||
await postStockInLine(qcData); | |||
if (data.qcDecision == 3) { // Escalate | |||
const escalationLog = { | |||
type : "qc", | |||
status : "pending", // TODO: update with supervisor decision | |||
reason : data.escalationLog?.reason, | |||
recordDate : dayjsToInputDateString(dayjs()), | |||
handlerId : Number(session?.id), | |||
} | |||
console.log("ESCALATION RESULT", escalationLog); | |||
await postStockInLine({...qcData, escalationLog}); | |||
} else { | |||
await postStockInLine(qcData); | |||
} | |||
if (qcData.qcAccept) { | |||
// submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?", | |||
@@ -257,12 +308,12 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
const submitData = { | |||
...itemDetail, ...args | |||
} as StockInLineEntry & ModalFormInput; | |||
console.log(submitData); | |||
console.log("Submitting", submitData); | |||
const res = await updateStockInLine(submitData); | |||
console.log("result ", res); | |||
console.log("Result ", res); | |||
return res; | |||
},[]) | |||
},[itemDetail]) | |||
// Email supplier handler | |||
const onSubmitEmailSupplier = useCallback<SubmitHandler<ModalFormInput>>( | |||
@@ -297,7 +348,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
// binLocation: data.binLocation, | |||
// putawayQuantity: data.putawayQuantity, | |||
// putawayNotes: data.putawayNotes, | |||
acceptQty: itemDetail.demandQty? itemDetail.demandQty : itemDetail.acceptedQty, | |||
acceptQty: itemDetail.demandQty?? itemDetail.acceptedQty, | |||
...data, | |||
dnDate : data.dnDate? arrayToInputDateString(data.dnDate) : dayjsToInputDateString(dayjs()), | |||
@@ -311,7 +362,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
// Handle putaway submission logic here | |||
const res = await postStockInLine(putawayData); | |||
console.log("result ", res); | |||
console.log("Result ", res); | |||
// Close modal after successful putaway | |||
closeHandler({}, "backdropClick"); | |||
@@ -438,7 +489,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
variant="contained" | |||
color="primary" | |||
sx={{ mt: 1 }} | |||
onClick={formProps.handleSubmit(onSubmitQc)} | |||
onClick={formProps.handleSubmit(onSubmitQc, onSubmitErrorQc)} | |||
> | |||
{t("confirm qc result")} | |||
</Button>)} | |||
@@ -76,18 +76,18 @@ const StockInFormVer2: React.FC<Props> = ({ | |||
clearErrors, | |||
} = useFormContext<StockInInput>(); | |||
// console.log(itemDetail); | |||
useEffect(() => { | |||
console.log("triggered"); | |||
// receiptDate default tdy | |||
setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT)); | |||
setValue("status", "received"); | |||
// console.log("triggered"); | |||
// // receiptDate default tdy | |||
// setValue("receiptDate", dayjs().add(0, "month").format(INPUT_DATE_FORMAT)); | |||
// setValue("status", "received"); | |||
}, [setValue]); | |||
useEffect(() => { | |||
console.log(errors); | |||
}, [errors]); | |||
const productionDate = watch("productionDate"); | |||
const expiryDate = watch("expiryDate"); | |||
const uom = watch("uom"); | |||
@@ -322,7 +322,7 @@ const StockInFormVer2: React.FC<Props> = ({ | |||
<TextField | |||
label={t("acceptedQty")} | |||
fullWidth | |||
disabled={disabled} | |||
disabled={true} | |||
{...register("acceptedQty", { | |||
required: "acceptedQty required!", | |||
})} | |||
@@ -215,7 +215,7 @@ const PoSearch: React.FC<Props> = ({ | |||
name: "escalated", | |||
label: t("Escalated"), | |||
renderCell: (params) => { | |||
console.log(params.escalated); | |||
// console.log(params.escalated); | |||
return params.escalated ? ( | |||
<NotificationIcon color="warning" /> | |||
) : undefined; | |||
@@ -42,5 +42,6 @@ | |||
"Received Qty": "收貨數量", | |||
"Escalation List": "上報列表", | |||
"Purchase UoM": "計量單位", | |||
"QC Completed Count": "品檢完成數量" | |||
"QC Completed Count": "品檢完成數量", | |||
"QC Fail-Total Count": "品檢不合格/總數" | |||
} |
@@ -72,7 +72,8 @@ | |||
"receiving": "收貨中", | |||
"received": "已檢收", | |||
"completed": "已上架", | |||
"rejected": "已拒絕及上報", | |||
"rejected": "已拒絕", | |||
"escalated": "已上報", | |||
"status": "狀態", | |||
"acceptedQty must not greater than": "接受數量不得大於", | |||
"minimal value is 1": "最小值為1", | |||
@@ -131,5 +132,6 @@ | |||
"print": "列印", | |||
"bind": "綁定", | |||
"Search": "搜尋", | |||
"Found": "已找到" | |||
"Found": "已找到", | |||
"escalation processing": "處理上報記錄" | |||
} |