|
|
@@ -11,7 +11,7 @@ import SearchBox from "../SearchBox/SearchBox"; |
|
|
import { useRouter } from "next/navigation"; |
|
|
import { useRouter } from "next/navigation"; |
|
|
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; |
|
|
import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; |
|
|
import { StockInLineInput } from "@/app/api/stockIn"; |
|
|
import { StockInLineInput } from "@/app/api/stockIn"; |
|
|
import { JobOrder, JoStatus } from "@/app/api/jo"; |
|
|
|
|
|
|
|
|
import { JobOrder, JoDetailPickLine, JoStatus } from "@/app/api/jo"; |
|
|
import { Button, Stack } from "@mui/material"; |
|
|
import { Button, Stack } from "@mui/material"; |
|
|
import { BomCombo } from "@/app/api/bom"; |
|
|
import { BomCombo } from "@/app/api/bom"; |
|
|
import JoCreateFormModal from "./JoCreateFormModal"; |
|
|
import JoCreateFormModal from "./JoCreateFormModal"; |
|
|
@@ -23,6 +23,9 @@ import { createStockInLine } from "@/app/api/stockIn/actions"; |
|
|
import { msg } from "../Swal/CustomAlerts"; |
|
|
import { msg } from "../Swal/CustomAlerts"; |
|
|
import dayjs from "dayjs"; |
|
|
import dayjs from "dayjs"; |
|
|
|
|
|
|
|
|
|
|
|
import { fetchInventories } from "@/app/api/inventory/actions"; |
|
|
|
|
|
import { InventoryResult } from "@/app/api/inventory"; |
|
|
|
|
|
|
|
|
interface Props { |
|
|
interface Props { |
|
|
defaultInputs: SearchJoResultRequest, |
|
|
defaultInputs: SearchJoResultRequest, |
|
|
bomCombo: BomCombo[] |
|
|
bomCombo: BomCombo[] |
|
|
@@ -46,18 +49,104 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => { |
|
|
const [totalCount, setTotalCount] = useState(0) |
|
|
const [totalCount, setTotalCount] = useState(0) |
|
|
const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) |
|
|
const [isCreateJoModalOpen, setIsCreateJoModalOpen] = useState(false) |
|
|
|
|
|
|
|
|
|
|
|
const [inventoryData, setInventoryData] = useState<InventoryResult[]>([]); |
|
|
|
|
|
|
|
|
|
|
|
const [detailedJos, setDetailedJos] = useState<Map<number, JobOrder>>(new Map()); |
|
|
|
|
|
|
|
|
|
|
|
const fetchJoDetailClient = async (id: number): Promise<JobOrder> => { |
|
|
|
|
|
const response = await fetch(`/api/jo/detail?id=${id}`); |
|
|
|
|
|
if (!response.ok) { |
|
|
|
|
|
throw new Error('Failed to fetch JO detail'); |
|
|
|
|
|
} |
|
|
|
|
|
return response.json(); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
const fetchDetailedJos = async () => { |
|
|
|
|
|
const detailedMap = new Map<number, JobOrder>(); |
|
|
|
|
|
|
|
|
|
|
|
for (const jo of filteredJos) { |
|
|
|
|
|
try { |
|
|
|
|
|
const detailedJo = await fetchJoDetailClient(jo.id); // Use client function |
|
|
|
|
|
detailedMap.set(jo.id, detailedJo); |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
console.error(`Error fetching detail for JO ${jo.id}:`, error); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
setDetailedJos(detailedMap); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
if (filteredJos.length > 0) { |
|
|
|
|
|
fetchDetailedJos(); |
|
|
|
|
|
} |
|
|
|
|
|
}, [filteredJos]); |
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
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 getStockCounts = (jo: JobOrder) => { |
|
|
|
|
|
const detailedJo = detailedJos.get(jo.id); |
|
|
|
|
|
|
|
|
|
|
|
if (!detailedJo?.pickLines || detailedJo.pickLines.length === 0) { |
|
|
|
|
|
return { total: 0, sufficient: 0, insufficient: 0 }; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const totalLines = detailedJo.pickLines.length; |
|
|
|
|
|
const sufficientLines = detailedJo.pickLines.filter(pickLine => isStockSufficient(pickLine)).length; |
|
|
|
|
|
const insufficientLines = totalLines - sufficientLines; |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
|
|
total: totalLines, |
|
|
|
|
|
sufficient: sufficientLines, |
|
|
|
|
|
insufficient: insufficientLines |
|
|
|
|
|
}; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [ |
|
|
const searchCriteria: Criterion<SearchParamNames>[] = useMemo(() => [ |
|
|
{ label: t("Code"), paramName: "code", type: "text" }, |
|
|
{ label: t("Code"), paramName: "code", type: "text" }, |
|
|
{ label: t("Item Name"), paramName: "itemName", type: "text" }, |
|
|
|
|
|
{ label: t("Estimated Production Date From"), paramName: "planStartFrom", type: "date" }, |
|
|
|
|
|
{ label: t("Estimated Production Date To"), paramName: "planStartTo", type: "date" }, |
|
|
|
|
|
|
|
|
{ label: t("Item Name"), paramName: "itemName", type: "text" }, |
|
|
], [t]) |
|
|
], [t]) |
|
|
|
|
|
|
|
|
const columns = useMemo<Column<JobOrder>[]>( |
|
|
const columns = useMemo<Column<JobOrder>[]>( |
|
|
() => [ |
|
|
() => [ |
|
|
{ |
|
|
{ |
|
|
name: "code", |
|
|
name: "code", |
|
|
label: t("Code") |
|
|
|
|
|
|
|
|
label: t("Code"), |
|
|
|
|
|
flex: 2 |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
{ |
|
|
name: "item", |
|
|
name: "item", |
|
|
@@ -102,10 +191,26 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => { |
|
|
},{ |
|
|
},{ |
|
|
name: "planStart", |
|
|
name: "planStart", |
|
|
label: t("Estimated Production Date"), |
|
|
label: t("Estimated Production Date"), |
|
|
|
|
|
align: "left", |
|
|
|
|
|
headerAlign: "left", |
|
|
renderCell: (row) => { |
|
|
renderCell: (row) => { |
|
|
return row.planStart ? arrayToDateString(row.planStart, "output") : '-' |
|
|
return row.planStart ? arrayToDateString(row.planStart, "output") : '-' |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
|
|
|
{ |
|
|
|
|
|
name: "stockStatus" as keyof JobOrder, |
|
|
|
|
|
label: t("BOM Status"), |
|
|
|
|
|
align: "left", |
|
|
|
|
|
headerAlign: "left", |
|
|
|
|
|
renderCell: (row) => { |
|
|
|
|
|
const stockCounts = getStockCounts(row); |
|
|
|
|
|
return ( |
|
|
|
|
|
<span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}> |
|
|
|
|
|
{stockCounts.sufficient}/{stockCounts.total} |
|
|
|
|
|
</span> |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
{ |
|
|
{ |
|
|
// TODO put it inside Action Buttons |
|
|
// TODO put it inside Action Buttons |
|
|
name: "id", |
|
|
name: "id", |
|
|
@@ -127,7 +232,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo }) => { |
|
|
) |
|
|
) |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
], [] |
|
|
|
|
|
|
|
|
], [inventoryData] |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
const handleUpdate = useCallback(async (jo: JobOrder) => { |
|
|
const handleUpdate = useCallback(async (jo: JobOrder) => { |
|
|
|