diff --git a/src/app/(main)/jo/workbench/page.tsx b/src/app/(main)/jo/workbench/page.tsx
index 2140658..d66d7b7 100644
--- a/src/app/(main)/jo/workbench/page.tsx
+++ b/src/app/(main)/jo/workbench/page.tsx
@@ -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 (
<>
}>
-
+
>
diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts
index e2805fc..b08ac05 100644
--- a/src/app/api/jo/actions.ts
+++ b/src/app/api/jo/actions.ts
@@ -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(
+ return serverFetchJson(
`${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order-workbench/${pickOrderId}`,
{
method: "GET",
diff --git a/src/components/DoDetail/DoDetail.tsx b/src/components/DoDetail/DoDetail.tsx
index 0801d81..496d8ec 100644
--- a/src/components/DoDetail/DoDetail.tsx
+++ b/src/components/DoDetail/DoDetail.tsx
@@ -40,8 +40,8 @@ const DoDetail: React.FC = ({
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({
defaultValues: defaultValues
})
diff --git a/src/components/DoSearch/DoSearch.tsx b/src/components/DoSearch/DoSearch.tsx
index 18a0a79..fe3799c 100644
--- a/src/components/DoSearch/DoSearch.tsx
+++ b/src/components/DoSearch/DoSearch.tsx
@@ -43,7 +43,7 @@ type Props = {
searchQuery?: Record;
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, EntryError>;
+/** 已填車線但未選預計送貨日:後端會掃全量再篩,需擋下。 */
+function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean {
+ return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === "";
+}
const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSearch }) => {
const apiRef = useGridApiRef();
@@ -70,8 +74,8 @@ const DoSearch: React.FC = ({ 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(null);
/** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */
const [excludedRowIds, setExcludedRowIds] = useState([]);
@@ -93,6 +97,7 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea
shopName: "",
deliveryOrderLines: "",
truckLanceCode: "", // 添加这个字段
+ floor: "All",
codeTo: "",
statusTo: "",
estimatedArrivalDateTo: "",
@@ -100,7 +105,8 @@ const DoSearch: React.FC = ({ filterArgs, searchQuery, onDeliveryOrderSea
supplierNameTo: "",
shopNameTo: "",
deliveryOrderLinesTo: "",
- truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中
+ truckLanceCodeTo: "",
+ floorTo: "",
});
const [hasSearched, setHasSearched] = useState(false);
@@ -143,7 +149,14 @@ const DoSearch: React.FC = ({ 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[] = useMemo(
@@ -151,6 +164,15 @@ const DoSearch: React.FC = ({ 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 = ({ 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) => {
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();
diff --git a/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx b/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx
index 4e2301b..7ffba63 100644
--- a/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx
+++ b/src/components/DoSearchWorkbench/DoSearchWorkbench.tsx
@@ -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, EntryError>;
+function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean {
+ return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === "";
+}
const DoSearchWorkbench: React.FC = ({
filterArgs,
@@ -77,8 +80,8 @@ const DoSearchWorkbench: React.FC = ({
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(null);
/** 使用者明確取消勾選的送貨單 id;未在此集合中的搜尋結果視為「已選」以便跨頁記憶 */
const [excludedRowIds, setExcludedRowIds] = useState([]);
@@ -100,6 +103,7 @@ const DoSearchWorkbench: React.FC = ({
shopName: "",
deliveryOrderLines: "",
truckLanceCode: "", // 添加这个字段
+ floor: "All",
codeTo: "",
statusTo: "",
estimatedArrivalDateTo: "",
@@ -107,7 +111,8 @@ const DoSearchWorkbench: React.FC = ({
supplierNameTo: "",
shopNameTo: "",
deliveryOrderLinesTo: "",
- truckLanceCodeTo: "" // 这个字段已经存在,但需要确保在类型定义中
+ truckLanceCodeTo: "",
+ floorTo: "",
});
const [hasSearched, setHasSearched] = useState(false);
@@ -150,7 +155,14 @@ const DoSearchWorkbench: React.FC = ({
...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[] = useMemo(
@@ -158,6 +170,15 @@ const DoSearchWorkbench: React.FC = ({
{ 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 = ({
//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) => {
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();
diff --git a/src/components/JoWorkbench/JoPickOrderList.tsx b/src/components/JoWorkbench/JoPickOrderList.tsx
index 5f3a0be..f40abef 100644
--- a/src/components/JoWorkbench/JoPickOrderList.tsx
+++ b/src/components/JoWorkbench/JoPickOrderList.tsx
@@ -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 = () => {
+const JoPickOrderList: React.FC = ({ 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 = () => {
);
diff --git a/src/components/JoWorkbench/JoWorkbenchTabs.tsx b/src/components/JoWorkbench/JoWorkbenchTabs.tsx
index 4d07254..caa662a 100644
--- a/src/components/JoWorkbench/JoWorkbenchTabs.tsx
+++ b/src/components/JoWorkbench/JoWorkbenchTabs.tsx
@@ -51,7 +51,7 @@ const JoWorkbenchTabs: React.FC = ({
-
+
);
diff --git a/src/components/JoWorkbench/newJobPickExecution.tsx b/src/components/JoWorkbench/newJobPickExecution.tsx
index 5c09d51..1579fb9 100644
--- a/src/components/JoWorkbench/newJobPickExecution.tsx
+++ b/src/components/JoWorkbench/newJobPickExecution.tsx
@@ -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;
//onSwitchToRecordTab: () => void;
onBackToList?: () => void;
+ printerCombo?: PrinterCombo[];
}
/** 過期批號:與 noLot 類似——單筆/批量預設 0,除非 Issue 改數(對齊 GoodPickExecutiondetail) */
@@ -608,7 +613,7 @@ const QrCodeModal: React.FC<{
);
};
-const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => {
+const JobPickExecution: React.FC = ({ filterArgs, onBackToList, printerCombo = [] }) => {
const workbenchMode = true;
const { t } = useTranslation("jo");
const { t: tPick } = useTranslation("pickOrder");
@@ -640,7 +645,82 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => {
const [qrScanErrorMsg, setQrScanErrorMsg] = useState("");
const [qrScanSuccess, setQrScanSuccess] = useState(false);
const [jobOrderData, setJobOrderData] =
- useState(null);
+ useState(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(
+ printerOptions.length > 0 ? printerOptions[0] : null,
+ );
+ const [printQty, setPrintQty] = useState(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 = ({ 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 = ({ filterArgs, onBackToList }) => {
return Math.max(0, requiredQty - stockOutLineQty);
}, []);
- // Search criteria
- const searchCriteria: Criterion[] = [
- {
- 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 = ({ filterArgs, onBackToList }) => {
{floor}
))}
+
+
+
+
+ {t("Select Printer")}:
+
+
+ option.name || option.label || option.code || `Printer ${option.id}`
+ }
+ value={selectedPrinter}
+ onChange={(_, newValue) => setSelectedPrinter(newValue)}
+ sx={{ minWidth: 220 }}
+ size="small"
+ renderInput={(params) => (
+
+ )}
+ />
+
+ {t("Print Quantity")}:
+
+ {
+ const value = parseInt(e.target.value) || 1;
+ setPrintQty(Math.max(1, value));
+ }}
+ inputProps={{ min: 1, step: 1 }}
+ sx={{ width: 120 }}
+ size="small"
+ />
+
+
+
+
+
+ {isPrinterComboMissing && (
+
+ {t("Printer list is empty")}
+
+ )}
{/* Job Order Header */}
{jobOrderData && (
+
+ {t("Item Name")}:{" "}
+ {jobOrderData.pickOrder.jobOrder.itemCode || "-"}{" "}{jobOrderData.pickOrder.jobOrder.itemName || "-"}
+
{t("Job Order")}:{" "}
{jobOrderData.pickOrder?.jobOrder?.code || "-"}
diff --git a/src/components/Jodetail/JodetailSearch.tsx b/src/components/Jodetail/JodetailSearch.tsx
index 83c9749..be37cf9 100644
--- a/src/components/Jodetail/JodetailSearch.tsx
+++ b/src/components/Jodetail/JodetailSearch.tsx
@@ -504,7 +504,7 @@ const JodetailSearch: React.FC = ({ printerCombo }) => {
{/* Content section */}
- {tabIndex === 0 && }
+ {tabIndex === 0 && }
{tabIndex === 1 && (