| @@ -38,15 +38,17 @@ export interface SearchJoResult { | |||||
| status: JoStatus; | status: JoStatus; | ||||
| } | } | ||||
| export interface ReleaseJoRequest { | |||||
| // For Jo Button Actions | |||||
| export interface CommonActionJoRequest { | |||||
| id: number; | id: number; | ||||
| } | } | ||||
| export interface ReleaseJoResponse { | |||||
| export interface CommonActionJoResponse { | |||||
| id: number; | id: number; | ||||
| entity: { status: JoStatus } | entity: { status: JoStatus } | ||||
| } | } | ||||
| // For Jo Process | |||||
| export interface IsOperatorExistResponse<T> { | export interface IsOperatorExistResponse<T> { | ||||
| id: number | null; | id: number | null; | ||||
| name: string; | name: string; | ||||
| @@ -251,8 +253,17 @@ export const fetchJos = cache(async (data?: SearchJoResultRequest) => { | |||||
| return response | return response | ||||
| }) | }) | ||||
| export const releaseJo = cache(async (data: ReleaseJoRequest) => { | |||||
| return serverFetchJson<ReleaseJoResponse>(`${BASE_API_URL}/jo/release`, | |||||
| export const releaseJo = cache(async (data: CommonActionJoRequest) => { | |||||
| return serverFetchJson<CommonActionJoResponse>(`${BASE_API_URL}/jo/release`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }) | |||||
| }) | |||||
| export const startJo = cache(async (data: CommonActionJoRequest) => { | |||||
| return serverFetchJson<CommonActionJoResponse>(`${BASE_API_URL}/jo/start`, | |||||
| { | { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| @@ -261,7 +272,6 @@ export const releaseJo = cache(async (data: ReleaseJoRequest) => { | |||||
| }) | }) | ||||
| export const manualCreateJo = cache(async (data: SaveJo) => { | export const manualCreateJo = cache(async (data: SaveJo) => { | ||||
| console.log(data) | |||||
| return serverFetchJson<SaveJoResponse>(`${BASE_API_URL}/jo/manualCreate`, { | return serverFetchJson<SaveJoResponse>(`${BASE_API_URL}/jo/manualCreate`, { | ||||
| method: "POST", | method: "POST", | ||||
| body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
| @@ -14,6 +14,7 @@ type Props = { | |||||
| interface ErrorEntry { | interface ErrorEntry { | ||||
| qtyErr: boolean; | qtyErr: boolean; | ||||
| scanErr: boolean; | scanErr: boolean; | ||||
| pickErr: boolean; | |||||
| } | } | ||||
| const ActionButtons: React.FC<Props> = ({ | const ActionButtons: React.FC<Props> = ({ | ||||
| @@ -36,24 +37,31 @@ const ActionButtons: React.FC<Props> = ({ | |||||
| const errors: ErrorEntry = useMemo(() => { | const errors: ErrorEntry = useMemo(() => { | ||||
| let qtyErr = false; | let qtyErr = false; | ||||
| let scanErr = false; | let scanErr = false; | ||||
| let pickErr = false | |||||
| pickLines.forEach((line) => { | pickLines.forEach((line) => { | ||||
| if (!qtyErr) { | if (!qtyErr) { | ||||
| const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0 | const pickedQty = line.pickedLotNo?.reduce((acc, cur) => acc + cur.qty, 0) ?? 0 | ||||
| qtyErr = pickedQty > 0 && pickedQty >= line.reqQty | |||||
| qtyErr = pickedQty <= 0 || pickedQty < line.reqQty | |||||
| } | } | ||||
| if (!scanErr) { | if (!scanErr) { | ||||
| scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false | scanErr = line.pickedLotNo?.some((lotNo) => Boolean(lotNo.isScanned) === false) ?? false // default false | ||||
| } | } | ||||
| if (!pickErr) { | |||||
| pickErr = line.pickedLotNo === null | |||||
| } | |||||
| }) | }) | ||||
| return { | return { | ||||
| qtyErr: qtyErr, | qtyErr: qtyErr, | ||||
| scanErr: scanErr | |||||
| scanErr: scanErr, | |||||
| pickErr: pickErr | |||||
| } | } | ||||
| }, [pickLines]) | |||||
| }, [pickLines, status]) | |||||
| console.log(pickLines) | |||||
| return ( | return ( | ||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | <Stack direction="row" justifyContent="flex-start" gap={1}> | ||||
| {status === "planning" && ( | {status === "planning" && ( | ||||
| @@ -71,12 +79,13 @@ const ActionButtons: React.FC<Props> = ({ | |||||
| variant="outlined" | variant="outlined" | ||||
| startIcon={<PlayCircleFilledWhiteIcon />} | startIcon={<PlayCircleFilledWhiteIcon />} | ||||
| onClick={handleStart} | onClick={handleStart} | ||||
| disabled={errors.qtyErr || errors.scanErr} | |||||
| disabled={errors.qtyErr || errors.scanErr || errors.pickErr} | |||||
| > | > | ||||
| {t("Start Job Order")} | {t("Start Job Order")} | ||||
| </Button> | </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>)} | |||||
| {errors.pickErr && (<Typography variant="h3" color="error">{t("Please make sure all required items are picked")}</Typography>)} | |||||
| {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> | </Box> | ||||
| )} | )} | ||||
| </Stack> | </Stack> | ||||
| @@ -8,12 +8,13 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } fro | |||||
| 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, startJo } 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 ActionButtons from "./ActionButtons"; | ||||
| import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; | ||||
| import { fetchStockInLineInfo } from "@/app/api/po/actions"; | import { fetchStockInLineInfo } from "@/app/api/po/actions"; | ||||
| import { submitDialog } from "../Swal/CustomAlerts"; | |||||
| type Props = { | type Props = { | ||||
| id?: number; | id?: number; | ||||
| @@ -93,12 +94,18 @@ const JoSave: React.FC<Props> = ({ | |||||
| shouldValidate: true, | shouldValidate: true, | ||||
| shouldDirty: true, | shouldDirty: true, | ||||
| }); | }); | ||||
| // Ask user and confirm to start JO | |||||
| await submitDialog(() => handleStart(), t, { | |||||
| title: t("Do you want to start job order"), | |||||
| confirmButtonText: t("Start Job Order") | |||||
| }) | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| } finally { | } finally { | ||||
| scanner.resetScan() | |||||
| setIsUploading(false) | |||||
| scanner.resetScan() | |||||
| setIsUploading(false) | |||||
| } | } | ||||
| }, []) | }, []) | ||||
| @@ -126,7 +133,6 @@ const JoSave: React.FC<Props> = ({ | |||||
| formProps.setValue("status", response.entity.status) | formProps.setValue("status", response.entity.status) | ||||
| } | } | ||||
| } | } | ||||
| } catch (e) { | } catch (e) { | ||||
| // backend error | // backend error | ||||
| setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
| @@ -137,7 +143,25 @@ const JoSave: React.FC<Props> = ({ | |||||
| }, []) | }, []) | ||||
| const handleStart = useCallback(async () => { | const handleStart = useCallback(async () => { | ||||
| console.log("first") | |||||
| try { | |||||
| setIsUploading(true) | |||||
| if (id) { | |||||
| const response = await startJo({ id: id }) | |||||
| if (response) { | |||||
| formProps.setValue("status", response.entity.status) | |||||
| pickLines.map((line) => ({...line, status: "completed"})) | |||||
| handleBack() | |||||
| } | |||||
| } | |||||
| } catch (e) { | |||||
| // backend error | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| console.log(e); | |||||
| } finally { | |||||
| setIsUploading(false) | |||||
| } | |||||
| }, []) | }, []) | ||||
| // --------------------------------------------- Form Submit --------------------------------------------- // | // --------------------------------------------- Form Submit --------------------------------------------- // | ||||
| @@ -53,8 +53,8 @@ const PickTable: React.FC<Props> = ({ | |||||
| if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | if (params.row.pickedLotNo === null || params.row.pickedLotNo === undefined) { | ||||
| return notPickedStatusColumn | 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> | |||||
| const scanStatus = params.row.pickedLotNo.map((pln) => params.row.status === "completed" ? true : Boolean(pln.isScanned)) | |||||
| return isEmpty(scanStatus) ? notPickedStatusColumn : <Stack key={`${params.id}-scan`} direction={"column"}>{scanStatus.map((status) => scanStatusColumn(status))}</Stack> | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -107,10 +107,11 @@ const PickTable: React.FC<Props> = ({ | |||||
| align: "right", | align: "right", | ||||
| headerAlign: "right", | headerAlign: "right", | ||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | ||||
| const status = Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))) || params.row.status === "completed" | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))} | {params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned)) ? t("Scanned") : t(upperFirst(params.value))} | ||||
| {scanStatusColumn(Boolean(params.row.pickedLotNo?.every((lotNo) => Boolean(lotNo.isScanned))))} | |||||
| {scanStatusColumn(status)} | |||||
| </> | </> | ||||
| ) | ) | ||||
| }, | }, | ||||
| @@ -16,6 +16,11 @@ | |||||
| "Pending for pick": "待提料", | "Pending for pick": "待提料", | ||||
| "Planning": "計劃中", | "Planning": "計劃中", | ||||
| "Scanned": "已掃碼", | "Scanned": "已掃碼", | ||||
| "Processing": "已開始工序", | |||||
| "Storing": "入倉中", | |||||
| "completed": "已完成", | |||||
| "Completed": "已完成", | |||||
| "Cancel": "取消", | |||||
| "Scan Status": "掃碼狀態", | "Scan Status": "掃碼狀態", | ||||
| "Start Job Order": "開始工單", | "Start Job Order": "開始工單", | ||||
| "Job Order Pickexcution": "工單提料", | "Job Order Pickexcution": "工單提料", | ||||
| @@ -30,7 +35,6 @@ | |||||
| "Location": "位置", | "Location": "位置", | ||||
| "Scan Result": "掃碼結果", | "Scan Result": "掃碼結果", | ||||
| "Expiry Date": "有效期", | "Expiry Date": "有效期", | ||||
| "Pick Order Code": "提料單編號", | |||||
| "Target Date": "需求日期", | "Target Date": "需求日期", | ||||
| "Lot Required Pick Qty": "批號需求數量", | "Lot Required Pick Qty": "批號需求數量", | ||||
| "Job Order Match": "工單對料", | "Job Order Match": "工單對料", | ||||
| @@ -64,5 +68,9 @@ | |||||
| "Second Scan Status": "對料狀態", | "Second Scan Status": "對料狀態", | ||||
| "Job Order Pick Order Details": "工單提料單詳情", | "Job Order Pick Order Details": "工單提料單詳情", | ||||
| "Scanning...": "掃碼中", | "Scanning...": "掃碼中", | ||||
| "Unassigned Job Orders": "未分配工單" | |||||
| "Unassigned Job Orders": "未分配工單", | |||||
| "Please scan the item qr code": "請掃描物料二維碼", | |||||
| "Please make sure the qty is enough": "請確保物料數量是足夠", | |||||
| "Please make sure all required items are picked": "請確保所有物料已被提取", | |||||
| "Do you want to start job order": "是否開始工單" | |||||
| } | } | ||||