Sfoglia il codice sorgente

update do search and jo bom name coe

production
CANCERYS\kw093 1 giorno fa
parent
commit
c8fe2cb962
11 ha cambiato i file con 421 aggiunte e 58 eliminazioni
  1. +4
    -1
      src/app/(main)/jo/workbench/page.tsx
  2. +27
    -1
      src/app/api/jo/actions.ts
  3. +2
    -2
      src/components/DoDetail/DoDetail.tsx
  4. +107
    -12
      src/components/DoSearch/DoSearch.tsx
  5. +106
    -12
      src/components/DoSearchWorkbench/DoSearchWorkbench.tsx
  6. +4
    -1
      src/components/JoWorkbench/JoPickOrderList.tsx
  7. +1
    -1
      src/components/JoWorkbench/JoWorkbenchTabs.tsx
  8. +165
    -27
      src/components/JoWorkbench/newJobPickExecution.tsx
  9. +1
    -1
      src/components/Jodetail/JodetailSearch.tsx
  10. +3
    -0
      src/i18n/zh/do.json
  11. +1
    -0
      src/i18n/zh/jo.json

+ 4
- 1
src/app/(main)/jo/workbench/page.tsx Vedi File

@@ -1,6 +1,7 @@
import GeneralLoading from "@/components/General/GeneralLoading";
import PageTitleBar from "@/components/PageTitleBar";
import JoPickOrderList from "@/components/JoWorkbench/JoPickOrderList";
import { fetchPrinterCombo } from "@/app/api/settings/printer";
import { I18nProvider, getServerI18n } from "@/i18n";
import { Metadata } from "next";
import React, { Suspense } from "react";
@@ -11,13 +12,15 @@ export const metadata: Metadata = {

const JoWorkbenchPage = async () => {
const { t } = await getServerI18n("jo");
const printerCombo = await fetchPrinterCombo();
//console.log("[JO Workbench Page] printerCombo count:", printerCombo?.length ?? 0);

return (
<>
<PageTitleBar title={t("Job Order Pickexcution", { defaultValue: "Job Order Pickexcution" })} className="mb-4" />
<I18nProvider namespaces={["jo", "common", "pickOrder", "purchaseOrder", "dashboard"]}>
<Suspense fallback={<GeneralLoading />}>
<JoPickOrderList />
<JoPickOrderList printerCombo={printerCombo ?? []} />
</Suspense>
</I18nProvider>
</>


+ 27
- 1
src/app/api/jo/actions.ts Vedi File

@@ -567,6 +567,12 @@ export interface JobOrderLotsHierarchicalResponse {
pickOrderLines: PickOrderLineWithLotsResponse[];
}

/** JO Workbench: same shape as [JobOrderLotsHierarchicalResponse] but `pickOrder.jobOrder` includes BOM code/name. */
export interface JobOrderLotsHierarchicalWorkbenchResponse {
pickOrder: PickOrderInfoWorkbenchResponse;
pickOrderLines: PickOrderLineWithLotsResponse[];
}

export interface PickOrderInfoResponse {
id: number | null;
code: string | null;
@@ -578,12 +584,32 @@ export interface PickOrderInfoResponse {
jobOrder: JobOrderBasicInfoResponse;
}

export interface PickOrderInfoWorkbenchResponse {
id: number | null;
code: string | null;
consoCode: string | null;
targetDate: string | null;
type: string | null;
status: string | null;
assignTo: number | null;
jobOrder: JobOrderBasicInfoWorkbenchResponse;
}

export interface JobOrderBasicInfoResponse {
id: number;
code: string;
name: string;
}

/** BOM header code/name from job order's BOM (workbench hierarchical API only). */
export interface JobOrderBasicInfoWorkbenchResponse {
id: number;
code: string;
name: string;
itemCode: string | null;
itemName: string | null;
}

export interface PickOrderLineWithLotsResponse {
id: number;
itemId: number | null;
@@ -724,7 +750,7 @@ export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrder

export const fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench = cache(
async (pickOrderId: number) => {
return serverFetchJson<JobOrderLotsHierarchicalResponse>(
return serverFetchJson<JobOrderLotsHierarchicalWorkbenchResponse>(
`${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order-workbench/${pickOrderId}`,
{
method: "GET",


+ 2
- 2
src/components/DoDetail/DoDetail.tsx Vedi File

@@ -40,8 +40,8 @@ const DoDetail: React.FC<Props> = ({
const { data: session } = useSession() as { data: SessionWithTokens | null }; // Use correct session type
const currentUserId = session?.id ? parseInt(session.id) : undefined; // Get user ID from session.id
console.log("🔍 DoSearch - session:", session);
console.log("🔍 DoSearch - currentUserId:", currentUserId);
//console.log("🔍 DoSearch - session:", session);
//console.log("🔍 DoSearch - currentUserId:", currentUserId);
const formProps = useForm<DoDetailType>({
defaultValues: defaultValues
})


+ 107
- 12
src/components/DoSearch/DoSearch.tsx Vedi File

@@ -43,7 +43,7 @@ type Props = {
searchQuery?: Record<string, any>;
onDeliveryOrderSearch?: () => void;
};
type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>;
type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "floor" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo" | "floorTo", string>;
type SearchParamNames = keyof SearchBoxInputs;

// put all this into a new component
@@ -55,6 +55,10 @@ type EntryError =
| undefined;
type DoRow = TableRow<Partial<DoResult>, EntryError>;

/** 已填車線但未選預計送貨日:後端會掃全量再篩,需擋下。 */
function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean {
return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === "";
}

const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSearch }) => {
const apiRef = useGridApiRef();
@@ -70,8 +74,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;
console.log("🔍 DoSearch - session:", session);
console.log("🔍 DoSearch - currentUserId:", currentUserId);
//console.log("🔍 DoSearch - session:", session);
//console.log("🔍 DoSearch - currentUserId:", currentUserId);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
/** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */
const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]);
@@ -93,6 +97,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
shopName: "",
deliveryOrderLines: "",
truckLanceCode: "", // 添加这个字段
floor: "All",
codeTo: "",
statusTo: "",
estimatedArrivalDateTo: "",
@@ -100,7 +105,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
supplierNameTo: "",
shopNameTo: "",
deliveryOrderLinesTo: "",
truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中
truckLanceCodeTo: "",
floorTo: "",
});

const [hasSearched, setHasSearched] = useState(false);
@@ -143,7 +149,14 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
...p,
pageNum: 1,
}));
}, [currentSearchParams.code, currentSearchParams.shopName, currentSearchParams.status, currentSearchParams.estimatedArrivalDate]);
}, [
currentSearchParams.code,
currentSearchParams.shopName,
currentSearchParams.status,
currentSearchParams.estimatedArrivalDate,
currentSearchParams.truckLanceCode,
currentSearchParams.floor,
]);


