| @@ -37,7 +37,7 @@ export default async function MainLayout({ | |||
| return ( | |||
| <SessionProviderWrapper session={session}> | |||
| <UploadProvider> | |||
| <CameraProvider> | |||
| {/* <CameraProvider> */} | |||
| <AxiosProvider> | |||
| <QrCodeScannerProvider> | |||
| <> | |||
| @@ -62,7 +62,7 @@ export default async function MainLayout({ | |||
| </> | |||
| </QrCodeScannerProvider> | |||
| </AxiosProvider> | |||
| </CameraProvider> | |||
| {/* </CameraProvider> */} | |||
| </UploadProvider> | |||
| </SessionProviderWrapper> | |||
| ); | |||
| @@ -30,9 +30,6 @@ export interface StockInLineEntry { | |||
| acceptedQty: number; | |||
| status?: string; | |||
| expiryDate?: string; | |||
| productLotNo?: string; | |||
| receiptDate?: string; | |||
| dnDate?: string; | |||
| } | |||
| export interface PurchaseQcResult { | |||
| @@ -31,11 +31,14 @@ export interface StockInLineEntry { | |||
| acceptedQty: number; | |||
| status?: string; | |||
| expiryDate?: string; | |||
| productLotNo?: string; | |||
| receiptDate?: string; | |||
| dnDate?: string; | |||
| } | |||
| export interface PurchaseQcResult{ | |||
| qcItemId: number; | |||
| isPassed: boolean; | |||
| qcPassed: boolean; | |||
| failQty: number; | |||
| remarks?: string; | |||
| @@ -111,7 +114,7 @@ export const fetchStockInLineInfo = cache(async (stockInLineId: number) => { | |||
| export const createStockInLine = async (data: StockInLineEntry) => { | |||
| const stockInLine = await serverFetchJson< | |||
| PostStockInLineResponse<StockInLineEntry> | |||
| PostStockInLineResponse<StockInLine> | |||
| >(`${BASE_API_URL}/stockInLine/create`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| @@ -125,7 +128,7 @@ export const updateStockInLine = async ( | |||
| data: StockInLineEntry & ModalFormInput, | |||
| ) => { | |||
| const stockInLine = await serverFetchJson< | |||
| PostStockInLineResponse<StockInLineEntry & ModalFormInput> | |||
| PostStockInLineResponse<StockInLine & ModalFormInput> | |||
| >(`${BASE_API_URL}/stockInLine/update`, { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| @@ -54,7 +54,7 @@ export interface StockUomForPoLine { | |||
| export interface StockInLine { | |||
| id: number; | |||
| stockInId: number; | |||
| stockInId?: number; | |||
| purchaseOrderId?: number; | |||
| purchaseOrderLineId: number; | |||
| itemId: number; | |||
| @@ -63,23 +63,24 @@ export interface StockInLine { | |||
| itemType: string; | |||
| demandQty: number; | |||
| acceptedQty: number; | |||
| qty: number; | |||
| processed: number; | |||
| price: number; | |||
| priceUnit: string; | |||
| qty?: 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; | |||
| supplier?: string; | |||
| lotNo?: string; | |||
| poCode?: string; | |||
| uom?: Uom; | |||
| defaultWarehouseId: number; // id for now | |||
| dnNo: string; | |||
| dnDate: number[]; | |||
| stockQty: number; | |||
| dnNo?: string; | |||
| dnDate?: number[]; | |||
| stockQty?: number; | |||
| } | |||
| export const fetchPoList = cache(async (queryParams?: Record<string, any>) => { | |||
| @@ -20,7 +20,7 @@ export interface QcData { | |||
| code: string, | |||
| name: string, | |||
| qcDescription: string, | |||
| isPassed: boolean | undefined | |||
| qcPassed: boolean | undefined | |||
| failQty: number | undefined | |||
| remarks: string | undefined | |||
| } | |||
| @@ -49,7 +49,7 @@ import EscalationComponent from "./EscalationComponent"; | |||
| import QcDataGrid from "./QCDatagrid"; | |||
| import StockInFormVer2 from "./StockInFormVer2"; | |||
| import { dummyEscalationHistory, dummyQCData, QcData } from "./dummyQcTemplate"; | |||
| import { ModalFormInput } from "@/app/api/dashboard/actions"; | |||
| import { ModalFormInput } from "@/app/api/po/actions"; | |||
| import { escape } from "lodash"; | |||
| interface Props { | |||
| @@ -43,9 +43,11 @@ import { | |||
| import { | |||
| checkPolAndCompletePo, | |||
| fetchPoInClient, | |||
| fetchPoListClient, | |||
| fetchStockInLineInfo, | |||
| PurchaseQcResult, | |||
| startPo, | |||
| createStockInLine | |||
| } from "@/app/api/po/actions"; | |||
| import { | |||
| useCallback, | |||
| @@ -69,9 +71,7 @@ import DoneIcon from "@mui/icons-material/Done"; | |||
| import { getCustomWidth } from "@/app/utils/commonUtil"; | |||
| import PoInfoCard from "./PoInfoCard"; | |||
| import { decimalFormatter, integerFormatter } from "@/app/utils/formatUtil"; | |||
| import { fetchPoListClient } from "@/app/api/po/actions"; | |||
| import { List, ListItem, ListItemButton, ListItemText, Divider } from "@mui/material"; | |||
| import { createStockInLine } from "@/app/api/dashboard/actions"; | |||
| import { Controller, FormProvider, useForm } from "react-hook-form"; | |||
| import dayjs, { Dayjs } from "dayjs"; | |||
| import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| @@ -81,7 +81,6 @@ import LoadingComponent from "../General/LoadingComponent"; | |||
| //import { useRouter } from "next/navigation"; | |||
| type Props = { | |||
| po: PoResult; | |||
| qc: QcItemWithChecks[]; | |||
| @@ -397,6 +396,9 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| setTimeout(async () => { | |||
| // post stock in line | |||
| const oldId = row.id; | |||
| const acceptedQty = Number(polInputList[rowIndex].dnQty); | |||
| if (isNaN(acceptedQty) || acceptedQty <= 0) { alert("來貨數量必須大於0!"); return; } // Temp check, need update | |||
| const postData = { | |||
| dnNo: dnFormProps.watch("dnNo"), | |||
| dnDate: outputDateStringToInputDateString(dnFormProps.watch("dnDate")), | |||
| @@ -405,7 +407,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| itemName: row.itemName, | |||
| purchaseOrderId: row.purchaseOrderId, | |||
| purchaseOrderLineId: row.id, | |||
| acceptedQty: polInputList[rowIndex].dnQty || 0, | |||
| acceptedQty: acceptedQty, | |||
| productLotNo: polInputList[rowIndex].lotNo || '', | |||
| // acceptedQty: secondReceiveQty || 0, | |||
| // acceptedQty: row.acceptedQty, | |||
| @@ -535,7 +537,7 @@ const PoDetail: React.FC<Props> = ({ po, qc, warehouse }) => { | |||
| handleStart() | |||
| } | |||
| > | |||
| 提交 | |||
| {t("submit")} | |||
| </Button> | |||
| </TableCell> | |||
| </TableRow> | |||
| @@ -49,7 +49,7 @@ import EscalationComponent from "./EscalationComponent"; | |||
| import QcDataGrid from "./QCDatagrid"; | |||
| import StockInFormVer2 from "./StockInFormVer2"; | |||
| import { dummyEscalationHistory, dummyQCData } from "./dummyQcTemplate"; | |||
| import { ModalFormInput } from "@/app/api/dashboard/actions"; | |||
| import { ModalFormInput } from "@/app/api/po/actions"; | |||
| import { escape } from "lodash"; | |||
| import { PanoramaSharp } from "@mui/icons-material"; | |||
| @@ -197,7 +197,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| ), | |||
| }, | |||
| { | |||
| field: 'qcResult', | |||
| field: 'qcPassed', | |||
| headerName: t("qcResult"), | |||
| flex: 1.5, | |||
| renderCell: (params) => { | |||
| @@ -208,14 +208,15 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| <RadioGroup | |||
| row | |||
| aria-labelledby="demo-radio-buttons-group-label" | |||
| value={currentValue.isPassed === undefined ? (currentValue.failQty!==undefined?(currentValue.failQty==0?"true":"false"):"") : (currentValue.isPassed ? "true" : "false")} | |||
| value={currentValue.qcPassed === undefined ? "" : (currentValue.qcPassed ? "true" : "false")} | |||
| // value={currentValue.qcPassed === undefined ? (currentValue.failQty!==undefined?(currentValue.failQty==0?"true":"false"):"") : (currentValue.qcPassed ? "true" : "false")} | |||
| onChange={(e) => { | |||
| const value = e.target.value; | |||
| setQcItems((prev) => | |||
| prev.map((r): QcData => (r.id === params.id ? { ...r, isPassed: value === "true" } : r)) | |||
| prev.map((r): QcData => (r.id === params.id ? { ...r, qcPassed: value === "true" } : r)) | |||
| ); | |||
| }} | |||
| name={`isPassed-${params.id}`} | |||
| name={`qcPassed-${params.id}`} | |||
| > | |||
| <FormControlLabel | |||
| value="true" | |||
| @@ -223,7 +224,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| label="合格" | |||
| disabled={disabled} | |||
| sx={{ | |||
| color: currentValue.isPassed === true ? "green" : "inherit", | |||
| color: currentValue.qcPassed === true ? "green" : "inherit", | |||
| "& .Mui-checked": {color: "green"} | |||
| }} | |||
| /> | |||
| @@ -233,7 +234,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| label="不合格" | |||
| disabled={disabled} | |||
| sx={{ | |||
| color: currentValue.isPassed === false ? "red" : "inherit", | |||
| color: currentValue.qcPassed === false ? "red" : "inherit", | |||
| "& .Mui-checked": {color: "red"} | |||
| }} | |||
| /> | |||
| @@ -251,8 +252,8 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={!params.row.isPassed? (params.value ?? '') : '0'} | |||
| disabled={params.row.isPassed || disabled} | |||
| value={!params.row.qcPassed? (params.value ?? '') : '0'} | |||
| disabled={params.row.qcPassed || disabled} | |||
| onChange={(e) => { | |||
| const v = e.target.value; | |||
| const next = v === '' ? undefined : Number(v); | |||
| @@ -313,7 +314,7 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| const [isCollapsed, setIsCollapsed] = useState<boolean>(true); | |||
| const onFailedOpenCollapse = useCallback((qcItems: PurchaseQcResult[]) => { | |||
| const isFailed = qcItems.some((qc) => !qc.isPassed) | |||
| const isFailed = qcItems.some((qc) => !qc.qcPassed) | |||
| console.log(isFailed) | |||
| if (isFailed) { | |||
| setIsCollapsed(true) | |||
| @@ -439,7 +440,11 @@ const QcFormVer2: React.FC<Props> = ({ qc, itemDetail, disabled, qcItems, setQcI | |||
| <FormControlLabel disabled={disabled} | |||
| value="false" control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "red"}}} | |||
| label="不接受及上報" /> | |||
| label="不接受" /> | |||
| <FormControlLabel disabled={disabled} | |||
| value="false" control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "blue"}}} | |||
| label="上報品檢結果" /> | |||
| </RadioGroup> | |||
| )} | |||
| /> | |||
| @@ -1,6 +1,6 @@ | |||
| "use client"; | |||
| import { StockInLine } from "@/app/api/po"; | |||
| import { ModalFormInput, PurchaseQcResult, StockInLineEntry, updateStockInLine } from "@/app/api/po/actions"; | |||
| import { ModalFormInput, PurchaseQcResult, StockInLineEntry, updateStockInLine, PurchaseQCInput } from "@/app/api/po/actions"; | |||
| import { QcItemWithChecks, QcData } from "@/app/api/qc"; | |||
| import { | |||
| Box, | |||
| @@ -22,7 +22,6 @@ import PutawayForm from "./PutawayForm"; | |||
| import { dummyPutawayLine, dummyQCData } from "./dummyQcTemplate"; | |||
| import { useGridApiRef } from "@mui/x-data-grid"; | |||
| 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"; | |||
| @@ -114,10 +113,11 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| }, [open]) | |||
| const [isCompleted, setIsCompleted] = useState(false); | |||
| const [viewOnly, setViewOnly] = useState(false); | |||
| useEffect(() => { | |||
| setIsCompleted(itemDetail.status.toLowerCase() == "completed") | |||
| const isViewOnly = itemDetail.status.toLowerCase() == "completed" || itemDetail.status.toLowerCase() == "rejected" | |||
| setViewOnly(isViewOnly) | |||
| }, [itemDetail]); | |||
| const [openPutaway, setOpenPutaway] = useState(false); | |||
| @@ -162,12 +162,12 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const qcAccept = data.qcAccept; | |||
| const acceptQty = data.acceptQty as number; | |||
| const qcResults = qcItems; | |||
| // const qcResults = isCompleted? 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.isPassed === undefined); | |||
| 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(', ')}`); | |||
| @@ -175,7 +175,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| // Check if failed items have failed quantity | |||
| const failedItemsWithoutQty = qcResults.filter(item => | |||
| item.isPassed === false && (!item.failQty || item.failQty <= 0) | |||
| item.qcPassed === false && (!item.failQty || item.failQty <= 0) | |||
| ); | |||
| if (failedItemsWithoutQty.length > 0) { | |||
| validationErrors.push(`${t("Failed items must have failed quantity")}`); | |||
| @@ -194,10 +194,16 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| // Check if dates are input | |||
| if (data.productionDate === undefined || data.productionDate == null) { | |||
| validationErrors.push("Production Date cannot be null!"); | |||
| validationErrors.push("請輸入生產日期!"); | |||
| } | |||
| if (data.expiryDate === undefined || data.expiryDate == null) { | |||
| validationErrors.push("Expiry Date cannot be null!"); | |||
| validationErrors.push("請輸入到期日!"); | |||
| } | |||
| if (!qcResults.every((qc) => qc.qcPassed) && qcAccept) { | |||
| validationErrors.push("有不合格檢查項目,無法收貨!"); | |||
| // submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?", | |||
| // confirmButtonText: t("confirm putaway"), html: ""}); | |||
| // return; | |||
| } | |||
| if (validationErrors.length > 0) { | |||
| @@ -219,8 +225,8 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| qcItemId: item.id, | |||
| // code: item.code, | |||
| // qcDescription: item.qcDescription, | |||
| isPassed: item.isPassed? item.isPassed : false, | |||
| failQty: (item.failQty && !item.isPassed) ? item.failQty : 0, | |||
| qcPassed: item.qcPassed? item.qcPassed : false, | |||
| failQty: (item.failQty && !item.qcPassed) ? item.failQty : 0, | |||
| // failedQty: (typeof item.failedQty === "number" && !item.isPassed) ? item.failedQty : 0, | |||
| remarks: item.remarks || '' | |||
| })) | |||
| @@ -228,42 +234,21 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| // const qcData = data; | |||
| console.log("QC Data for submission:", qcData); | |||
| await postStockInLine(qcData); | |||
| if (!qcData.qcResult.every((qc) => qc.isPassed) && qcData.qcAccept) { | |||
| submitDialogWithWarning(() => postStockInLineWithQc(qcData), t, {title:"有不合格檢查項目,確認接受收貨?", | |||
| confirmButtonText: t("confirm putaway"), html: ""}); | |||
| return; | |||
| if (qcData.qcAccept) { | |||
| // submitDialogWithWarning(onOpenPutaway, t, {title:"Save success, confirm to proceed?", | |||
| // confirmButtonText: t("confirm putaway"), html: ""}); | |||
| onOpenPutaway(); | |||
| } else { | |||
| closeHandler({}, "backdropClick"); | |||
| } | |||
| await postStockInLineWithQc(qcData); | |||
| // return; | |||
| return ; | |||
| }, | |||
| [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 = { | |||
| @@ -340,7 +325,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const acceptQty = formProps.watch("acceptedQty") | |||
| const checkQcIsPassed = useCallback((qcItems: PurchaseQcResult[]) => { | |||
| const isPassed = qcItems.every((qc) => qc.isPassed); | |||
| const isPassed = qcItems.every((qc) => qc.qcPassed); | |||
| console.log(isPassed) | |||
| if (isPassed) { | |||
| formProps.setValue("passingQty", acceptQty) | |||
| @@ -378,7 +363,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| <PutawayForm | |||
| itemDetail={itemDetail} | |||
| warehouse={warehouse!} | |||
| disabled={isCompleted} | |||
| disabled={viewOnly} | |||
| /> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| @@ -416,7 +401,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <StockInFormVer2 itemDetail={itemDetail} disabled={isCompleted} /> | |||
| <StockInFormVer2 itemDetail={itemDetail} disabled={viewOnly} /> | |||
| </Grid> | |||
| </Grid> | |||
| {/* <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| @@ -438,13 +423,13 @@ const [qcItems, setQcItems] = useState(dummyQCData) | |||
| <QcFormVer2 | |||
| qc={qc!} | |||
| itemDetail={itemDetail} | |||
| disabled={isCompleted} | |||
| disabled={viewOnly} | |||
| qcItems={qcItems} | |||
| setQcItems={setQcItems} | |||
| /> | |||
| </Grid> | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| {!isCompleted && (<Button | |||
| {!viewOnly && (<Button | |||
| id="qcSubmit" | |||
| type="button" | |||
| variant="contained" | |||
| @@ -16,7 +16,7 @@ export const dummyQCData: QcData[] = [ | |||
| code: "包裝", | |||
| qcDescription: "有破爛、污糟、脹袋、積水、與實物不符等任何一種情況,則不合格", | |||
| name: "有破爛、污糟、脹袋、積水、與實物不符等任何一種情況,則不合格", | |||
| isPassed: undefined, | |||
| qcPassed: undefined, | |||
| failQty: undefined, | |||
| remarks: undefined, | |||
| }, | |||
| @@ -25,7 +25,7 @@ export const dummyQCData: QcData[] = [ | |||
| code: "肉質", | |||
| qcDescription: "肉質鬆散,則不合格", | |||
| name: "肉質鬆散,則不合格", | |||
| isPassed: undefined, | |||
| qcPassed: undefined, | |||
| failQty: undefined, | |||
| remarks: undefined, | |||
| }, | |||
| @@ -34,7 +34,7 @@ export const dummyQCData: QcData[] = [ | |||
| code: "顔色", | |||
| qcDescription: "不是食材應有的顔色、顔色不均匀、出現其他顔色、腌料/醬顔色不均匀,油脂部分變綠色、黃色,則不合格", | |||
| name: "不是食材應有的顔色、顔色不均匀、出現其他顔色、腌料/醬顔色不均匀,油脂部分變綠色、黃色,則不合格", | |||
| isPassed: undefined, | |||
| qcPassed: undefined, | |||
| failQty: undefined, | |||
| remarks: undefined, | |||
| }, | |||
| @@ -43,7 +43,7 @@ export const dummyQCData: QcData[] = [ | |||
| code: "狀態", | |||
| qcDescription: "有結晶、結霜、解凍跡象、發霉、散發異味等任何一種情況,則不合格", | |||
| name: "有結晶、結霜、解凍跡象、發霉、散發異味等任何一種情況,則不合格", | |||
| isPassed: undefined, | |||
| qcPassed: undefined, | |||
| failQty: undefined, | |||
| remarks: undefined, | |||
| }, | |||
| @@ -52,7 +52,7 @@ export const dummyQCData: QcData[] = [ | |||
| code: "異物", | |||
| qcDescription: "有不屬於本食材的雜質,則不合格", | |||
| name: "有不屬於本食材的雜質,則不合格", | |||
| isPassed: undefined, | |||
| qcPassed: undefined, | |||
| failQty: undefined, | |||
| remarks: undefined, | |||
| }, | |||