Bläddra i källkod

update jo search

MergeProblem1
CANCERYS\kw093 1 vecka sedan
förälder
incheckning
3c1b180148
6 ändrade filer med 147 tillägg och 93 borttagningar
  1. +2
    -2
      src/app/api/doworkbench/actions.ts
  2. +20
    -1
      src/app/api/jo/actions.ts
  3. +2
    -1
      src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx
  4. +110
    -88
      src/components/JoWorkbench/JoPickOrderList.tsx
  5. +7
    -0
      src/i18n/zh/common.json
  6. +6
    -1
      src/i18n/zh/jo.json

+ 2
- 2
src/app/api/doworkbench/actions.ts Visa fil

@@ -365,8 +365,8 @@ export async function printWorkbenchLotLabel(params: {
searchParams.set("inventoryLotLineId", String(params.inventoryLotLineId));
searchParams.set("printerId", String(params.printerId));
searchParams.set("printQty", String(params.printQty));
return serverFetchJson<number>(
`${BASE_API_URL}/inventoryLotLine/print-label?${searchParams.toString()}`,
return serverFetchJson<WorkbenchMessageResponse>(
`${BASE_API_URL}/inventoryLotLine/workbench/print-label?${searchParams.toString()}`,
{ method: "GET", cache: "no-store" },
);
}

+ 20
- 1
src/app/api/jo/actions.ts Visa fil

@@ -536,7 +536,9 @@ export interface AllJoPickOrderResponse {
jobOrderType: string | null;
itemId: number;
itemName: string;
bomDescription?: string | null;
lotNo: string | null;
planStart?: string | number[] | null;
reqQty: number;
uomId: number;
uomName: string;
@@ -733,10 +735,27 @@ export const fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench = cache(

// NOTE: Do NOT wrap in `cache()` because the list needs to reflect just-completed lines
// immediately when navigating back from JobPickExecution.
export const fetchAllJoPickOrders = async (type?: string | null, floor?: string | null) => {
export interface FetchAllJoPickOrdersFilters {
jobOrderCode?: string | null;
pickOrderCode?: string | null;
itemName?: string | null;
bomDescription?: string | null;
planStart?: string | null;
}

export const fetchAllJoPickOrders = async (
type?: string | null,
floor?: string | null,
filters?: FetchAllJoPickOrdersFilters,
) => {
const params = new URLSearchParams();
if (type) params.set("type", type);
if (floor) params.set("floor", floor);
if (filters?.jobOrderCode) params.set("jobOrderCode", filters.jobOrderCode);
if (filters?.pickOrderCode) params.set("pickOrderCode", filters.pickOrderCode);
if (filters?.itemName) params.set("itemName", filters.itemName);
if (filters?.bomDescription) params.set("bomDescription", filters.bomDescription);
if (filters?.planStart) params.set("planStart", filters.planStart);
const query = params.toString() ? `?${params.toString()}` : "";
return serverFetchJson<AllJoPickOrderResponse[]>(
`${BASE_API_URL}/jo/AllJoPickOrder${query}`,


+ 2
- 1
src/components/DoWorkbench/GoodPickExecutionWorkbenchRecord.tsx Visa fil

@@ -398,6 +398,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({

hierarchicalData.pickOrders.forEach((po: any) => {
po.pickOrderLines?.forEach((line: any) => {
const lineRequiredQty = Number(line.requiredQty ?? line.qty ?? 0);
const lineStockouts = line.stockouts || [];
const lots = line.lots || [];

@@ -413,7 +414,7 @@ const GoodPickExecutionWorkbenchRecord: React.FC<Props> = ({
lotNo: so.lotNo || lot.lotNo,
location: so.location || lot.location,
deliveryOrderCode: po.deliveryOrderCodes?.[0] || po.deliveryOrderCode,
requiredQty: lot.requiredQty,
requiredQty: lineRequiredQty,
actualPickQty: so.qty ?? lot.actualPickQty ?? 0,
processingStatus: toProc(so.status),
stockOutLineStatus: so.status,


+ 110
- 88
src/components/JoWorkbench/JoPickOrderList.tsx Visa fil

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


+ 7
- 0
src/i18n/zh/common.json Visa fil

@@ -11,6 +11,13 @@
"Issue BOM List": "問題 BOM 列表",
"File Name": "檔案名稱",
"Please Select BOM": "請選擇 BOM",
"Plan Start": "預計生產日期",
"Floor": "樓層",
"Job Order Type": "工單類型",

"FG": "成品",
"WIP": "半成品",
"BOM Type": "BOM 類型",
"No Lot": "沒有批號",
"Select All": "全選",
"Do Workbench": "新版成品出倉",


+ 6
- 1
src/i18n/zh/jo.json Visa fil

@@ -115,7 +115,8 @@
"Today": "今天",
"Yesterday": "昨天",
"Two Days Ago": "前天",
"Item Code": "成品/半成品編號",
"Item Code": "物料編號",
"Floor": "樓層",
"Paused": "已暫停",
"paused": "已暫停",
"Total pick orders": "總提料單數量",
@@ -388,6 +389,7 @@
"success": "成功",
"Total (Verified + Bad + Missing) must equal Required quantity": "驗證數量 + 不良數量 + 缺失數量必須等於需求數量",
"BOM Status": "材料預備狀況",
"Job Order Type": "工單類型",
"Estimated Production Date": "預計生產日期",
"Plan Start": "預計生產日期",
"Plan Start From": "預計生產日期",
@@ -600,5 +602,8 @@
"seq": "序號",
"Handled By": "處理者",
"Job Order Pick Execution": "工單提料",
"BOM Type": "BOM 類型",
"BOM Description": "BOM 說明",
"Floor": "樓層",
"Finish": "完成"
}

Laddar…
Avbryt
Spara