const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
@@ -151,6 +164,15 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
{ label: t("Code"), paramName: "code", type: "text" },
{ label: t("Shop Name"), paramName: "shopName", type: "text" },
{ label: t("Truck Lance Code"), paramName: "truckLanceCode", type: "text" },
{
label: t("Floor"),
paramName: "floor",
type: "select-labelled",
options: [
{ label: "2F", value: "2F" },
{ label: "4F", value: "4F" },
],
},
{
label: t("Estimated Arrival"),
paramName: "estimatedArrivalDate",
@@ -297,6 +319,16 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea
//SEARCH FUNCTION
const handleSearch = useCallback(async (query: SearchBoxInputs) => {
try {
if (isTruckLaneSearchMissingEta(query.truckLanceCode ?? "", query.estimatedArrivalDate ?? "")) {
await Swal.fire({
icon: "warning",
title: t("Truck lane search requires date title"),
text: t("Truck lane search requires date message"),
confirmButtonText: t("Confirm"),
});
return;
}

setCurrentSearchParams(query);

let estArrStartDate = query.estimatedArrivalDate;
@@ -313,6 +345,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
else{
status = query.status;
}

const floorParam = query.floor === "All" || !query.floor ? null : query.floor;
// 调用新的 API,传入分页参数和 truckLanceCode
const response = await fetchDoSearch(
@@ -325,7 +359,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
"", // estArrEndDate - 不再使用
pagingController.pageNum, // 传入当前页码
pagingController.pageSize, // 传入每页大小
query.truckLanceCode || "" // 添加 truckLanceCode 参数
query.truckLanceCode || "",
floorParam,
);

setSearchAllDos(response.records);
@@ -342,7 +377,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
setHasResults(false);
setExcludedRowIds([]);
}
}, [pagingController]);
}, [pagingController, t]);

