Browse Source

update PO flow

master
kelvin.yau 2 months ago
parent
commit
2701b467bc
9 changed files with 572 additions and 35 deletions
  1. +42
    -7
      src/app/api/do/actions.tsx
  2. +1
    -0
      src/app/api/inventory/index.ts
  3. +271
    -6
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  4. +6
    -2
      src/components/FinishedGoodSearch/GoodPickExecution.tsx
  5. +21
    -2
      src/components/JoSave/InfoCard.tsx
  6. +103
    -0
      src/components/JoSave/JoRelease.tsx
  7. +2
    -0
      src/components/JoSave/JoSave.tsx
  8. +122
    -15
      src/components/JoSave/PickTable.tsx
  9. +4
    -3
      src/i18n/zh/pickOrder.json

+ 42
- 7
src/app/api/do/actions.tsx View File

@@ -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
} }





+ 1
- 0
src/app/api/inventory/index.ts View File

@@ -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;


+ 271
- 6
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx View File

@@ -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>


+ 6
- 2
src/components/FinishedGoodSearch/GoodPickExecution.tsx View File

@@ -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) => {


+ 21
- 2
src/components/JoSave/InfoCard.tsx View File

@@ -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>


+ 103
- 0
src/components/JoSave/JoRelease.tsx View File

@@ -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;

+ 2
- 0
src/components/JoSave/JoSave.tsx View File

@@ -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


+ 122
- 15
src/components/JoSave/PickTable.tsx View File

@@ -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'}
/> />


+ 4
- 3
src/i18n/zh/pickOrder.json View File

@@ -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.": "已成功列印"






Loading…
Cancel
Save