|
|
|
@@ -1,5 +1,5 @@ |
|
|
|
"use client"; |
|
|
|
import React, { useCallback, useEffect, useState } from "react"; |
|
|
|
import React, { useCallback, useEffect, useMemo, useState } from "react"; |
|
|
|
import { |
|
|
|
Box, |
|
|
|
Button, |
|
|
|
@@ -16,6 +16,8 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; |
|
|
|
import { useTranslation } from "react-i18next"; |
|
|
|
import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actions"; |
|
|
|
import JobPickExecution from "./newJobPickExecution"; |
|
|
|
import SearchBox, { Criterion } from "../SearchBox"; |
|
|
|
import dayjs from "dayjs"; |
|
|
|
|
|
|
|
interface Props { |
|
|
|
/** Reserved for tabs parity with Jodetail; not used in workbench list yet. */ |
|
|
|
@@ -25,20 +27,88 @@ interface Props { |
|
|
|
/** Jo workbench: same list + detail flow as Jodetail `JoPickOrderList`, detail uses `JoWorkbench/newJobPickExecution`. */ |
|
|
|
const JoPickOrderList: React.FC<Props> = () => { |
|
|
|
const { t } = useTranslation(["common", "jo"]); |
|
|
|
const today = dayjs().format("YYYY-MM-DD"); |
|
|
|
const [loading, setLoading] = useState(false); |
|
|
|
const [pickOrders, setPickOrders] = useState<AllJoPickOrderResponse[]>([]); |
|
|
|
const [selectedPickOrderId, setSelectedPickOrderId] = useState<number | undefined>(undefined); |
|
|
|
const [selectedJobOrderId, setSelectedJobOrderId] = useState<number | undefined>(undefined); |
|
|
|
type PickOrderFilter = "all" | "drink" | "Powder_Mixture" | "other"; |
|
|
|
const [filter, setFilter] = useState<PickOrderFilter>("all"); |
|
|
|
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({ planStart: today }); |
|
|
|
|
|
|
|
type FloorFilter = "ALL" | "2F" | "3F" | "4F" | "NO_LOT"; |
|
|
|
const [floorFilter, setFloorFilter] = useState<FloorFilter>("ALL"); |
|
|
|
const fetchPickOrders = useCallback(async () => { |
|
|
|
|
|
|
|
const searchCriteria: Criterion<any>[] = [ |
|
|
|
{ label: t("Job Order Code"), paramName: "jobOrderCode", type: "text" }, |
|
|
|
{ label: t("Pick Order"), paramName: "pickOrderCode", type: "text" }, |
|
|
|
{ label: t("Item Name"), paramName: "itemName", type: "text" }, |
|
|
|
{ |
|
|
|
label: t("Job Order Type"), |
|
|
|
paramName: "BOM Description", |
|
|
|
type: "select-labelled", |
|
|
|
options: [ |
|
|
|
{ label: t("All"), value: "All" }, |
|
|
|
{ label: t("FG"), value: "FG" }, |
|
|
|
{ label: t("WIP"), value: "WIP" }, |
|
|
|
], |
|
|
|
}, |
|
|
|
{ label: t("Plan Start"), paramName: "planStart", type: "date" }, |
|
|
|
{ |
|
|
|
label: t("BOM Type"), |
|
|
|
paramName: "bomType", |
|
|
|
type: "select-labelled", |
|
|
|
options: [ |
|
|
|
{ label: t("All"), value: "All" }, |
|
|
|
{ label: t("Drink"), value: "drink" }, |
|
|
|
{ label: t("Powder Mixture"), value: "Powder_Mixture" }, |
|
|
|
{ label: t("Other"), value: "other" }, |
|
|
|
], |
|
|
|
}, |
|
|
|
{ |
|
|
|
label: t("Floor"), |
|
|
|
paramName: "floor", |
|
|
|
type: "select-labelled", |
|
|
|
options: [ |
|
|
|
{ label: t("All"), value: "ALL" }, |
|
|
|
{ label: "2F", value: "2F" }, |
|
|
|
{ label: "3F", value: "3F" }, |
|
|
|
{ label: "4F", value: "4F" }, |
|
|
|
{ label: t("No Lot"), value: "NO_LOT" }, |
|
|
|
], |
|
|
|
}, |
|
|
|
]; |
|
|
|
|
|
|
|
const selectedFloor: FloorFilter = (() => { |
|
|
|
const floor = String(searchQuery.floor || "ALL"); |
|
|
|
if (floor === "2F" || floor === "3F" || floor === "4F" || floor === "NO_LOT") { |
|
|
|
return floor; |
|
|
|
} |
|
|
|
return "ALL"; |
|
|
|
})(); |
|
|
|
|
|
|
|
const fetchPickOrders = useCallback(async (query?: Record<string, any>) => { |
|
|
|
setLoading(true); |
|
|
|
try { |
|
|
|
const typeParam = filter === "all" ? undefined : filter; |
|
|
|
const floorParam = floorFilter === "ALL" ? undefined : floorFilter; |
|
|
|
const data = await fetchAllJoPickOrders(typeParam, floorParam); |
|
|
|
const currentQuery = query ?? { planStart: today }; |
|
|
|
const bomTypeValue = String(currentQuery.bomType || "All"); |
|
|
|
const floorValue = String(currentQuery.floor || "ALL"); |
|
|
|
const isAllOption = (value: string) => { |
|
|
|
const normalized = value.trim().toLowerCase(); |
|
|
|
return normalized === "" || normalized === "all"; |
|
|
|
}; |
|
|
|
const typeParam = isAllOption(bomTypeValue) ? undefined : bomTypeValue; |
|
|
|
const floorParam = isAllOption(floorValue) ? undefined : floorValue; |
|
|
|
const jobOrderCode = String(currentQuery.jobOrderCode || "").trim(); |
|
|
|
const pickOrderCode = String(currentQuery.pickOrderCode || "").trim(); |
|
|
|
const itemName = String(currentQuery.itemName || "").trim(); |
|
|
|
const bomDescription = String(currentQuery.bomDescription || "").trim(); |
|
|
|
const planStart = String(currentQuery.planStart || "").trim(); |
|
|
|
|
|
|
|
const data = await fetchAllJoPickOrders(typeParam, floorParam, { |
|
|
|
jobOrderCode: jobOrderCode || undefined, |
|
|
|
pickOrderCode: pickOrderCode || undefined, |
|
|
|
itemName: itemName || undefined, |
|
|
|
bomDescription: isAllOption(bomDescription) ? undefined : bomDescription, |
|
|
|
planStart: planStart || undefined, |
|
|
|
}); |
|
|
|
setPickOrders(Array.isArray(data) ? data : []); |
|
|
|
} catch (e) { |
|
|
|
console.error(e); |
|
|
|
@@ -46,16 +116,28 @@ const JoPickOrderList: React.FC<Props> = () => { |
|
|
|
} finally { |
|
|
|
setLoading(false); |
|
|
|
} |
|
|
|
}, [filter, floorFilter]); |
|
|
|
}, [today]); |
|
|
|
|
|
|
|
const handleSearch = useCallback((query: Record<string, any>) => { |
|
|
|
const nextQuery = { ...query }; |
|
|
|
setSearchQuery(nextQuery); |
|
|
|
void fetchPickOrders(nextQuery); |
|
|
|
}, [fetchPickOrders]); |
|
|
|
|
|
|
|
const handleSearchReset = useCallback(() => { |
|
|
|
const resetQuery = {}; |
|
|
|
setSearchQuery(resetQuery); |
|
|
|
void fetchPickOrders(resetQuery); |
|
|
|
}, [fetchPickOrders]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
fetchPickOrders(); |
|
|
|
}, [fetchPickOrders, filter, floorFilter]); |
|
|
|
void fetchPickOrders({ planStart: today }); |
|
|
|
}, [fetchPickOrders, today]); |
|
|
|
const handleBackToList = useCallback(() => { |
|
|
|
setSelectedPickOrderId(undefined); |
|
|
|
setSelectedJobOrderId(undefined); |
|
|
|
fetchPickOrders(); |
|
|
|
}, [fetchPickOrders]); |
|
|
|
void fetchPickOrders(searchQuery); |
|
|
|
}, [fetchPickOrders, searchQuery]); |
|
|
|
if (selectedPickOrderId !== undefined) { |
|
|
|
return ( |
|
|
|
<Box> |
|
|
|
@@ -78,84 +160,23 @@ const JoPickOrderList: React.FC<Props> = () => { |
|
|
|
|
|
|
|
return ( |
|
|
|
<Box> |
|
|
|
<Box sx={{ mb: 2 }}> |
|
|
|
<SearchBox |
|
|
|
criteria={searchCriteria} |
|
|
|
onSearch={handleSearch} |
|
|
|
onReset={handleSearchReset} |
|
|
|
/> |
|
|
|
</Box> |
|
|
|
{loading ? ( |
|
|
|
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}> |
|
|
|
<CircularProgress /> |
|
|
|
</Box> |
|
|
|
) : ( |
|
|
|
<Box> |
|
|
|
<Box sx={{ display: "flex", gap: 1, alignItems: "center", flexWrap: "wrap", mb: 2 }}> |
|
|
|
<Button |
|
|
|
variant={filter === "all" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFilter("all")} |
|
|
|
> |
|
|
|
{t("All")} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant={filter === "drink" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFilter("drink")} |
|
|
|
> |
|
|
|
{t("Drink")} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant={filter === "Powder_Mixture" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFilter("Powder_Mixture")} |
|
|
|
> |
|
|
|
{t("Powder Mixture")} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant={filter === "other" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFilter("other")} |
|
|
|
> |
|
|
|
{t("Other")} |
|
|
|
</Button> |
|
|
|
</Box> |
|
|
|
|
|
|
|
<Box sx={{ display: "flex", gap: 1, alignItems: "center", flexWrap: "wrap", mb: 2 }}> |
|
|
|
<Button |
|
|
|
variant={floorFilter === "ALL" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFloorFilter("ALL")} |
|
|
|
> |
|
|
|
{t("Select All")} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant={floorFilter === "2F" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFloorFilter("2F")} |
|
|
|
> |
|
|
|
2F |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant={floorFilter === "3F" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFloorFilter("3F")} |
|
|
|
> |
|
|
|
3F |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant={floorFilter === "4F" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFloorFilter("4F")} |
|
|
|
> |
|
|
|
4F |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant={floorFilter === "NO_LOT" ? "contained" : "outlined"} |
|
|
|
size="small" |
|
|
|
onClick={() => setFloorFilter("NO_LOT")} |
|
|
|
> |
|
|
|
{t("No Lot")} |
|
|
|
</Button> |
|
|
|
</Box> |
|
|
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> |
|
|
|
{t("Total pick orders")}: {pickOrders.length} |
|
|
|
</Typography> |
|
|
|
|
|
|
|
|
|
|
|
<Grid container spacing={2}> |
|
|
|
{pickOrders.map((pickOrder) => { |
|
|
|
const status = String(pickOrder.jobOrderStatus || ""); |
|
|
|
@@ -166,9 +187,9 @@ const JoPickOrderList: React.FC<Props> = () => { |
|
|
|
: statusLower === "pending" || statusLower === "processing" |
|
|
|
? "primary" |
|
|
|
: "default"; |
|
|
|
|
|
|
|
|
|
|
|
const finishedCount = pickOrder.finishedPickOLineCount ?? 0; |
|
|
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
<Grid key={pickOrder.id} item xs={12} sm={6} md={4}> |
|
|
|
<Card |
|
|
|
@@ -202,11 +223,12 @@ const JoPickOrderList: React.FC<Props> = () => { |
|
|
|
</Typography> |
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
{t("Item Name")}: {pickOrder.itemName} |
|
|
|
{pickOrder.bomDescription ? ` (${t(pickOrder.bomDescription)})` : ""} |
|
|
|
</Typography> |
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
{t("Required Qty")}: {pickOrder.reqQty} ({pickOrder.uomName}) |
|
|
|
</Typography> |
|
|
|
{floorFilter === "ALL" ? ( |
|
|
|
{selectedFloor === "ALL" ? ( |
|
|
|
<> |
|
|
|
{pickOrder.floorPickCounts?.map(({ floor, finishedCount, totalCount }) => ( |
|
|
|
<Typography |
|
|
|
@@ -231,7 +253,7 @@ const JoPickOrderList: React.FC<Props> = () => { |
|
|
|
</Typography> |
|
|
|
)} |
|
|
|
</> |
|
|
|
) : floorFilter === "NO_LOT" ? ( |
|
|
|
) : selectedFloor === "NO_LOT" ? ( |
|
|
|
!!pickOrder.noLotPickCount && ( |
|
|
|
<Typography |
|
|
|
key="NO_LOT" |
|
|
|
@@ -245,7 +267,7 @@ const JoPickOrderList: React.FC<Props> = () => { |
|
|
|
) |
|
|
|
) : ( |
|
|
|
pickOrder.floorPickCounts |
|
|
|
?.filter((c) => c.floor === floorFilter) |
|
|
|
?.filter((c) => c.floor === selectedFloor) |
|
|
|
.map(({ floor, finishedCount, totalCount }) => ( |
|
|
|
<Typography |
|
|
|
key={floor} |
|
|
|
@@ -271,7 +293,7 @@ const JoPickOrderList: React.FC<Props> = () => { |
|
|
|
</Box> |
|
|
|
)} |
|
|
|
</CardContent> |
|
|
|
|
|
|
|
|
|
|
|
<CardActions sx={{ pt: 0.5 }}> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
|