useEffect(() => {
if (typeof window !== 'undefined') {
@@ -402,6 +437,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
// 使用新的分页参数重新搜索
const searchWithNewPage = async () => {
try {
if (
isTruckLaneSearchMissingEta(
currentSearchParams.truckLanceCode ?? "",
currentSearchParams.estimatedArrivalDate ?? "",
)
) {
await Swal.fire({
icon: "warning",
title: t("Truck lane search requires date title"),
text: t("Truck lane search requires date message"),
confirmButtonText: t("Confirm"),
});
return;
}
let estArrStartDate = currentSearchParams.estimatedArrivalDate;
const time = "T00:00:00";
@@ -416,6 +465,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
else{
status = currentSearchParams.status;
}

const floorParam =
currentSearchParams.floor === "All" || !currentSearchParams.floor
? null
: currentSearchParams.floor;
const response = await fetchDoSearch(
currentSearchParams.code || "",
@@ -427,7 +481,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
"",
newPagingController.pageNum,
newPagingController.pageSize,
currentSearchParams.truckLanceCode || "" // 添加这个参数
currentSearchParams.truckLanceCode || "",
floorParam,
);
setSearchAllDos(response.records);
@@ -438,7 +493,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
};
searchWithNewPage();
}
}, [pagingController, hasSearched, currentSearchParams]);
}, [pagingController, hasSearched, currentSearchParams, t]);

const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10);
@@ -451,6 +506,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
if (hasSearched && currentSearchParams) {
const searchWithNewPageSize = async () => {
try {
if (
isTruckLaneSearchMissingEta(
currentSearchParams.truckLanceCode ?? "",
currentSearchParams.estimatedArrivalDate ?? "",
)
) {
await Swal.fire({
icon: "warning",
title: t("Truck lane search requires date title"),
text: t("Truck lane search requires date message"),
confirmButtonText: t("Confirm"),
});
return;
}
let estArrStartDate = currentSearchParams.estimatedArrivalDate;
const time = "T00:00:00";
@@ -465,6 +534,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
else{
status = currentSearchParams.status;
}

const floorParam =
currentSearchParams.floor === "All" || !currentSearchParams.floor
? null
: currentSearchParams.floor;
const response = await fetchDoSearch(
currentSearchParams.code || "",
@@ -476,7 +550,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
"",
1, // 重置到第一页
newPageSize,
currentSearchParams.truckLanceCode || "" // 添加这个参数
currentSearchParams.truckLanceCode || "",
floorParam,
);
setSearchAllDos(response.records);
@@ -487,10 +562,24 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
};
searchWithNewPageSize();
}
}, [hasSearched, currentSearchParams]);
}, [hasSearched, currentSearchParams, t]);

