diff --git a/src/app/api/do/actions.tsx b/src/app/api/do/actions.tsx index 34627af..8605885 100644 --- a/src/app/api/do/actions.tsx +++ b/src/app/api/do/actions.tsx @@ -3,7 +3,7 @@ import { BASE_API_URL } from "@/config/api"; // import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { revalidateTag } from "next/cache"; import { cache } from "react"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { QcItemResult } from "../settings/qcItem"; import { RecordsRes } from "../utils"; import { DoResult } from "."; @@ -87,6 +87,18 @@ export interface PrintDeliveryNoteResponse{ 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) => { return await serverFetchJson(`${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){ - const response = await serverFetchJson(`${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", - 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 } diff --git a/src/app/api/inventory/index.ts b/src/app/api/inventory/index.ts index 5e4c335..f29d0af 100644 --- a/src/app/api/inventory/index.ts +++ b/src/app/api/inventory/index.ts @@ -16,6 +16,7 @@ export interface InventoryResult { availableQty: number; uomCode: string; uomUdfudesc: string; + uomShortDesc: string; // germPerSmallestUnit: number; qtyPerSmallestUnit: number; baseUom: string; diff --git a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx index 3631e80..038a907 100644 --- a/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx +++ b/src/components/FinishedGoodSearch/FinishedGoodSearch.tsx @@ -30,6 +30,11 @@ import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import PickExecutionDetail from "./GoodPickExecutiondetail"; 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 { pickOrders: PickOrderResult[]; } @@ -57,6 +62,263 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { const [hideCompletedUntilNext, setHideCompletedUntilNext] = useState( typeof window !== 'undefined' && localStorage.getItem('hideCompletedUntilNext') === 'true' ); + + const [fgPickOrdersData, setFgPickOrdersData] = useState([]); + + 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(() => { const onAssigned = () => { localStorage.removeItem('hideCompletedUntilNext'); @@ -132,7 +394,6 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { }; // ✅ Manual assignment handler - uses the action function - const handleTabChange = useCallback>( (_e, newValue) => { setTabIndex(newValue); @@ -385,29 +646,33 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { */} @@ -435,7 +700,7 @@ const PickOrderSearch: React.FC = ({ pickOrders }) => { - {tabIndex === 0 && } + {tabIndex === 0 && } {tabIndex === 1 && } {tabIndex === 2 && } diff --git a/src/components/FinishedGoodSearch/GoodPickExecution.tsx b/src/components/FinishedGoodSearch/GoodPickExecution.tsx index 1bd52b8..dbad109 100644 --- a/src/components/FinishedGoodSearch/GoodPickExecution.tsx +++ b/src/components/FinishedGoodSearch/GoodPickExecution.tsx @@ -51,6 +51,7 @@ import GoodPickExecutionForm from "./GoodPickExecutionForm"; import FGPickOrderCard from "./FGPickOrderCard"; interface Props { filterArgs: Record; + onFgPickOrdersChange?: (fgPickOrders: FGPickOrderResponse[]) => void; } // ✅ QR Code Modal Component (from LotTable) @@ -307,7 +308,7 @@ const QrCodeModal: React.FC<{ ); }; -const PickExecution: React.FC = ({ filterArgs }) => { +const PickExecution: React.FC = ({ filterArgs, onFgPickOrdersChange }) => { const { t } = useTranslation("pickOrder"); const router = useRouter(); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -359,6 +360,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { if (pickOrderIds.length === 0) { setFgPickOrders([]); + onFgPickOrdersChange?.([]); return; } @@ -373,10 +375,12 @@ const PickExecution: React.FC = ({ filterArgs }) => { const allFgPickOrders = fgPickOrdersResults.flat(); setFgPickOrders(allFgPickOrders); + onFgPickOrdersChange?.(allFgPickOrders); console.log("✅ Fetched FG pick orders:", allFgPickOrders); } catch (error) { console.error("❌ Error fetching FG pick orders:", error); setFgPickOrders([]); + onFgPickOrdersChange?.([]); } finally { setFgPickOrdersLoading(false); } @@ -385,7 +389,7 @@ const PickExecution: React.FC = ({ filterArgs }) => { if (combinedLotData.length > 0) { fetchFgPickOrdersData(); } - }, [combinedLotData, fetchFgPickOrdersData]); + }, [combinedLotData, fetchFgPickOrdersData, onFgPickOrdersChange]); // ✅ Handle QR code button click const handleQrCodeClick = (pickOrderId: number) => { diff --git a/src/components/JoSave/InfoCard.tsx b/src/components/JoSave/InfoCard.tsx index 6f6ad06..2342b5b 100644 --- a/src/components/JoSave/InfoCard.tsx +++ b/src/components/JoSave/InfoCard.tsx @@ -4,6 +4,7 @@ import { Box, Card, CardContent, Grid, Stack, TextField } from "@mui/material"; import { upperFirst } from "lodash"; import { useFormContext } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { arrayToDateString } from "@/app/utils/formatUtil"; type Props = { @@ -21,7 +22,7 @@ const InfoCard: React.FC = ({ - + {/* = ({ value={`${t(upperFirst(watch("status")))}`} /> - + */} = ({ disabled={true} /> + + + + + + diff --git a/src/components/JoSave/JoRelease.tsx b/src/components/JoSave/JoRelease.tsx new file mode 100644 index 0000000..e613297 --- /dev/null +++ b/src/components/JoSave/JoRelease.tsx @@ -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 = ({ + onActionClick, + pickLines +}) => { + const { t } = useTranslation("jo"); + const [inventoryData, setInventoryData] = useState([]); + + 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 ( + + + + + {t("Total lines: ")}{stockCounts.total} + + + + {t("Lines with sufficient stock: ")}{stockCounts.sufficient} + + + + {t("Lines with insufficient stock: ")}{stockCounts.insufficient} + + + + + + + ); +}; + +export default JoRelease; diff --git a/src/components/JoSave/JoSave.tsx b/src/components/JoSave/JoSave.tsx index 000768c..0c3b24e 100644 --- a/src/components/JoSave/JoSave.tsx +++ b/src/components/JoSave/JoSave.tsx @@ -14,6 +14,7 @@ import PickTable from "./PickTable"; import ActionButtons from "./ActionButtons"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { fetchStockInLineInfo } from "@/app/api/po/actions"; +import JoRelease from "./JoRelease"; type Props = { id?: number; @@ -163,6 +164,7 @@ const JoSave: React.FC = ({ )} +