| @@ -3,7 +3,7 @@ import { BASE_API_URL } from "@/config/api"; | |||||
| // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | ||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
| import { QcItemResult } from "../settings/qcItem"; | import { QcItemResult } from "../settings/qcItem"; | ||||
| import { RecordsRes } from "../utils"; | import { RecordsRes } from "../utils"; | ||||
| import { DoResult } from "."; | import { DoResult } from "."; | ||||
| @@ -87,6 +87,18 @@ export interface PrintDeliveryNoteResponse{ | |||||
| message?: string | message?: string | ||||
| } | } | ||||
| export interface PrintDNLabelsRequest{ | |||||
| deliveryOrderId: number, | |||||
| printerId: number, | |||||
| printQty: number, | |||||
| numOfCarton: number | |||||
| } | |||||
| export interface PrintDNLabelsRespone{ | |||||
| success: boolean; | |||||
| message?: string | |||||
| } | |||||
| export const assignPickOrderByStore = cache(async (data: AssignByStoreRequest) => { | export const assignPickOrderByStore = cache(async (data: AssignByStoreRequest) => { | ||||
| return await serverFetchJson<AssignByStoreResponse>(`${BASE_API_URL}/doPickOrder/assign-by-store`, | return await serverFetchJson<AssignByStoreResponse>(`${BASE_API_URL}/doPickOrder/assign-by-store`, | ||||
| { | { | ||||
| @@ -146,14 +158,37 @@ export const fetchDoSearch = cache(async (code: string, shopName: string, status | |||||
| }); | }); | ||||
| export async function printDN(request: PrintDeliveryNoteRequest){ | export async function printDN(request: PrintDeliveryNoteRequest){ | ||||
| const response = await serverFetchJson<PrintDeliveryNoteResponse>(`${BASE_API_URL}/do/print-DN`,{ | |||||
| const params = new URLSearchParams(); | |||||
| params.append('deliveryOrderId', request.deliveryOrderId.toString()); | |||||
| params.append('printerId', request.printerId.toString()); | |||||
| if (request.printQty !== null && request.printQty !== undefined) { | |||||
| params.append('printQty', request.printQty.toString()); | |||||
| } | |||||
| params.append('numOfCarton', request.numOfCarton.toString()); | |||||
| params.append('isDraft', request.isDraft.toString()); | |||||
| params.append('pickOrderId', request.pickOrderId.toString()); | |||||
| const response = await serverFetchWithNoContent(`${BASE_API_URL}/do/print-DN?${params.toString()}`,{ | |||||
| method: "GET", | method: "GET", | ||||
| body: JSON.stringify(request), | |||||
| headers: { | |||||
| 'Content-type': 'application/json', | |||||
| }, | |||||
| }); | }); | ||||
| return response; | |||||
| return { success: true, message: "Print job sent successfully (DN)" } as PrintDeliveryNoteResponse; | |||||
| } | |||||
| export async function printDNLabels(request: PrintDNLabelsRequest){ | |||||
| const params = new URLSearchParams(); | |||||
| params.append('deliveryOrderId', request.deliveryOrderId.toString()); | |||||
| params.append('printerId', request.printerId.toString()); | |||||
| if (request.printQty !== null && request.printQty !== undefined) { | |||||
| params.append('printQty', request.printQty.toString()); | |||||
| } | |||||
| params.append('numOfCarton', request.numOfCarton.toString()); | |||||
| const response = await serverFetchWithNoContent(`${BASE_API_URL}/do/print-DNLabels?${params.toString()}`,{ | |||||
| method: "GET" | |||||
| }); | |||||
| return { success: true, message: "Print job sent successfully (labels)"} as PrintDeliveryNoteResponse | |||||
| } | } | ||||
| @@ -16,6 +16,7 @@ export interface InventoryResult { | |||||
| availableQty: number; | availableQty: number; | ||||
| uomCode: string; | uomCode: string; | ||||
| uomUdfudesc: string; | uomUdfudesc: string; | ||||
| uomShortDesc: string; | |||||
| // germPerSmallestUnit: number; | // germPerSmallestUnit: number; | ||||
| qtyPerSmallestUnit: number; | qtyPerSmallestUnit: number; | ||||
| baseUom: string; | baseUom: string; | ||||
| @@ -30,6 +30,11 @@ import { useSession } from "next-auth/react"; | |||||
| import { SessionWithTokens } from "@/config/authConfig"; | import { SessionWithTokens } from "@/config/authConfig"; | ||||
| import PickExecutionDetail from "./GoodPickExecutiondetail"; | import PickExecutionDetail from "./GoodPickExecutiondetail"; | ||||
| import GoodPickExecutionRecord from "./GoodPickExecutionRecord"; | import GoodPickExecutionRecord from "./GoodPickExecutionRecord"; | ||||
| import Swal from "sweetalert2"; | |||||
| import { printDN, printDNLabels } from "@/app/api/do/actions"; | |||||
| import { FGPickOrderResponse } from "@/app/api/pickOrder/actions"; | |||||
| import FGPickOrderCard from "./FGPickOrderCard"; | |||||
| interface Props { | interface Props { | ||||
| pickOrders: PickOrderResult[]; | pickOrders: PickOrderResult[]; | ||||
| } | } | ||||
| @@ -57,6 +62,263 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState<boolean>( | ||||
| typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' | ||||
| ); | ); | ||||
| const [fgPickOrdersData, setFgPickOrdersData] = useState<FGPickOrderResponse[]>([]); | |||||
| const handleDraft = useCallback(async () =>{ | |||||
| try{ | |||||
| if (fgPickOrdersData.length === 0) { | |||||
| console.error("No FG Pick order data available"); | |||||
| return; | |||||
| } | |||||
| const currentFgOrder = fgPickOrdersData[0]; | |||||
| const printRequest = { | |||||
| printerId: 2, | |||||
| printQty: 1, | |||||
| isDraft: true, | |||||
| numOfCarton: 0, | |||||
| deliveryOrderId: currentFgOrder.deliveryOrderId, | |||||
| pickOrderId: currentFgOrder.pickOrderId | |||||
| }; | |||||
| console.log("Printing draft with request: ", printRequest); | |||||
| const response = await printDN(printRequest); | |||||
| console.log("Print Draft response: ", response); | |||||
| if(response.success){ | |||||
| Swal.fire({ | |||||
| position: "bottom-end", | |||||
| icon: "info", | |||||
| text: t("Printed Successfully."), | |||||
| showConfirmButton: false, | |||||
| timer: 1500 | |||||
| }); | |||||
| } else { | |||||
| console.error("Print failed: ", response.message); | |||||
| } | |||||
| } catch(error){ | |||||
| console.error("error: ", error) | |||||
| } | |||||
| },[t, fgPickOrdersData]); | |||||
| const handleDN = useCallback(async () =>{ | |||||
| const askNumofCarton = await Swal.fire({ | |||||
| title: t("Enter the number of cartons: "), | |||||
| input: "number", | |||||
| inputPlaceholder: t("Number of cartons"), | |||||
| inputAttributes:{ | |||||
| min: "1", | |||||
| step: "1" | |||||
| }, | |||||
| inputValidator: (value) => { | |||||
| if(!value){ | |||||
| return t("You need to enter a number") | |||||
| } | |||||
| if(parseInt(value) < 1){ | |||||
| return t("Number must be at least 1"); | |||||
| } | |||||
| return null | |||||
| }, | |||||
| showCancelButton: true, | |||||
| confirmButtonText: t("Confirm"), | |||||
| cancelButtonText: t("Cancel"), | |||||
| showLoaderOnConfirm: true, | |||||
| allowOutsideClick: () => !Swal.isLoading() | |||||
| }); | |||||
| if (askNumofCarton.isConfirmed) { | |||||
| const numOfCartons = askNumofCarton.value; | |||||
| try{ | |||||
| if (fgPickOrdersData.length === 0) { | |||||
| console.error("No FG Pick order data available"); | |||||
| return; | |||||
| } | |||||
| const currentFgOrder = fgPickOrdersData[0]; | |||||
| const printRequest = { | |||||
| printerId: 2, | |||||
| printQty: 1, | |||||
| isDraft: false, | |||||
| numOfCarton: numOfCartons, | |||||
| deliveryOrderId: currentFgOrder.deliveryOrderId, | |||||
| pickOrderId: currentFgOrder.pickOrderId | |||||
| }; | |||||
| console.log("Printing Delivery Note with request: ", printRequest); | |||||
| const response = await printDN(printRequest); | |||||
| console.log("Print Delivery Note response: ", response); | |||||
| if(response.success){ | |||||
| Swal.fire({ | |||||
| position: "bottom-end", | |||||
| icon: "info", | |||||
| text: t("Printed Successfully."), | |||||
| showConfirmButton: false, | |||||
| timer: 1500 | |||||
| }); | |||||
| } else { | |||||
| console.error("Print failed: ", response.message); | |||||
| } | |||||
| } catch(error){ | |||||
| console.error("error: ", error) | |||||
| } | |||||
| } | |||||
| },[t, fgPickOrdersData]); | |||||
| const handleDNandLabel = useCallback(async () =>{ | |||||
| const askNumofCarton = await Swal.fire({ | |||||
| title: t("Enter the number of cartons: "), | |||||
| input: "number", | |||||
| inputPlaceholder: t("Number of cartons"), | |||||
| inputAttributes:{ | |||||
| min: "1", | |||||
| step: "1" | |||||
| }, | |||||
| inputValidator: (value) => { | |||||
| if(!value){ | |||||
| return t("You need to enter a number") | |||||
| } | |||||
| if(parseInt(value) < 1){ | |||||
| return t("Number must be at least 1"); | |||||
| } | |||||
| return null | |||||
| }, | |||||
| showCancelButton: true, | |||||
| confirmButtonText: t("Confirm"), | |||||
| cancelButtonText: t("Cancel"), | |||||
| showLoaderOnConfirm: true, | |||||
| allowOutsideClick: () => !Swal.isLoading() | |||||
| }); | |||||
| if (askNumofCarton.isConfirmed) { | |||||
| const numOfCartons = askNumofCarton.value; | |||||
| try{ | |||||
| if (fgPickOrdersData.length === 0) { | |||||
| console.error("No FG Pick order data available"); | |||||
| return; | |||||
| } | |||||
| const currentFgOrder = fgPickOrdersData[0]; | |||||
| const printDNRequest = { | |||||
| printerId: 2, | |||||
| printQty: 1, | |||||
| isDraft: false, | |||||
| numOfCarton: numOfCartons, | |||||
| deliveryOrderId: currentFgOrder.deliveryOrderId, | |||||
| pickOrderId: currentFgOrder.pickOrderId | |||||
| }; | |||||
| const printDNLabelsRequest = { | |||||
| printerId: 1, | |||||
| printQty: 1, | |||||
| numOfCarton: numOfCartons, | |||||
| deliveryOrderId: currentFgOrder.deliveryOrderId | |||||
| }; | |||||
| console.log("Printing Labels with request: ", printDNLabelsRequest); | |||||
| console.log("Printing DN with request: ", printDNRequest); | |||||
| const LabelsResponse = await printDNLabels(printDNLabelsRequest); | |||||
| const DNResponse = await printDN(printDNRequest); | |||||
| console.log("Print Labels response: ", LabelsResponse); | |||||
| console.log("Print DN response: ", DNResponse); | |||||
| if(LabelsResponse.success && DNResponse.success){ | |||||
| Swal.fire({ | |||||
| position: "bottom-end", | |||||
| icon: "info", | |||||
| text: t("Printed Successfully."), | |||||
| showConfirmButton: false, | |||||
| timer: 1500 | |||||
| }); | |||||
| } else { | |||||
| if(!LabelsResponse.success){ | |||||
| console.error("Print failed: ", LabelsResponse.message); | |||||
| } | |||||
| else{ | |||||
| console.error("Print failed: ", DNResponse.message); | |||||
| } | |||||
| } | |||||
| } catch(error){ | |||||
| console.error("error: ", error) | |||||
| } | |||||
| } | |||||
| },[t, fgPickOrdersData]); | |||||
| const handleLabel = useCallback(async () =>{ | |||||
| const askNumofCarton = await Swal.fire({ | |||||
| title: t("Enter the number of cartons: "), | |||||
| input: "number", | |||||
| inputPlaceholder: t("Number of cartons"), | |||||
| inputAttributes:{ | |||||
| min: "1", | |||||
| step: "1" | |||||
| }, | |||||
| inputValidator: (value) => { | |||||
| if(!value){ | |||||
| return t("You need to enter a number") | |||||
| } | |||||
| if(parseInt(value) < 1){ | |||||
| return t("Number must be at least 1"); | |||||
| } | |||||
| return null | |||||
| }, | |||||
| showCancelButton: true, | |||||
| confirmButtonText: t("Confirm"), | |||||
| cancelButtonText: t("Cancel"), | |||||
| showLoaderOnConfirm: true, | |||||
| allowOutsideClick: () => !Swal.isLoading() | |||||
| }); | |||||
| if (askNumofCarton.isConfirmed) { | |||||
| const numOfCartons = askNumofCarton.value; | |||||
| try{ | |||||
| if (fgPickOrdersData.length === 0) { | |||||
| console.error("No FG Pick order data available"); | |||||
| return; | |||||
| } | |||||
| const currentFgOrder = fgPickOrdersData[0]; | |||||
| const printRequest = { | |||||
| printerId: 1, | |||||
| printQty: 1, | |||||
| numOfCarton: numOfCartons, | |||||
| deliveryOrderId: currentFgOrder.deliveryOrderId, | |||||
| }; | |||||
| console.log("Printing Labels with request: ", printRequest); | |||||
| const response = await printDNLabels(printRequest); | |||||
| console.log("Print Labels response: ", response); | |||||
| if(response.success){ | |||||
| Swal.fire({ | |||||
| position: "bottom-end", | |||||
| icon: "info", | |||||
| text: t("Printed Successfully."), | |||||
| showConfirmButton: false, | |||||
| timer: 1500 | |||||
| }); | |||||
| } else { | |||||
| console.error("Print failed: ", response.message); | |||||
| } | |||||
| } catch(error){ | |||||
| console.error("error: ", error) | |||||
| } | |||||
| } | |||||
| },[t, fgPickOrdersData]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| const onAssigned = () => { | const onAssigned = () => { | ||||
| localStorage.removeItem('hideCompletedUntilNext'); | localStorage.removeItem('hideCompletedUntilNext'); | ||||
| @@ -132,7 +394,6 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| }; | }; | ||||
| // ✅ Manual assignment handler - uses the action function | // ✅ Manual assignment handler - uses the action function | ||||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | ||||
| (_e, newValue) => { | (_e, newValue) => { | ||||
| setTabIndex(newValue); | setTabIndex(newValue); | ||||
| @@ -385,29 +646,33 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| */} | */} | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| disabled={!printButtonsEnabled} | |||||
| // disabled={!printButtonsEnabled} | |||||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | ||||
| onClick={handleDraft} | |||||
| > | > | ||||
| {t("Print Draft")} | {t("Print Draft")} | ||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| disabled={!printButtonsEnabled} | |||||
| // disabled={!printButtonsEnabled} | |||||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | ||||
| onClick={handleDNandLabel} | |||||
| > | > | ||||
| {t("Print Pick Order and DN Label")} | {t("Print Pick Order and DN Label")} | ||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| disabled={!printButtonsEnabled} | |||||
| // disabled={!printButtonsEnabled} | |||||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | ||||
| onClick={handleDN} | |||||
| > | > | ||||
| {t("Print Pick Order")} | {t("Print Pick Order")} | ||||
| </Button> | </Button> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| disabled={!printButtonsEnabled} | |||||
| // disabled={!printButtonsEnabled} | |||||
| title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | title={!printButtonsEnabled ? t("All lots must be completed before printing") : ""} | ||||
| onClick={handleLabel} | |||||
| > | > | ||||
| {t("Print DN Label")} | {t("Print DN Label")} | ||||
| </Button> | </Button> | ||||
| @@ -435,7 +700,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| <Box sx={{ | <Box sx={{ | ||||
| p: 2 | p: 2 | ||||
| }}> | }}> | ||||
| {tabIndex === 0 && <PickExecution filterArgs={filterArgs} />} | |||||
| {tabIndex === 0 && <PickExecution filterArgs={filterArgs} onFgPickOrdersChange={setFgPickOrdersData}/>} | |||||
| {tabIndex === 1 && <PickExecutionDetail filterArgs={filterArgs} />} | {tabIndex === 1 && <PickExecutionDetail filterArgs={filterArgs} />} | ||||
| {tabIndex === 2 && <GoodPickExecutionRecord filterArgs={filterArgs} />} | {tabIndex === 2 && <GoodPickExecutionRecord filterArgs={filterArgs} />} | ||||
| </Box> | </Box> | ||||
| @@ -51,6 +51,7 @@ import GoodPickExecutionForm from "./GoodPickExecutionForm"; | |||||
| import FGPickOrderCard from "./FGPickOrderCard"; | import FGPickOrderCard from "./FGPickOrderCard"; | ||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| onFgPickOrdersChange?: (fgPickOrders: FGPickOrderResponse[]) => void; | |||||
| } | } | ||||
| // ✅ QR Code Modal Component (from LotTable) | // ✅ QR Code Modal Component (from LotTable) | ||||
| @@ -307,7 +308,7 @@ const QrCodeModal: React.FC<{ | |||||
| ); | ); | ||||
| }; | }; | ||||
| const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const PickExecution: React.FC<Props> = ({ filterArgs, onFgPickOrdersChange }) => { | |||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { data: session } = useSession() as { data: SessionWithTokens | null }; | const { data: session } = useSession() as { data: SessionWithTokens | null }; | ||||
| @@ -359,6 +360,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| if (pickOrderIds.length === 0) { | if (pickOrderIds.length === 0) { | ||||
| setFgPickOrders([]); | setFgPickOrders([]); | ||||
| onFgPickOrdersChange?.([]); | |||||
| return; | return; | ||||
| } | } | ||||
| @@ -373,10 +375,12 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| const allFgPickOrders = fgPickOrdersResults.flat(); | const allFgPickOrders = fgPickOrdersResults.flat(); | ||||
| setFgPickOrders(allFgPickOrders); | setFgPickOrders(allFgPickOrders); | ||||
| onFgPickOrdersChange?.(allFgPickOrders); | |||||
| console.log("✅ Fetched FG pick orders:", allFgPickOrders); | console.log("✅ Fetched FG pick orders:", allFgPickOrders); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("❌ Error fetching FG pick orders:", error); | console.error("❌ Error fetching FG pick orders:", error); | ||||
| setFgPickOrders([]); | setFgPickOrders([]); | ||||
| onFgPickOrdersChange?.([]); | |||||
| } finally { | } finally { | ||||
| setFgPickOrdersLoading(false); | setFgPickOrdersLoading(false); | ||||
| } | } | ||||
| @@ -385,7 +389,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| if (combinedLotData.length > 0) { | if (combinedLotData.length > 0) { | ||||
| fetchFgPickOrdersData(); | fetchFgPickOrdersData(); | ||||
| } | } | ||||
| }, [combinedLotData, fetchFgPickOrdersData]); | |||||
| }, [combinedLotData, fetchFgPickOrdersData, onFgPickOrdersChange]); | |||||
| // ✅ Handle QR code button click | // ✅ Handle QR code button click | ||||
| const handleQrCodeClick = (pickOrderId: number) => { | const handleQrCodeClick = (pickOrderId: number) => { | ||||
| @@ -4,6 +4,7 @@ import { Box, Card, CardContent, Grid, Stack, TextField } from "@mui/material"; | |||||
| import { upperFirst } from "lodash"; | import { upperFirst } from "lodash"; | ||||
| import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { arrayToDateString } from "@/app/utils/formatUtil"; | |||||
| type Props = { | type Props = { | ||||
| @@ -21,7 +22,7 @@ const InfoCard: React.FC<Props> = ({ | |||||
| <CardContent component={Stack} spacing={4}> | <CardContent component={Stack} spacing={4}> | ||||
| <Box> | <Box> | ||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
| <Grid item xs={6}> | |||||
| {/*<Grid item xs={6}> | |||||
| <TextField | <TextField | ||||
| // { | // { | ||||
| // ...register("status") | // ...register("status") | ||||
| @@ -32,7 +33,7 @@ const InfoCard: React.FC<Props> = ({ | |||||
| value={`${t(upperFirst(watch("status")))}`} | value={`${t(upperFirst(watch("status")))}`} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}/> | |||||
| <Grid item xs={6}/>*/} | |||||
| <Grid item xs={6}> | <Grid item xs={6}> | ||||
| <TextField | <TextField | ||||
| { | { | ||||
| @@ -74,6 +75,24 @@ const InfoCard: React.FC<Props> = ({ | |||||
| disabled={true} | disabled={true} | ||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| value={arrayToDateString(watch("planStart"))} | |||||
| label={t("Target Production Date")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Production Priority")} | |||||
| fullWidth | |||||
| disabled={true} | |||||
| { | |||||
| ...register("id") | |||||
| } | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | </Grid> | ||||
| </Box> | </Box> | ||||
| </CardContent> | </CardContent> | ||||
| @@ -0,0 +1,103 @@ | |||||
| import { Button, Card, CardContent, Stack, Typography } from "@mui/material"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { JoDetailPickLine } from "@/app/api/jo"; | |||||
| import { fetchInventories } from "@/app/api/inventory/actions"; | |||||
| import { InventoryResult } from "@/app/api/inventory"; | |||||
| import { useEffect, useState, useMemo } from "react"; | |||||
| type Props = { | |||||
| onActionClick?: () => void; | |||||
| pickLines: JoDetailPickLine[]; | |||||
| } | |||||
| const JoRelease: React.FC<Props> = ({ | |||||
| onActionClick, | |||||
| pickLines | |||||
| }) => { | |||||
| const { t } = useTranslation("jo"); | |||||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||||
| useEffect(() => { | |||||
| const fetchInventoryData = async () => { | |||||
| try { | |||||
| const inventoryResponse = await fetchInventories({ | |||||
| code: "", | |||||
| name: "", | |||||
| type: "", | |||||
| pageNum: 0, | |||||
| pageSize: 1000 | |||||
| }); | |||||
| setInventoryData(inventoryResponse.records); | |||||
| } catch (error) { | |||||
| console.error("Error fetching inventory data:", error); | |||||
| } | |||||
| }; | |||||
| fetchInventoryData(); | |||||
| }, [pickLines]); | |||||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | |||||
| const inventory = inventoryData.find(inventory => | |||||
| inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name | |||||
| ); | |||||
| if (inventory) { | |||||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||||
| } | |||||
| return 0; | |||||
| }; | |||||
| const isStockSufficient = (pickLine: JoDetailPickLine) => { | |||||
| const stockAvailable = getStockAvailable(pickLine); | |||||
| return stockAvailable >= pickLine.reqQty; | |||||
| }; | |||||
| const stockCounts = useMemo(() => { | |||||
| const totalLines = pickLines.length; | |||||
| const sufficientLines = pickLines.filter(pickLine => isStockSufficient(pickLine)).length; | |||||
| const insufficientLines = totalLines - sufficientLines; | |||||
| return { | |||||
| total: totalLines, | |||||
| sufficient: sufficientLines, | |||||
| insufficient: insufficientLines | |||||
| }; | |||||
| }, [pickLines, inventoryData]); | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack | |||||
| direction="row" | |||||
| alignItems="center" | |||||
| justifyContent="space-between" | |||||
| spacing={2} | |||||
| > | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||||
| {t("Total lines: ")}<strong>{stockCounts.total}</strong> | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||||
| {t("Lines with sufficient stock: ")}<strong style={{ color: 'green' }}>{stockCounts.sufficient}</strong> | |||||
| </Typography> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> | |||||
| {t("Lines with insufficient stock: ")}<strong style={{ color: 'red' }}>{stockCounts.insufficient}</strong> | |||||
| </Typography> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="primary" | |||||
| onClick={onActionClick} | |||||
| > | |||||
| {t("Release")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default JoRelease; | |||||
| @@ -14,6 +14,7 @@ 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 JoRelease from "./JoRelease"; | |||||
| type Props = { | type Props = { | ||||
| id?: number; | id?: number; | ||||
| @@ -163,6 +164,7 @@ const JoSave: React.FC<Props> = ({ | |||||
| )} | )} | ||||
| <ActionButtons handleRelease={handleRelease} handleStart={handleStart}/> | <ActionButtons handleRelease={handleRelease} handleStart={handleStart}/> | ||||
| <InfoCard /> | <InfoCard /> | ||||
| <JoRelease pickLines={pickLines}/> | |||||
| <PickTable /> | <PickTable /> | ||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
| <Button | <Button | ||||
| @@ -1,7 +1,7 @@ | |||||
| import { JoDetail, JoDetailPickLine } 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, GridRenderCellParams, GridValidRowModel } from "@mui/x-data-grid"; | import { GridColDef, GridRenderCellParams, GridValidRowModel } from "@mui/x-data-grid"; | ||||
| import { isEmpty, upperFirst } from "lodash"; | |||||
| import { isEmpty, pick, upperFirst } from "lodash"; | |||||
| import { useCallback, 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"; | ||||
| @@ -10,6 +10,15 @@ import { Box, Grid, Icon, IconButton, Stack, Typography } from "@mui/material"; | |||||
| import PendingOutlinedIcon from '@mui/icons-material/PendingOutlined'; | import PendingOutlinedIcon from '@mui/icons-material/PendingOutlined'; | ||||
| import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; | import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutlineOutlined'; | ||||
| import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; | import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; | ||||
| import { fetchInventories } from "@/app/api/inventory/actions"; | |||||
| import { InventoryResult } from "@/app/api/inventory"; | |||||
| import { useEffect, useState } from "react"; | |||||
| import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; | |||||
| type JoDetailPickLineWithCalculations = JoDetailPickLine & { | |||||
| stockAvailable: number; | |||||
| isStockSufficient: boolean; | |||||
| }; | |||||
| type Props = { | type Props = { | ||||
| @@ -23,9 +32,74 @@ const PickTable: React.FC<Props> = ({ | |||||
| watch | watch | ||||
| } = useFormContext<JoDetail>() | } = useFormContext<JoDetail>() | ||||
| const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); | |||||
| const pickLines = watch("pickLines"); | |||||
| useEffect(() => { | |||||
| const fetchInventoryData = async () => { | |||||
| try { | |||||
| const inventoryResponse = await fetchInventories({ | |||||
| code: "", | |||||
| name: "", | |||||
| type: "", | |||||
| pageNum: 0, | |||||
| pageSize: 1000 | |||||
| }); | |||||
| setInventoryData(inventoryResponse.records); | |||||
| } catch (error) { | |||||
| console.error("Error fetching inventory data:", error); | |||||
| } | |||||
| }; | |||||
| fetchInventoryData(); | |||||
| }, [pickLines]); | |||||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | |||||
| const inventory = inventoryData.find(inventory => | |||||
| inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name | |||||
| ); | |||||
| if (inventory) { | |||||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||||
| } | |||||
| return 0; | |||||
| }; | |||||
| const getUomShortDesc = (pickLine: JoDetailPickLine) => { | |||||
| const inventory = inventoryData.find(inventory => | |||||
| inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name | |||||
| ); | |||||
| return inventory?.uomShortDesc; // || pickLine.uom; | |||||
| }; | |||||
| const isStockSufficient = (pickLine: JoDetailPickLine) => { | |||||
| const stockAvailable = getStockAvailable(pickLine); | |||||
| return stockAvailable >= pickLine.reqQty; | |||||
| }; | |||||
| const sufficientStockIcon = useMemo(() => { | |||||
| return <CheckCircleOutlineOutlinedIcon fontSize={"large"} color="success" /> | |||||
| }, []); | |||||
| const insufficientStockIcon = useMemo(() => { | |||||
| return <DoDisturbAltRoundedIcon fontSize={"large"} color="error" /> | |||||
| }, []); | |||||
| const rowsWithCalculatedFields = useMemo(() => { | |||||
| return pickLines.map((pickLine, index) => ({ | |||||
| ...pickLine, | |||||
| id: pickLine.id || index, | |||||
| sequence: index + 1, | |||||
| stockAvailable: getStockAvailable(pickLine), | |||||
| isStockSufficient: isStockSufficient(pickLine), | |||||
| })); | |||||
| }, [pickLines, inventoryData]); | |||||
| const notPickedStatusColumn = useMemo(() => { | const notPickedStatusColumn = useMemo(() => { | ||||
| return (<HelpOutlineOutlinedIcon fontSize={"large"} color={"error"} />) | return (<HelpOutlineOutlinedIcon fontSize={"large"} color={"error"} />) | ||||
| }, []) | }, []) | ||||
| const scanStatusColumn = useCallback((status: boolean) => { | const scanStatusColumn = useCallback((status: boolean) => { | ||||
| return status ? | return status ? | ||||
| <CheckCircleOutlineOutlinedIcon fontSize={"large"} sx={{ ml: "5px" }} color="success" /> | <CheckCircleOutlineOutlinedIcon fontSize={"large"} sx={{ ml: "5px" }} color="success" /> | ||||
| @@ -33,17 +107,31 @@ const PickTable: React.FC<Props> = ({ | |||||
| }, []) | }, []) | ||||
| const columns = useMemo<GridColDef[]>(() => [ | const columns = useMemo<GridColDef[]>(() => [ | ||||
| { | |||||
| field: "sequence", | |||||
| headerName: t("Sequence"), | |||||
| flex: 0.2, | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| type: "number", | |||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||||
| return params.value; | |||||
| }, | |||||
| }, | |||||
| { | { | ||||
| field: "code", | field: "code", | ||||
| headerName: t("Code"), | |||||
| headerName: t("Item Code"), | |||||
| flex: 0.6, | flex: 0.6, | ||||
| }, | }, | ||||
| { | { | ||||
| field: "name", | field: "name", | ||||
| headerName: t("Name"), | |||||
| headerName: t("Item Name"), | |||||
| flex: 1, | flex: 1, | ||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||||
| return `${params.value} (${params.row.uom})`; | |||||
| }, | |||||
| }, | }, | ||||
| { | |||||
| /*{ | |||||
| field: "scanStatus", | field: "scanStatus", | ||||
| headerName: t("Scan Status"), | headerName: t("Scan Status"), | ||||
| flex: 0.4, | flex: 0.4, | ||||
| @@ -56,7 +144,7 @@ const PickTable: React.FC<Props> = ({ | |||||
| const scanStatus = params.row.pickedLotNo.map((pln) => Boolean(pln.isScanned)) | const scanStatus = params.row.pickedLotNo.map((pln) => Boolean(pln.isScanned)) | ||||
| return isEmpty(scanStatus) ? notPickedStatusColumn : <Stack direction={"column"}>{scanStatus.map((status) => scanStatusColumn(status))}</Stack> | 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."), | ||||
| @@ -82,7 +170,7 @@ const PickTable: React.FC<Props> = ({ | |||||
| const qtys = params.row.pickedLotNo.map((pln) => pln.qty) | const qtys = params.row.pickedLotNo.map((pln) => pln.qty) | ||||
| return isEmpty(qtys) ? t("Pending for pick") : qtys.map((qty) => <>{qty}<br /></>) | return isEmpty(qtys) ? t("Pending for pick") : qtys.map((qty) => <>{qty}<br /></>) | ||||
| }, | }, | ||||
| }, | |||||
| },*/ | |||||
| { | { | ||||
| field: "reqQty", | field: "reqQty", | ||||
| headerName: t("Req. Qty"), | headerName: t("Req. Qty"), | ||||
| @@ -90,17 +178,35 @@ const PickTable: React.FC<Props> = ({ | |||||
| align: "right", | align: "right", | ||||
| headerAlign: "right", | headerAlign: "right", | ||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | ||||
| return decimalFormatter.format(params.value) | |||||
| const uomShortDesc = getUomShortDesc(params.row); | |||||
| return `${decimalFormatter.format(params.value)} ${uomShortDesc}`; | |||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| field: "uom", | |||||
| headerName: t("UoM"), | |||||
| flex: 1, | |||||
| align: "left", | |||||
| headerAlign: "left", | |||||
| field: "stockAvailable", | |||||
| headerName: t("Stock Available"), | |||||
| flex: 0.7, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| type: "number", | |||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLine>) => { | |||||
| const uomShortDesc = getUomShortDesc(params.row); | |||||
| return `${decimalFormatter.format(params.value)} ${uomShortDesc}`; | |||||
| }, | |||||
| }, | }, | ||||
| { | { | ||||
| field: "stockStatus", | |||||
| headerName: t("Stock Status"), | |||||
| flex: 0.5, | |||||
| align: "right", | |||||
| headerAlign: "right", | |||||
| type: "boolean", | |||||
| renderCell: (params: GridRenderCellParams<JoDetailPickLineWithCalculations>) => { | |||||
| return params.row.isStockSufficient ? sufficientStockIcon : insufficientStockIcon; | |||||
| }, | |||||
| } | |||||
| /*{ | |||||
| field: "status", | field: "status", | ||||
| headerName: t("Status"), | headerName: t("Status"), | ||||
| flex: 1, | flex: 1, | ||||
| @@ -114,8 +220,9 @@ const PickTable: React.FC<Props> = ({ | |||||
| </> | </> | ||||
| ) | ) | ||||
| }, | }, | ||||
| }, | |||||
| ], []) | |||||
| },*/ | |||||
| ], [t, inventoryData]) | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| @@ -132,7 +239,7 @@ const PickTable: React.FC<Props> = ({ | |||||
| }, | }, | ||||
| }} | }} | ||||
| disableColumnMenu | disableColumnMenu | ||||
| rows={watch("pickLines")} | |||||
| rows={rowsWithCalculatedFields} | |||||
| columns={columns} | columns={columns} | ||||
| getRowHeight={() => 'auto'} | getRowHeight={() => 'auto'} | ||||
| /> | /> | ||||
| @@ -288,11 +288,12 @@ | |||||
| "COMPLETED":"已完成", | "COMPLETED":"已完成", | ||||
| "FG orders":"成品提料單", | "FG orders":"成品提料單", | ||||
| "Back to List":"返回列表", | "Back to List":"返回列表", | ||||
| "No completed DO pick orders found":"沒有已完成送貨單提料單", | |||||
| "Print DN Label":"列印送貨單標貼", | |||||
| "Enter the number of cartons: ": "請輸入總箱數", | "Enter the number of cartons: ": "請輸入總箱數", | ||||
| "Number of cartons": "箱數" | |||||
| "Number of cartons": "箱數", | |||||
| "You need to enter a number": "箱數不能為空", | |||||
| "Number must be at least 1": "箱數最少為一", | |||||
| "Printed Successfully.": "已成功列印" | |||||