const handleBatchRelease = useCallback(async (isWorkbench: boolean) => {
try {
if (
isTruckLaneSearchMissingEta(
currentSearchParams.truckLanceCode ?? "",
currentSearchParams.estimatedArrivalDate ?? "",
)
) {
await Swal.fire({
icon: "warning",
title: t("Truck lane search requires date title"),
text: t("Truck lane search requires date message"),
confirmButtonText: t("Confirm"),
});
return;
}
// 根据当前搜索条件获取所有匹配的记录(不分页)
let estArrStartDate = currentSearchParams.estimatedArrivalDate;
const time = "T00:00:00";
@@ -506,6 +595,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
else{
status = currentSearchParams.status;
}

const floorParam =
currentSearchParams.floor === "All" || !currentSearchParams.floor
? null
: currentSearchParams.floor;
// 显示加载提示
const loadingSwal = Swal.fire({
@@ -525,7 +619,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
currentSearchParams.shopName || "",
status,
estArrStartDate,
currentSearchParams.truckLanceCode || ""
currentSearchParams.truckLanceCode || "",
floorParam,
);
Swal.close();


+ 106
- 12
src/components/DoSearchWorkbench/DoSearchWorkbench.tsx Vedi File

@@ -45,7 +45,7 @@ type Props = {
/** 明細頁路由前綴,預設 `/doworkbench`;在 `/do copy 2` 等別名頁面請傳對應 base */
workbenchHrefBase?: string;
};
type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo", string>;
type SearchBoxInputs = Record<"code" | "status" | "estimatedArrivalDate" | "orderDate" | "supplierName" | "shopName" | "deliveryOrderLines" | "truckLanceCode" | "floor" | "codeTo" | "statusTo" | "estimatedArrivalDateTo" | "orderDateTo" | "supplierNameTo" | "shopNameTo" | "deliveryOrderLinesTo" | "truckLanceCodeTo" | "floorTo", string>;
type SearchParamNames = keyof SearchBoxInputs;

// put all this into a new component
@@ -57,6 +57,9 @@ type EntryError =
| undefined;
type DoRow = TableRow<Partial<DoResult>, EntryError>;

function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean {
return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === "";
}

const DoSearchWorkbench: React.FC<Props> = ({
filterArgs,
@@ -77,8 +80,8 @@ const DoSearchWorkbench: React.FC<Props> = ({
const router = useRouter();
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;
console.log("🔍 DoSearch - session:", session);
console.log("🔍 DoSearch - currentUserId:", currentUserId);
//console.log("🔍 DoSearch - session:", session);
//console.log("🔍 DoSearch - currentUserId:", currentUserId);
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
/** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */
const [excludedRowIds, setExcludedRowIds] = useState<number[]>([]);
@@ -100,6 +103,7 @@ const DoSearchWorkbench: React.FC<Props> = ({
shopName: "",
deliveryOrderLines: "",
truckLanceCode: "", // 添加这个字段
floor: "All",
codeTo: "",
statusTo: "",
estimatedArrivalDateTo: "",
@@ -107,7 +111,8 @@ const DoSearchWorkbench: React.FC<Props> = ({
supplierNameTo: "",
shopNameTo: "",
deliveryOrderLinesTo: "",
truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中
truckLanceCodeTo: "",
floorTo: "",
});

const [hasSearched, setHasSearched] = useState(false);
@@ -150,7 +155,14 @@ const DoSearchWorkbench: React.FC<Props> = ({
...p,
pageNum: 1,
}));
}, [currentSearchParams.code, currentSearchParams.shopName, currentSearchParams.status, currentSearchParams.estimatedArrivalDate]);
}, [
currentSearchParams.code,
currentSearchParams.shopName,
currentSearchParams.status,
currentSearchParams.estimatedArrivalDate,
currentSearchParams.truckLanceCode,
currentSearchParams.floor,
]);


const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
@@ -158,6 +170,15 @@ const DoSearchWorkbench: React.FC<Props> = ({
{ label: t("Code"), paramName: "code", type: "text" },
{ label: t("Shop Name"), paramName: "shopName", type: "text" },
{ label: t("Truck Lance Code"), paramName: "truckLanceCode", type: "text" },
{
label: t("Floor"),
paramName: "floor",
type: "select-labelled",
options: [
{ label: "2F", value: "2F" },
{ label: "4F", value: "4F" },
],
},
{
label: t("Estimated Arrival"),
paramName: "estimatedArrivalDate",
@@ -299,6 +320,16 @@ const DoSearchWorkbench: React.FC<Props> = ({
//SEARCH FUNCTION
const handleSearch = useCallback(async (query: SearchBoxInputs) => {
try {
if (isTruckLaneSearchMissingEta(query.truckLanceCode ?? "", query.estimatedArrivalDate ?? "")) {
await Swal.fire({
icon: "warning",
title: t("Truck lane search requires date title"),
text: t("Truck lane search requires date message"),
confirmButtonText: t("Confirm"),
});
return;
}

setCurrentSearchParams(query);

let estArrStartDate = query.estimatedArrivalDate;
@@ -315,6 +346,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
else{
status = query.status;
}

const floorParam = query.floor === "All" || !query.floor ? null : query.floor;
// 调用新的 API,传入分页参数和 truckLanceCode
const response = await fetchDoSearch(
@@ -327,7 +360,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
"", // estArrEndDate - 不再使用
pagingController.pageNum, // 传入当前页码
pagingController.pageSize, // 传入每页大小
query.truckLanceCode || "" // 添加 truckLanceCode 参数
query.truckLanceCode || "",
floorParam,
);

setSearchAllDos(response.records);
@@ -344,7 +378,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
setHasResults(false);
setExcludedRowIds([]);
}
}, [pagingController]);
}, [pagingController, t]);

useEffect(() => {
if (typeof window !== 'undefined') {
@@ -404,6 +438,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
// 使用新的分页参数重新搜索
const searchWithNewPage = async () => {
try {
if (
isTruckLaneSearchMissingEta(
currentSearchParams.truckLanceCode ?? "",
currentSearchParams.estimatedArrivalDate ?? "",
)
) {
await Swal.fire({
icon: "warning",
title: t("Truck lane search requires date title"),
text: t("Truck lane search requires date message"),
confirmButtonText: t("Confirm"),
});
return;
}
let estArrStartDate = currentSearchParams.estimatedArrivalDate;
const time = "T00:00:00";
@@ -418,6 +466,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
else{
status = currentSearchParams.status;
}

const floorParam =
currentSearchParams.floor === "All" || !currentSearchParams.floor
? null
: currentSearchParams.floor;
const response = await fetchDoSearch(
currentSearchParams.code || "",
@@ -429,7 +482,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
"",
newPagingController.pageNum,
newPagingController.pageSize,
currentSearchParams.truckLanceCode || "" // 添加这个参数
currentSearchParams.truckLanceCode || "",
floorParam,
);
setSearchAllDos(response.records);
@@ -440,7 +494,7 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
};
searchWithNewPage();
}
}, [pagingController, hasSearched, currentSearchParams]);
}, [pagingController, hasSearched, currentSearchParams, t]);

const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const newPageSize = parseInt(event.target.value, 10);
@@ -453,6 +507,20 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
if (hasSearched && currentSearchParams) {
const searchWithNewPageSize = async () => {
try {
if (
isTruckLaneSearchMissingEta(
currentSearchParams.truckLanceCode ?? "",
currentSearchParams.estimatedArrivalDate ?? "",
)
) {
await Swal.fire({
icon: "warning",
title: t("Truck lane search requires date title"),
text: t("Truck lane search requires date message"),
confirmButtonText: t("Confirm"),
});
return;
}
let estArrStartDate = currentSearchParams.estimatedArrivalDate;
const time = "T00:00:00";
@@ -467,6 +535,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
else{
status = currentSearchParams.status;
}

const floorParam =
currentSearchParams.floor === "All" || !currentSearchParams.floor
? null
: currentSearchParams.floor;
const response = await fetchDoSearch(
currentSearchParams.code || "",
@@ -478,7 +551,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
"",
1, // 重置到第一页
newPageSize,
currentSearchParams.truckLanceCode || "" // 添加这个参数
currentSearchParams.truckLanceCode || "",
floorParam,
);
setSearchAllDos(response.records);
@@ -489,10 +563,24 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
};
searchWithNewPageSize();
}
}, [hasSearched, currentSearchParams]);
}, [hasSearched, currentSearchParams, t]);

const handleBatchRelease = useCallback(async () => {
try {
if (
isTruckLaneSearchMissingEta(
currentSearchParams.truckLanceCode ?? "",
currentSearchParams.estimatedArrivalDate ?? "",
)
) {
await Swal.fire({
icon: "warning",
title: t("Truck lane search requires date title"),
text: t("Truck lane search requires date message"),
confirmButtonText: t("Confirm"),
});
return;
}
// 根据当前搜索条件获取所有匹配的记录(不分页)
let estArrStartDate = currentSearchParams.estimatedArrivalDate;
const time = "T00:00:00";
@@ -508,6 +596,11 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
else{
status = currentSearchParams.status;
}

const floorParam =
currentSearchParams.floor === "All" || !currentSearchParams.floor
? null
: currentSearchParams.floor;
// 显示加载提示
const loadingSwal = Swal.fire({
@@ -527,7 +620,8 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => {
currentSearchParams.shopName || "",
status,
estArrStartDate,
currentSearchParams.truckLanceCode || ""
currentSearchParams.truckLanceCode || "",
floorParam,
);
Swal.close();


+ 4
- 1
src/components/JoWorkbench/JoPickOrderList.tsx Vedi File

@@ -18,14 +18,16 @@ import { fetchAllJoPickOrders, AllJoPickOrderResponse } from "@/app/api/jo/actio
import JobPickExecution from "./newJobPickExecution";
import SearchBox, { Criterion } from "../SearchBox";
import dayjs from "dayjs";
import type { PrinterCombo } from "@/app/api/settings/printer";

interface Props {
/** Reserved for tabs parity with Jodetail; not used in workbench list yet. */
onSwitchToRecordTab?: () => void;
printerCombo: PrinterCombo[];
}

/** Jo workbench: same list + detail flow as Jodetail `JoPickOrderList`, detail uses `JoWorkbench/newJobPickExecution`. */
const JoPickOrderList: React.FC<Props> = () => {
const JoPickOrderList: React.FC<Props> = ({ printerCombo }) => {
const { t } = useTranslation(["common", "jo"]);
const today = dayjs().format("YYYY-MM-DD");
const [loading, setLoading] = useState(false);
@@ -153,6 +155,7 @@ const JoPickOrderList: React.FC<Props> = () => {
<JobPickExecution
filterArgs={{ pickOrderId: selectedPickOrderId, jobOrderId: selectedJobOrderId }}
onBackToList={handleBackToList}
printerCombo={printerCombo}
/>
</Box>
);


+ 1
- 1
src/components/JoWorkbench/JoWorkbenchTabs.tsx Vedi File

@@ -51,7 +51,7 @@ const JoWorkbenchTabs: React.FC<JoWorkbenchTabsProps> = ({
</TabPanel>

<TabPanel value={tab} index={1}>
<JoPickOrderList />
<JoPickOrderList printerCombo={printerCombo} />
</TabPanel>
</Box>
);


+ 165
- 27
src/components/JoWorkbench/newJobPickExecution.tsx Vedi File

@@ -8,6 +8,7 @@ import {
Typography,
Alert,
CircularProgress,
Autocomplete,
Table,
TableBody,
TableCell,
@@ -48,8 +49,9 @@ import {
import {
fetchJobOrderLotsHierarchicalByPickOrderIdWorkbench,
updateJoPickOrderHandledBy,
JobOrderLotsHierarchicalResponse,
JobOrderLotsHierarchicalWorkbenchResponse,
applyPickExecutionHoldAndChecked,
PrintPickRecord,
} from "@/app/api/jo/actions";
import { assignJobOrderPickOrderForWorkbench } from "@/app/api/jo/workbenchActions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
@@ -75,10 +77,13 @@ import {
workbenchBatchScanPick,
workbenchScanPick,
} from "@/app/api/doworkbench/actions";
import type { PrinterCombo } from "@/app/api/settings/printer";
import { msg, msgError } from "@/components/Swal/CustomAlerts";
interface Props {
filterArgs: Record<string, any>;
//onSwitchToRecordTab: () => void;
onBackToList?: () => void;
printerCombo?: PrinterCombo[];
}

/** 過期批號:與 noLot 類似——單筆/批量預設 0,除非 Issue 改數(對齊 GoodPickExecutiondetail) */
@@ -608,7 +613,7 @@ const QrCodeModal: React.FC<{
);
};

const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList, printerCombo = [] }) => {
const workbenchMode = true;
const { t } = useTranslation("jo");
const { t: tPick } = useTranslation("pickOrder");
@@ -640,7 +645,82 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const [qrScanErrorMsg, setQrScanErrorMsg] = useState<string>("");
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
const [jobOrderData, setJobOrderData] =
useState<JobOrderLotsHierarchicalResponse | null>(null);
useState<JobOrderLotsHierarchicalWorkbenchResponse | null>(null);
const a4Printers = useMemo(
() =>
(printerCombo || []).filter((p) =>
String(p.type || "")
.trim()
.toUpperCase()
.includes("A4"),
),
[printerCombo],
);
const printerOptions = useMemo(
() => (a4Printers.length > 0 ? a4Printers : printerCombo || []),
[a4Printers, printerCombo],
);
const isPrinterComboMissing = printerCombo.length === 0;
const [selectedPrinter, setSelectedPrinter] = useState<PrinterCombo | null>(
printerOptions.length > 0 ? printerOptions[0] : null,
);
const [printQty, setPrintQty] = useState<number>(1);

useEffect(() => {
// Keep selected printer valid when combo list changes.
if (!printerOptions.length) {
setSelectedPrinter(null);
return;
}
setSelectedPrinter((prev) => {
if (!prev) return printerOptions[0];
const stillExists = printerOptions.some((p) => p.id === prev.id);
return stillExists ? prev : printerOptions[0];
});
}, [printerOptions]);

useEffect(() => {
console.log("[JO Workbench] printerCombo:", printerCombo);
console.log("[JO Workbench] a4Printers:", a4Printers);
console.log("[JO Workbench] printerOptions:", printerOptions);
}, [printerCombo, a4Printers, printerOptions]);

const handlePickRecord = useCallback(
async (floor: "2F" | "3F" | "4F" | "ALL") => {
try {
const pickOrderId = jobOrderData?.pickOrder?.id;
if (!pickOrderId) {
msgError(t("Pick Order not found"));
return;
}
if (!selectedPrinter) {
msgError(t("Please select a printer first"));
return;
}
if (!printQty || printQty < 1) {
msgError(t("Please enter a valid print quantity (at least 1)"));
return;
}

const response = await PrintPickRecord({
pickOrderId,
printerId: selectedPrinter.id,
printQty,
floor,
});

if (response?.success) {
msg(t("Printed Successfully."));
} else {
msgError(response?.message || t("Print failed"));
}
} catch (e) {
console.error(e);
msgError(t("An error occurred while printing"));
}
},
[jobOrderData, printQty, selectedPrinter, t],
);
const workbenchStoreId = useMemo(() => {
const po = jobOrderData?.pickOrder as
| { storeId?: string | null }
@@ -772,7 +852,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
const [manualLotConfirmationOpen, setManualLotConfirmationOpen] =
useState(false);
const getAllLotsFromHierarchical = useCallback(
(data: JobOrderLotsHierarchicalResponse | null): any[] => {
(data: JobOrderLotsHierarchicalWorkbenchResponse | null): any[] => {
if (!data || !data.pickOrder || !data.pickOrderLines) {
return [];
}
@@ -3573,29 +3653,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
return Math.max(0, requiredQty - stockOutLineQty);
}, []);

// Search criteria
const searchCriteria: Criterion<any>[] = [
{
label: t("Pick Order Code"),
paramName: "pickOrderCode",
type: "text",
},
{
label: t("Item Code"),
paramName: "itemCode",
type: "text",
},
{
label: t("Item Name"),
paramName: "itemName",
type: "text",
},
{
label: t("Lot No"),
paramName: "lotNo",
type: "text",
},
];


const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController((prev) => ({
@@ -3829,11 +3887,91 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs, onBackToList }) => {
{floor}
</Button>
))}

<Box sx={{ flex: 1 }} />

<Typography variant="body2" sx={{ minWidth: "fit-content", mr: 1 }}>
{t("Select Printer")}:
</Typography>
<Autocomplete
options={printerOptions}
getOptionLabel={(option) =>
option.name || option.label || option.code || `Printer ${option.id}`
}
value={selectedPrinter}
onChange={(_, newValue) => setSelectedPrinter(newValue)}
sx={{ minWidth: 220 }}
size="small"
renderInput={(params) => (
<TextField
{...params}
placeholder={t("Printer")}
inputProps={{ ...params.inputProps, readOnly: true }}
/>
)}
/>
<Typography variant="body2" sx={{ minWidth: "fit-content", ml: 1 }}>
{t("Print Quantity")}:
</Typography>
<TextField
type="number"
label={t("Print Quantity")}
value={printQty}
onChange={(e) => {
const value = parseInt(e.target.value) || 1;
setPrintQty(Math.max(1, value));
}}
inputProps={{ min: 1, step: 1 }}
sx={{ width: 120 }}
size="small"
/>

<Button
variant="contained"
color="primary"
size="small"
onClick={() => handlePickRecord("ALL")}
>
{t("Print Pick Record")} ALL
</Button>
<Button
variant="contained"
color="primary"
size="small"
onClick={() => handlePickRecord("2F")}
>
{t("Print Pick Record")} 2F
</Button>
<Button
variant="contained"
color="primary"
size="small"
onClick={() => handlePickRecord("3F")}
>
{t("Print Pick Record")} 3F
</Button>
<Button
variant="contained"
color="primary"
size="small"
onClick={() => handlePickRecord("4F")}
>
{t("Print Pick Record")} 4F
</Button>
{isPrinterComboMissing && (
<Alert severity="warning" sx={{ py: 0, px: 1 }}>
{t("Printer list is empty")}
</Alert>
)}
</Box>
{/* Job Order Header */}
{jobOrderData && (
<Paper sx={{ p: 2 }}>
<Stack direction="row" spacing={4} useFlexGap flexWrap="wrap">
<Typography variant="subtitle1">
<strong>{t("Item Name")}:</strong>{" "}
{jobOrderData.pickOrder.jobOrder.itemCode || "-"}{" "}{jobOrderData.pickOrder.jobOrder.itemName || "-"}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Job Order")}:</strong>{" "}
{jobOrderData.pickOrder?.jobOrder?.code || "-"}


+ 1
- 1
src/components/Jodetail/JodetailSearch.tsx Vedi File

@@ -504,7 +504,7 @@ const JodetailSearch: React.FC<Props> = ({ printerCombo }) => {

{/* Content section */}
<Box sx={{ p: 2 }}>
{tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} />}
{tabIndex === 0 && <JoPickOrderList onSwitchToRecordTab={handleSwitchToRecordTab} printerCombo={printerCombo} />}
{tabIndex === 1 && (
<CompleteJobOrderRecord
filterArgs={filterArgs}


+ 3
- 0
src/i18n/zh/do.json Vedi File

@@ -15,6 +15,9 @@
"do workbench": "新版成品出倉",
"Do Workbench": "新版成品出倉",
"Delivery Order Code": "送貨訂單編號",
"Floor": "樓層",
"Truck lane search requires date title": "需選擇預計送貨日期",
"Truck lane search requires date message": "已填寫車線號碼時,請一併選擇預計送貨日期後再搜尋。",
"Truck Lance Code": "車線號碼",
"Select Remark": "選擇備註",
"Confirm Assignment": "確認分配",


+ 1
- 0
src/i18n/zh/jo.json Vedi File

@@ -157,6 +157,7 @@
"Target Date": "需求日期",
"Lot Required Pick Qty": "批號需求數",
"Available Qty": "可用數量",
"Job Order Match": "工單對料",
"Lot No": "批號",
"Submit Required Pick Qty": "提交需求數",


Caricamento…
Annulla
Salva