| @@ -0,0 +1,34 @@ | |||||
| import { Metadata } from "next"; | |||||
| import { getServerI18n } from "@/i18n"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { Suspense } from "react"; | |||||
| import ExcelFileImport from "@/components/ExcelFileImport"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Excel File Import", | |||||
| }; | |||||
| const ImportExcel: React.FC = async () => { | |||||
| const { t } = await getServerI18n("common"); | |||||
| return ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Excel File Import")} | |||||
| </Typography> | |||||
| </Stack> | |||||
| <Suspense> | |||||
| <ExcelFileImport /> | |||||
| </Suspense> | |||||
| </> | |||||
| ) | |||||
| }; | |||||
| export default ImportExcel; | |||||
| @@ -2,7 +2,7 @@ | |||||
| import { cache } from 'react'; | import { cache } from 'react'; | ||||
| import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | import { Pageable, serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
| import { Machine, Operator } from "."; | |||||
| import { JoStatus, Machine, Operator } from "."; | |||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | import { convertObjToURLSearchParams } from "@/app/utils/commonUtil"; | ||||
| @@ -35,7 +35,7 @@ export interface SearchJoResult { | |||||
| name: string; | name: string; | ||||
| reqQty: number; | reqQty: number; | ||||
| uom: string; | uom: string; | ||||
| status: string; | |||||
| status: JoStatus; | |||||
| } | } | ||||
| export interface ReleaseJoRequest { | export interface ReleaseJoRequest { | ||||
| @@ -44,7 +44,7 @@ export interface ReleaseJoRequest { | |||||
| export interface ReleaseJoResponse { | export interface ReleaseJoResponse { | ||||
| id: number; | id: number; | ||||
| entity: { status: string } | |||||
| entity: { status: JoStatus } | |||||
| } | } | ||||
| export interface IsOperatorExistResponse<T> { | export interface IsOperatorExistResponse<T> { | ||||
| @@ -4,6 +4,9 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| export type JoStatus = "planning" | "pending" | "processing" | "packaging" | "storing" | "completed" | |||||
| export type JoBomMaterialStatus = "pending" | "completed" | |||||
| export interface Operator { | export interface Operator { | ||||
| id: number; | id: number; | ||||
| name: string; | name: string; | ||||
| @@ -24,7 +27,7 @@ export interface JoDetail { | |||||
| reqQty: number; | reqQty: number; | ||||
| uom: string; | uom: string; | ||||
| pickLines: JoDetailPickLine[]; | pickLines: JoDetailPickLine[]; | ||||
| status: string; | |||||
| status: JoStatus; | |||||
| planStart: number[]; | planStart: number[]; | ||||
| planEnd: number[]; | planEnd: number[]; | ||||
| type: string; | type: string; | ||||
| @@ -34,10 +37,16 @@ export interface JoDetailPickLine { | |||||
| id: number; | id: number; | ||||
| code: string; | code: string; | ||||
| name: string; | name: string; | ||||
| lotNo: string; | |||||
| pickedLotNo?: JoDetailPickedLotNo[]; | |||||
| reqQty: number; | reqQty: number; | ||||
| uom: string; | uom: string; | ||||
| status: string; | |||||
| status: JoBomMaterialStatus; | |||||
| } | |||||
| export interface JoDetailPickedLotNo { | |||||
| lotNo: string; | |||||
| qty: number; | |||||
| isScanned: boolean; | |||||
| } | } | ||||
| export const fetchJoDetail = cache(async (id: number) => { | export const fetchJoDetail = cache(async (id: number) => { | ||||
| @@ -0,0 +1,16 @@ | |||||
| "use server"; | |||||
| import { serverFetchString } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| export const importStockTake = async (data: FormData) => { | |||||
| const importStockTake = await serverFetchString<string>( | |||||
| `${BASE_API_URL}/stockTake/import`, | |||||
| { | |||||
| method: "POST", | |||||
| body: data, | |||||
| }, | |||||
| ); | |||||
| console.log(importStockTake) | |||||
| return importStockTake; | |||||
| } | |||||
| @@ -91,6 +91,25 @@ export async function serverFetchJson<T>(...args: FetchParams) { | |||||
| } | } | ||||
| } | } | ||||
| export async function serverFetchString<T>(...args: FetchParams) { | |||||
| const response = await serverFetch(...args); | |||||
| if (response.ok) { | |||||
| return response.text() as T; | |||||
| } else { | |||||
| switch (response.status) { | |||||
| case 401: | |||||
| signOutUser(); | |||||
| default: | |||||
| console.error(await response.text()); | |||||
| throw new ServerFetchError( | |||||
| "Something went wrong fetching data in server.", | |||||
| response, | |||||
| ); | |||||
| } | |||||
| } | |||||
| } | |||||
| export async function serverFetchBlob<T extends BlobResponse>(...args: FetchParams) { | export async function serverFetchBlob<T extends BlobResponse>(...args: FetchParams) { | ||||
| const response = await serverFetch(...args); | const response = await serverFetch(...args); | ||||
| @@ -0,0 +1,88 @@ | |||||
| "use client"; | |||||
| import { FileUpload } from "@mui/icons-material"; | |||||
| import { Button, Grid, Stack } from "@mui/material"; | |||||
| import React, { ChangeEvent, useCallback, useMemo } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { errorDialogWithContent, submitDialog, successDialog, successDialogWithContent } from "../Swal/CustomAlerts"; | |||||
| import { importStockTake } from "@/app/api/stockTake/actions"; | |||||
| interface Props { | |||||
| } | |||||
| const ExcelFileImport: React.FC<Props> = async ({ }) => { | |||||
| const { t } = useTranslation("common"); | |||||
| const buttonName: string[] = useMemo(() => { | |||||
| return ["Import Stock Take"] | |||||
| }, []) | |||||
| const handleExcelFileImportClick = useCallback(async (event: ChangeEvent<HTMLInputElement>) => { | |||||
| try { | |||||
| if (event.target.files !== null) { | |||||
| const file = event.target.files[0] | |||||
| const formData = new FormData(); | |||||
| formData.append('multipartFileList', file); | |||||
| if (file.name.endsWith(".xlsx") || file.name.endsWith(".csv")) { | |||||
| let response: String = "" | |||||
| console.log(event.target.id) | |||||
| switch (event.target.id) { | |||||
| case "Import Stock Take": | |||||
| response = await importStockTake(formData) | |||||
| break; | |||||
| } | |||||
| if (response.includes("Import Excel success")) { | |||||
| successDialogWithContent(t("Import Success"), t(`${response}`), t) | |||||
| } else { | |||||
| errorDialogWithContent(t("Import Fail"), t(`${response}`), t) | |||||
| } | |||||
| } | |||||
| } | |||||
| } catch (err) { | |||||
| console.log(err) | |||||
| return false | |||||
| } | |||||
| return | |||||
| }, []) | |||||
| return ( | |||||
| <> | |||||
| <Grid container rowGap={1.5}> | |||||
| { | |||||
| buttonName.map((name) => { | |||||
| return ( | |||||
| <Grid container> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="info" | |||||
| startIcon={<FileUpload />} | |||||
| component="label" | |||||
| > | |||||
| <input | |||||
| id={name} | |||||
| type='file' | |||||
| accept='.xlsx' | |||||
| hidden | |||||
| onChange={(event) => { | |||||
| handleExcelFileImportClick(event) | |||||
| }} | |||||
| /> | |||||
| {t(name)} | |||||
| </Button> | |||||
| </Grid> | |||||
| ) | |||||
| }) | |||||
| } | |||||
| </Grid> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default ExcelFileImport; | |||||
| @@ -0,0 +1,10 @@ | |||||
| import React from "react"; | |||||
| import ExcelFileImport from "./ExcelFileImport"; | |||||
| const ExcelFileImportWrapper: React.FC = async () => { | |||||
| return <ExcelFileImport/>; | |||||
| }; | |||||
| export default ExcelFileImportWrapper; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from './ExcelFileImportWrapper' | |||||
| @@ -279,38 +279,50 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| overflow: 'auto' // Single scrollbar for the whole page | overflow: 'auto' // Single scrollbar for the whole page | ||||
| }}> | }}> | ||||
| {/* Header section */} | {/* Header section */} | ||||
| <Box sx={{ | |||||
| p: 2, | |||||
| borderBottom: '1px solid #e0e0e0' | |||||
| }}> | |||||
| <Stack rowGap={2}> | |||||
| <Grid container alignItems="center"> | |||||
| <Grid item xs={8}> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Finished Good Order")} | |||||
| </Typography> | |||||
| </Grid> | |||||
| <Grid item xs={4} display="flex" justifyContent="end" alignItems="center"> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => handleAssignByStore("2/F")} | |||||
| disabled={isAssigning} | |||||
| > | |||||
| {isAssigning ? t("Assigning pick order...") : t("Pick Execution 2/F")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => handleAssignByStore("4/F")} | |||||
| disabled={isAssigning} | |||||
| > | |||||
| {isAssigning ? t("Assigning pick order...") : t("Pick Execution 4/F")} | |||||
| </Button> | |||||
| </Stack> | |||||
| <Box sx={{ p: 2, borderBottom: '1px solid #e0e0e0' }}> | |||||
| <Stack rowGap={2}> | |||||
| <Grid container alignItems="center"> | |||||
| <Grid item xs={8}> | |||||
| <Box mb={2}> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Finished Good Order")} | |||||
| </Typography> | |||||
| </Box> | |||||
| </Grid> | </Grid> | ||||
| </Grid> | |||||
| {/* First 4 buttons aligned left */} | |||||
| <Grid item xs={6}> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button variant="contained">{t("Print Draft")}</Button> | |||||
| <Button variant="contained">{t("Print Pick Order and DN Label")}</Button> | |||||
| <Button variant="contained">{t("Print Pick Order")}</Button> | |||||
| <Button variant="contained">{t("Print DN Label")}</Button> | |||||
| </Stack> | </Stack> | ||||
| </Box> | |||||
| </Grid> | |||||
| {/* Last 2 buttons aligned right */} | |||||
| <Grid item xs={6} display="flex" justifyContent="flex-end"> | |||||
| <Stack direction="row" spacing={1}> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => handleAssignByStore("2/F")} | |||||
| disabled={isAssigning} | |||||
| > | |||||
| {isAssigning ? t("Assigning pick order...") : t("Pick Execution 2/F")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => handleAssignByStore("4/F")} | |||||
| disabled={isAssigning} | |||||
| > | |||||
| {isAssigning ? t("Assigning pick order...") : t("Pick Execution 4/F")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Stack> | |||||
| </Box> | |||||
| {/* Tabs section - ✅ Move the click handler here */} | {/* Tabs section - ✅ Move the click handler here */} | ||||
| <Box sx={{ | <Box sx={{ | ||||
| @@ -514,8 +514,18 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||||
| qty: 0 | qty: 0 | ||||
| }); | }); | ||||
| console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate); | console.log(`Update stock out line result for line ${matchingLot.pickOrderLineId}:`, stockOutLineUpdate); | ||||
| if (stockOutLineUpdate && stockOutLineUpdate.code === "SUCCESS") { | |||||
| // Treat multiple backend shapes as success (type-safe via any) | |||||
| const r: any = stockOutLineUpdate as any; | |||||
| const updateOk = | |||||
| r?.code === 'SUCCESS' || | |||||
| typeof r?.id === 'number' || | |||||
| r?.type === 'checked' || | |||||
| r?.status === 'checked' || | |||||
| typeof r?.entity?.id === 'number' || | |||||
| r?.entity?.status === 'checked'; | |||||
| if (updateOk) { | |||||
| successCount++; | successCount++; | ||||
| } else { | } else { | ||||
| errorCount++; | errorCount++; | ||||
| @@ -570,8 +580,11 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||||
| if (successCount > 0) { | if (successCount > 0) { | ||||
| console.log(`✅ QR Code processing completed: ${successCount} updated/created`); | console.log(`✅ QR Code processing completed: ${successCount} updated/created`); | ||||
| setQrScanSuccess(true); | setQrScanSuccess(true); | ||||
| setQrScanError(false); | |||||
| setQrScanInput(''); // Clear input after successful processing | setQrScanInput(''); // Clear input after successful processing | ||||
| setIsManualScanning(false); | |||||
| stopScan(); | |||||
| resetScan(); | |||||
| // ✅ Clear success state after a delay | // ✅ Clear success state after a delay | ||||
| //setTimeout(() => { | //setTimeout(() => { | ||||
| @@ -778,7 +791,9 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false); | |||||
| qty: selectedLotForQr.stockOutLineQty || 0 | qty: selectedLotForQr.stockOutLineQty || 0 | ||||
| }); | }); | ||||
| console.log("Stock out line updated successfully!"); | console.log("Stock out line updated successfully!"); | ||||
| setQrScanSuccess(true); | |||||
| setQrScanError(false); | |||||
| // Close modal | // Close modal | ||||
| setQrModalOpen(false); | setQrModalOpen(false); | ||||
| setSelectedLotForQr(null); | setSelectedLotForQr(null); | ||||
| @@ -1276,7 +1291,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| {qrScanError && ( | |||||
| {qrScanError && !qrScanSuccess && ( | |||||
| <Alert severity="error" sx={{ mb: 2 }}> | <Alert severity="error" sx={{ mb: 2 }}> | ||||
| {t("QR code does not match any item in current orders.")} | {t("QR code does not match any item in current orders.")} | ||||
| </Alert> | </Alert> | ||||
| @@ -1444,6 +1459,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||||
| </Table> | </Table> | ||||
| </TableContainer> | </TableContainer> | ||||
| {/* ✅ Status Messages Display - Move here, outside the table */} | {/* ✅ Status Messages Display - Move here, outside the table */} | ||||
| {/* | |||||
| {paginatedData.length > 0 && ( | {paginatedData.length > 0 && ( | ||||
| <Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}> | <Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}> | ||||
| {paginatedData.map((lot, index) => ( | {paginatedData.map((lot, index) => ( | ||||
| @@ -1455,6 +1471,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe | |||||
| ))} | ))} | ||||
| </Box> | </Box> | ||||
| )} | )} | ||||
| */} | |||||
| <TablePagination | <TablePagination | ||||
| component="div" | component="div" | ||||
| count={combinedLotData.length} | count={combinedLotData.length} | ||||
| @@ -94,9 +94,7 @@ const LotConfirmationModal: React.FC<LotConfirmationModalProps> = ({ | |||||
| <Alert severity="info"> | <Alert severity="info"> | ||||
| {t("If you proceed, the system will:")} | {t("If you proceed, the system will:")} | ||||
| <ul style={{ margin: '8px 0 0 16px' }}> | <ul style={{ margin: '8px 0 0 16px' }}> | ||||
| <li>{t("Update the stock out line to use the scanned lot")}</li> | |||||
| <li>{t("Put the original suggested lot on hold")}</li> | |||||
| <li>{t("Update inventory lot line for the new lot")}</li> | |||||
| <li>{t("Update your suggested lot to the this scanned lot")}</li> | |||||
| </ul> | </ul> | ||||
| </Alert> | </Alert> | ||||
| </Stack> | </Stack> | ||||
| @@ -0,0 +1,86 @@ | |||||
| import { JoDetail } from "@/app/api/jo"; | |||||
| import { Box, Button, Stack, Typography } from "@mui/material"; | |||||
| import { useMemo } from "react"; | |||||
| import { useFormContext } from "react-hook-form"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import StartIcon from "@mui/icons-material/Start"; | |||||
| import PlayCircleFilledWhiteIcon from '@mui/icons-material/PlayCircleFilledWhite'; | |||||
| type Props = { | |||||
| handleRelease: () => void; | |||||
| handleStart: () => void; | |||||
| }; | |||||
| interface ErrorEntry { | |||||
| qtyErr: boolean; | |||||
| scanErr: boolean; | |||||
| } | |||||
| const ActionButtons: React.FC<Props> = ({ | |||||
| handleRelease, | |||||
| handleStart | |||||
| }) => { | |||||
| const { t } = useTranslation("jo"); | |||||
| const { watch } = useFormContext<JoDetail>(); | |||||
| const status = useMemo(() => { | |||||
| return watch("status").toLowerCase() | |||||
| }, [watch("status")]) | |||||
| const pickLines = useMemo(() => { | |||||
| return watch("pickLines") | |||||
| }, [watch("pickLines")]) | |||||
| // Check Error Handling (e.g. start jo) | |||||
| const errors: ErrorEntry = useMemo(() => { | |||||
| let qtyErr = false; | |||||
| let scanErr = false; | |||||
| pickLines.forEach((line) => { | |||||
| if (!qtyErr) { | |||||
| const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0 | |||||
| qtyErr = pickedQty > 0 && pickedQty >= line.reqQty | |||||
| } | |||||
| if (!scanErr) { | |||||
| scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false | |||||
| } | |||||
| }) | |||||
| return { | |||||
| qtyErr: qtyErr, | |||||
| scanErr: scanErr | |||||
| } | |||||
| }, [pickLines]) | |||||
| return ( | |||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||||
| {status === "planning" && ( | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<StartIcon />} | |||||
| onClick={handleRelease} | |||||
| > | |||||
| {t("Release")} | |||||
| </Button> | |||||
| )} | |||||
| {status === "pending" && ( | |||||
| <Box sx={{ flexDirection: 'column'}}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<PlayCircleFilledWhiteIcon />} | |||||
| onClick={handleStart} | |||||
| disabled={errors.qtyErr || errors.scanErr} | |||||
| > | |||||
| {t("Start Job Order")} | |||||
| </Button> | |||||
| {errors.scanErr && (<Typography variant="h3" color="error">{t("Please scan the item qr code.")}</Typography>)} | |||||
| {errors.qtyErr && (<Typography variant="h3" color="error">{t("Please make sure the qty is enough.")}</Typography>)} | |||||
| </Box> | |||||
| )} | |||||
| </Stack> | |||||
| ) | |||||
| } | |||||
| export default ActionButtons; | |||||
| @@ -4,13 +4,16 @@ import { useRouter } from "next/navigation"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import useUploadContext from "../UploadProvider/useUploadContext"; | import useUploadContext from "../UploadProvider/useUploadContext"; | ||||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | ||||
| import { useCallback, useState } from "react"; | |||||
| import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react"; | |||||
| import { Button, Stack, Typography } from "@mui/material"; | import { Button, Stack, Typography } from "@mui/material"; | ||||
| import StartIcon from "@mui/icons-material/Start"; | import StartIcon from "@mui/icons-material/Start"; | ||||
| import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | import ArrowBackIcon from '@mui/icons-material/ArrowBack'; | ||||
| import { releaseJo } from "@/app/api/jo/actions"; | import { releaseJo } from "@/app/api/jo/actions"; | ||||
| import InfoCard from "./InfoCard"; | import InfoCard from "./InfoCard"; | ||||
| import PickTable from "./PickTable"; | import PickTable from "./PickTable"; | ||||
| import ActionButtons from "./ActionButtons"; | |||||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | |||||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | |||||
| type Props = { | type Props = { | ||||
| id?: number; | id?: number; | ||||
| @@ -24,12 +27,92 @@ const JoSave: React.FC<Props> = ({ | |||||
| const { t } = useTranslation("jo") | const { t } = useTranslation("jo") | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { setIsUploading } = useUploadContext(); | const { setIsUploading } = useUploadContext(); | ||||
| const scanner = useQrCodeScannerContext() | |||||
| const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
| const finalDefaultValues = useMemo(() => { | |||||
| const values = { | |||||
| ...defaultValues, | |||||
| pickLines: defaultValues?.pickLines?.map(line => ({ | |||||
| ...line, | |||||
| pickedLotNo: Array.isArray(line.pickedLotNo) | |||||
| ? line.pickedLotNo.map(lot => ({ | |||||
| ...lot, | |||||
| isScanned: false | |||||
| })) | |||||
| : line.pickedLotNo | |||||
| })) | |||||
| } | |||||
| return values; | |||||
| }, [defaultValues]) | |||||
| const formProps = useForm<JoDetail>({ | const formProps = useForm<JoDetail>({ | ||||
| defaultValues: defaultValues | |||||
| defaultValues: finalDefaultValues | |||||
| }) | }) | ||||
| const pickLines = useMemo(() => { | |||||
| return formProps.watch("pickLines") | |||||
| }, [formProps.watch("pickLines")]) | |||||
| // --------------------------------------------- Qr Code Scan --------------------------------------------- // | |||||
| const needScan = useMemo(() => { | |||||
| return pickLines.some((line) => line.status === "pending") | |||||
| }, [pickLines.some((line) => line.status === "pending")]) | |||||
| useLayoutEffect(() => { | |||||
| if (needScan && !scanner.isScanning) { | |||||
| scanner.startScan(); | |||||
| } else if (!needScan) { | |||||
| scanner.stopScan(); | |||||
| } | |||||
| }, [needScan]) | |||||
| const checkScannedId = useCallback(async (stockInLineId: number | undefined) => { | |||||
| try { | |||||
| setIsUploading(true) | |||||
| if (stockInLineId) { | |||||
| const response = await fetchStockInLineInfo(stockInLineId); | |||||
| // const pickLines = formProps.watch("pickLines") | |||||
| const pickedLotNoIndex = pickLines.findIndex((line) => line.pickedLotNo?.some((pln) => pln.lotNo === response?.lotNo)) | |||||
| if (pickedLotNoIndex >= 0) { | |||||
| const updatedPickLines = [...pickLines] | |||||
| const matchedLotNoIndex = updatedPickLines[pickedLotNoIndex].pickedLotNo?.findIndex((line) => line?.lotNo === response?.lotNo && Boolean(line?.isScanned) === false) | |||||
| if (matchedLotNoIndex !== null && matchedLotNoIndex !== undefined && matchedLotNoIndex >= 0) { | |||||
| const updatedPickedLotNo = [...(updatedPickLines[pickedLotNoIndex].pickedLotNo || [])] | |||||
| updatedPickedLotNo[matchedLotNoIndex] = { | |||||
| ...updatedPickedLotNo[matchedLotNoIndex], | |||||
| isScanned: true, | |||||
| } | |||||
| updatedPickLines[pickedLotNoIndex] = { | |||||
| ...updatedPickLines[pickedLotNoIndex], | |||||
| pickedLotNo: updatedPickedLotNo, | |||||
| }; | |||||
| formProps.setValue("pickLines", updatedPickLines, { | |||||
| shouldValidate: true, | |||||
| shouldDirty: true, | |||||
| }); | |||||
| } | |||||
| } | |||||
| } | |||||
| } finally { | |||||
| scanner.resetScan() | |||||
| setIsUploading(false) | |||||
| } | |||||
| }, []) | |||||
| useEffect(() => { | |||||
| const result = scanner.result | |||||
| console.log(scanner.result) | |||||
| if (result?.value) { | |||||
| if (!isNaN(Number(result.value))) { checkScannedId(Number(result?.value)); } | |||||
| } else if (result?.stockInLineId) { | |||||
| checkScannedId(result?.stockInLineId) | |||||
| } | |||||
| }, [scanner.result]) | |||||
| // --------------------------------------------- Button Actions --------------------------------------------- // | |||||
| const handleBack = useCallback(() => { | const handleBack = useCallback(() => { | ||||
| router.replace(`/jo`) | router.replace(`/jo`) | ||||
| }, []) | }, []) | ||||
| @@ -38,12 +121,9 @@ const JoSave: React.FC<Props> = ({ | |||||
| try { | try { | ||||
| setIsUploading(true) | setIsUploading(true) | ||||
| if (id) { | if (id) { | ||||
| console.log(id) | |||||
| const response = await releaseJo({ id: id }) | const response = await releaseJo({ id: id }) | ||||
| console.log(response.entity.status) | |||||
| if (response) { | if (response) { | ||||
| formProps.setValue("status", response.entity.status) | formProps.setValue("status", response.entity.status) | ||||
| console.log(formProps.watch("status")) | |||||
| } | } | ||||
| } | } | ||||
| @@ -56,6 +136,11 @@ const JoSave: React.FC<Props> = ({ | |||||
| } | } | ||||
| }, []) | }, []) | ||||
| const handleStart = useCallback(async () => { | |||||
| console.log("first") | |||||
| }, []) | |||||
| // --------------------------------------------- Form Submit --------------------------------------------- // | |||||
| const onSubmit = useCallback<SubmitHandler<JoDetail>>(async (data, event) => { | const onSubmit = useCallback<SubmitHandler<JoDetail>>(async (data, event) => { | ||||
| console.log(data) | console.log(data) | ||||
| }, [t]) | }, [t]) | ||||
| @@ -76,18 +161,7 @@ const JoSave: React.FC<Props> = ({ | |||||
| {serverError} | {serverError} | ||||
| </Typography> | </Typography> | ||||
| )} | )} | ||||
| { | |||||
| formProps.watch("status").toLowerCase() === "planning" && ( | |||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<StartIcon />} | |||||
| onClick={handleRelease} | |||||
| > | |||||
| {t("Release")} | |||||
| </Button> | |||||
| </Stack> | |||||
| )} | |||||
| <ActionButtons handleRelease={handleRelease} handleStart={handleStart}/> | |||||
| <InfoCard /> | <InfoCard /> | ||||
| <PickTable /> | <PickTable /> | ||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| @@ -17,7 +17,7 @@ const JoSaveWrapper: React.FC<Props> & SubComponents = async ({ | |||||
| id, | id, | ||||
| }) => { | }) => { | ||||
| const jo = id ? await fetchJoDetail(id) : undefined | const jo = id ? await fetchJoDetail(id) : undefined | ||||
| return <JoSave id={id} defaultValues={jo}/> | return <JoSave id={id} defaultValues={jo}/> | ||||
| } | } | ||||
| @@ -1,11 +1,15 @@ | |||||
| import { JoDetail } from "@/app/api/jo"; | |||||
| import { JoDetail, JoDetailPickLine } from "@/app/api/jo"; | |||||
| import { decimalFormatter } from "@/app/utils/formatUtil"; | import { decimalFormatter } from "@/app/utils/formatUtil"; | ||||
| import { GridColDef } from "@mui/x-data-grid"; | |||||
| import { GridColDef, GridRenderCellParams, GridValidRowModel } from "@mui/x-data-grid"; | |||||
| import { isEmpty, upperFirst } from "lodash"; | import { isEmpty, upperFirst } from "lodash"; | ||||
| import { useMemo } from "react"; | |||||
| import { useCallback, useMemo } from "react"; | |||||
| import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid/StyledDataGrid"; | ||||
| import { Box, Grid, Icon, IconButton, Stack, Typography } from "@mui/material"; | |||||
| import PendingOutlinedIcon from '@mui/icons-material/PendingOutlined'; | |||||
| import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; | |||||
| import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; | |||||
| type Props = { | type Props = { | ||||
| @@ -19,33 +23,74 @@ const PickTable: React.FC<Props> = ({ | |||||
| watch | watch | ||||
| } = useFormContext<JoDetail>() | } = useFormContext<JoDetail>() | ||||
| const notPickedStatusColumn = useMemo(() => { | |||||
| return (<HelpOutlineOutlinedIcon fontSize={"large"} color={"error"} />) | |||||
| }, []) | |||||
| const scanStatusColumn = useCallback((status: boolean) => { | |||||
| return status ? | |||||
| <CheckCircleOutlineOutlinedIcon fontSize={"large"} sx={{ ml: "5px" }} color="success" /> | |||||
| : <PendingOutlinedIcon fontSize={"large"} sx={{ ml: "5px" }} color="warning" /> | |||||
| }, []) | |||||
| const columns = useMemo<GridColDef[]>(() => [ | const columns = useMemo<GridColDef[]>(() => [ | ||||
| { | { | ||||
| field: "code", | field: "code", | ||||
| headerName: t("Code"), | headerName: t("Code"), | ||||
| flex: 1, | |||||
| flex: 0.6, | |||||
| }, | }, | ||||
| { | { | ||||
| field: "name", | field: "name", | ||||
| headerName: t("Name"), | headerName: t("Name"), | ||||
| flex: 1, | flex: 1, | ||||
| }, | }, | ||||
| { | |||||
| field: "scanStatus", | |||||
| headerName: t("Scan Status"), | |||||
| flex: 0.4, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||||
| if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | |||||
| return notPickedStatusColumn | |||||
| } | |||||
| const scanStatus = params.row.pickedLotNo.map((pln) => Boolean(pln.isScanned)) | |||||
| return isEmpty(scanStatus) ? notPickedStatusColumn : <Stack direction={"column"}>{scanStatus.map((status) => scanStatusColumn(status))}</Stack> | |||||
| }, | |||||
| }, | |||||
| { | { | ||||
| field: "lotNo", | field: "lotNo", | ||||
| headerName: t("Lot No."), | headerName: t("Lot No."), | ||||
| flex: 1, | flex: 1, | ||||
| renderCell: (row) => { | |||||
| return isEmpty(row.value) ? "N/A" : row.value | |||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||||
| if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | |||||
| return t("Pending for pick") | |||||
| } | |||||
| const lotNos = params.row.pickedLotNo.map((pln) => pln.lotNo) | |||||
| return isEmpty(lotNos) ? t("Pending for pick") : lotNos.map((lotNo) => (<>{lotNo}<br /></>)) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| field: "pickedQty", | |||||
| headerName: t("Picked Qty"), | |||||
| flex: 0.7, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||||
| if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | |||||
| return t("Pending for pick") | |||||
| } | |||||
| const qtys = params.row.pickedLotNo.map((pln) => pln.qty) | |||||
| return isEmpty(qtys) ? t("Pending for pick") : qtys.map((qty) => <>{qty}<br /></>) | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| field: "reqQty", | field: "reqQty", | ||||
| headerName: t("Req. Qty"), | headerName: t("Req. Qty"), | ||||
| flex: 1, | |||||
| flex: 0.7, | |||||
| align: "right", | align: "right", | ||||
| headerAlign: "right", | headerAlign: "right", | ||||
| renderCell: (row) => { | |||||
| return decimalFormatter.format(row.value) | |||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||||
| return decimalFormatter.format(params.value) | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -59,8 +104,15 @@ const PickTable: React.FC<Props> = ({ | |||||
| field: "status", | field: "status", | ||||
| headerName: t("Status"), | headerName: t("Status"), | ||||
| flex: 1, | flex: 1, | ||||
| renderCell: (row) => { | |||||
| return t(upperFirst(row.value)) | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||||
| return ( | |||||
| <> | |||||
| {params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))} | |||||
| {scanStatusColumn(Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))))} | |||||
| </> | |||||
| ) | |||||
| }, | }, | ||||
| }, | }, | ||||
| ], []) | ], []) | ||||
| @@ -82,6 +134,7 @@ const PickTable: React.FC<Props> = ({ | |||||
| disableColumnMenu | disableColumnMenu | ||||
| rows={watch("pickLines")} | rows={watch("pickLines")} | ||||
| columns={columns} | columns={columns} | ||||
| getRowHeight={() => 'auto'} | |||||
| /> | /> | ||||
| </> | </> | ||||
| ) | ) | ||||
| @@ -295,6 +295,11 @@ const NavigationContent: React.FC = () => { | |||||
| label: "Import Testing", | label: "Import Testing", | ||||
| path: "/settings/m18ImportTesting", | path: "/settings/m18ImportTesting", | ||||
| }, | }, | ||||
| { | |||||
| icon: <RequestQuote />, | |||||
| label: "Import Excel", | |||||
| path: "/settings/importExcel", | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| ]; | ]; | ||||
| @@ -41,7 +41,11 @@ | |||||
| "Delete": "刪除", | "Delete": "刪除", | ||||
| "Release": "放單", | "Release": "放單", | ||||
| "Back": "返回", | "Back": "返回", | ||||
| <<<<<<< HEAD | |||||
| "Batch Release": "批量放單", | "Batch Release": "批量放單", | ||||
| "Batch release completed successfully.": "已完成批量放單" | "Batch release completed successfully.": "已完成批量放單" | ||||
| ======= | |||||
| "Edit Delivery Order Detail": "編輯交貨單詳情" | |||||
| >>>>>>> 99400f68e2291628aae3d93b1a901a41ff80c570 | |||||
| } | } | ||||
| @@ -1,19 +1,21 @@ | |||||
| { | { | ||||
| "Job Order": "工單", | |||||
| "Create Job Order": "創建工單", | |||||
| "Edit Job Order Detail": "編輯工單", | |||||
| "Details": "細節", | |||||
| "Code": "編號", | |||||
| "Name": "名稱", | |||||
| "Req. Qty": "需求數量", | |||||
| "UoM": "單位", | |||||
| "Status": "來貨狀態", | |||||
| "Lot No.": "批號", | |||||
| "Bom": "物料清單", | |||||
| "Release": "發佈", | |||||
| "Pending": "待提料", | |||||
| "Planning": "計劃中" | |||||
| } | |||||
| "Job Order": "工單", | |||||
| "Create Job Order": "創建工單", | |||||
| "Edit Job Order Detail": "編輯工單", | |||||
| "Details": "細節", | |||||
| "Code": "編號", | |||||
| "Name": "名稱", | |||||
| "Picked Qty": "已提料數量", | |||||
| "Req. Qty": "需求數量", | |||||
| "UoM": "單位", | |||||
| "Status": "來貨狀態", | |||||
| "Lot No.": "批號", | |||||
| "Bom": "物料清單", | |||||
| "Release": "發佈", | |||||
| "Pending": "待掃碼", | |||||
| "Pending for pick": "待提料", | |||||
| "Planning": "計劃中", | |||||
| "Scanned": "已掃碼", | |||||
| "Scan Status": "掃碼狀態", | |||||
| "Start Job Order": "開始工單" | |||||
| } | |||||
| @@ -262,7 +262,20 @@ | |||||
| "Stop QR Scan":"停止QR掃描", | "Stop QR Scan":"停止QR掃描", | ||||
| "Scanning...":"掃描中...", | "Scanning...":"掃描中...", | ||||
| "Print DN/Label":"列印送貨單/標籤", | "Print DN/Label":"列印送貨單/標籤", | ||||
| "Store ID":"店鋪編號", | |||||
| "QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。" | |||||
| "Store ID":"儲存編號", | |||||
| "QR code does not match any item in current orders.":"QR 碼不符合當前訂單中的任何貨品。", | |||||
| "Lot Number Mismatch":"批次號碼不符", | |||||
| "The scanned item matches the expected item, but the lot number is different. Do you want to proceed with this different lot?":"掃描的貨品與預期的貨品相同,但批次號碼不同。您是否要繼續使用不同的批次?", | |||||
| "Expected Lot:":"預期批次:", | |||||
| "Scanned Lot:":"掃描批次:", | |||||
| "Confirm":"確認", | |||||
| "Update your suggested lot to the this scanned lot":"更新您的建議批次為此掃描的批次", | |||||
| "Print Draft":"列印草稿", | |||||
| "Print Pick Order and DN Label":"列印提料單和送貨單標貼", | |||||
| "Print Pick Order":"列印提料單", | |||||
| "Print DN Label":"列印送貨單標貼" | |||||
| } | } | ||||