|
|
@@ -11,6 +11,8 @@ import { |
|
|
releaseDo, |
|
|
releaseDo, |
|
|
startBatchReleaseAsync, |
|
|
startBatchReleaseAsync, |
|
|
getBatchReleaseProgress, |
|
|
getBatchReleaseProgress, |
|
|
|
|
|
check4FTrucksBatch, |
|
|
|
|
|
type ProblemDoDto, |
|
|
} from "@/app/api/do/actions"; |
|
|
} from "@/app/api/do/actions"; |
|
|
import { |
|
|
import { |
|
|
startWorkbenchBatchReleaseAsyncV2, |
|
|
startWorkbenchBatchReleaseAsyncV2, |
|
|
@@ -18,7 +20,7 @@ import { |
|
|
} from "@/app/api/doworkbench/actions"; |
|
|
} from "@/app/api/doworkbench/actions"; |
|
|
|
|
|
|
|
|
import { useRouter } from "next/navigation"; |
|
|
import { useRouter } from "next/navigation"; |
|
|
import React, { ForwardedRef, useCallback, useEffect, useMemo, useState } from "react"; |
|
|
|
|
|
|
|
|
import React, { ForwardedRef, useCallback, useEffect, useMemo, useRef, useState } from "react"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
import { useTranslation } from "react-i18next"; |
|
|
import { Criterion } from "../SearchBox"; |
|
|
import { Criterion } from "../SearchBox"; |
|
|
import { isEmpty, sortBy, uniqBy } from "lodash"; |
|
|
import { isEmpty, sortBy, uniqBy } from "lodash"; |
|
|
@@ -41,7 +43,7 @@ import { |
|
|
SubmitHandler, |
|
|
SubmitHandler, |
|
|
useForm, |
|
|
useForm, |
|
|
} from "react-hook-form"; |
|
|
} from "react-hook-form"; |
|
|
import { Box, Button, Paper, Stack, Tab, Tabs, TablePagination, Typography } from "@mui/material"; |
|
|
|
|
|
|
|
|
import { Alert, Box, Button, Paper, Stack, Tab, Tabs, TablePagination, Typography } from "@mui/material"; |
|
|
import StyledDataGrid from "../StyledDataGrid"; |
|
|
import StyledDataGrid from "../StyledDataGrid"; |
|
|
import Swal from "sweetalert2"; |
|
|
import Swal from "sweetalert2"; |
|
|
import { useSession } from "next-auth/react"; |
|
|
import { useSession } from "next-auth/react"; |
|
|
@@ -62,7 +64,7 @@ type Props = { |
|
|
searchQuery?: Record<string, any>; |
|
|
searchQuery?: Record<string, any>; |
|
|
onDeliveryOrderSearch?: () => void; |
|
|
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; |
|
|
type SearchParamNames = keyof SearchBoxInputs; |
|
|
type DoSearchTab = "2F" | "4F" | "TRUCK_X" | "ETRA" | "REPLENISH"; |
|
|
type DoSearchTab = "2F" | "4F" | "TRUCK_X" | "ETRA" | "REPLENISH"; |
|
|
type TabFilter = { floor: "2F" | "4F" | null; isExtra: boolean; forceTruckKeyword?: string }; |
|
|
type TabFilter = { floor: "2F" | "4F" | null; isExtra: boolean; forceTruckKeyword?: string }; |
|
|
@@ -76,9 +78,141 @@ type EntryError = |
|
|
| undefined; |
|
|
| undefined; |
|
|
type DoRow = TableRow<Partial<DoResult>, EntryError>; |
|
|
type DoRow = TableRow<Partial<DoResult>, EntryError>; |
|
|
|
|
|
|
|
|
/** 已填車線但未選預計送貨日:後端會掃全量再篩,需擋下。 */ |
|
|
|
|
|
function isTruckLaneSearchMissingEta(truckLanceCode: string, estimatedArrivalDate: string): boolean { |
|
|
|
|
|
return truckLanceCode.trim() !== "" && estimatedArrivalDate.trim() === ""; |
|
|
|
|
|
|
|
|
/** 2/F、4/F、車線-X 必須有預計送貨日,否則後端無法判斷當天車線。 */ |
|
|
|
|
|
function deliveryDateRequired(tab: DoSearchTab): boolean { |
|
|
|
|
|
return tab === "2F" || tab === "4F" || tab === "TRUCK_X"; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function escapeHtml(value: string): string { |
|
|
|
|
|
return value |
|
|
|
|
|
.replace(/&/g, "&") |
|
|
|
|
|
.replace(/</g, "<") |
|
|
|
|
|
.replace(/>/g, ">") |
|
|
|
|
|
.replace(/"/g, """); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function formatCheckDate(value: unknown): string { |
|
|
|
|
|
if (Array.isArray(value) && value.length >= 3) { |
|
|
|
|
|
const year = Number(value[0]); |
|
|
|
|
|
const month = Number(value[1]); |
|
|
|
|
|
const day = Number(value[2]); |
|
|
|
|
|
if (!year || !month || !day) return ""; |
|
|
|
|
|
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`; |
|
|
|
|
|
} |
|
|
|
|
|
if (typeof value === "string" && value.length >= 10) return value.slice(0, 10); |
|
|
|
|
|
return ""; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const WEEKDAY_LABEL_KEY = ["Weekday Sun", "Weekday Mon", "Weekday Tue", "Weekday Wed", "Weekday Thu", "Weekday Fri", "Weekday Sat"]; |
|
|
|
|
|
|
|
|
|
|
|
function weekdayLabelKey(isoDate: string): string { |
|
|
|
|
|
const parsed = new Date(`${isoDate}T00:00:00`); |
|
|
|
|
|
if (Number.isNaN(parsed.getTime())) return ""; |
|
|
|
|
|
return WEEKDAY_LABEL_KEY[parsed.getDay()] ?? ""; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const DAY_TRUCK_WARN_LIMIT = 12; |
|
|
|
|
|
|
|
|
|
|
|
const LANE_DAY_KEYS: Array<[string, string]> = [ |
|
|
|
|
|
["sat", "Weekday Sat"], |
|
|
|
|
|
["sun", "Weekday Sun"], |
|
|
|
|
|
["mon", "Weekday Mon"], |
|
|
|
|
|
["tue", "Weekday Tue"], |
|
|
|
|
|
["wed", "Weekday Wed"], |
|
|
|
|
|
["thu", "Weekday Thu"], |
|
|
|
|
|
["fri", "Weekday Fri"], |
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
function shortShopLabel(shopCode: string, shopName: string, fallback: string): string { |
|
|
|
|
|
const branch = shopName.includes(" - ") |
|
|
|
|
|
? shopName.split(" - ").slice(1).join(" - ").trim() |
|
|
|
|
|
: shopName.trim(); |
|
|
|
|
|
const label = [shopCode, branch].filter(Boolean).join(" "); |
|
|
|
|
|
return label || fallback; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function weekdayNamesInLanes( |
|
|
|
|
|
lanes: string[], |
|
|
|
|
|
t: (key: string, options?: Record<string, unknown>) => string, |
|
|
|
|
|
): string[] { |
|
|
|
|
|
const names: string[] = []; |
|
|
|
|
|
const blob = lanes.join(" ").toLowerCase(); |
|
|
|
|
|
for (const [token, key] of LANE_DAY_KEYS) { |
|
|
|
|
|
if (blob.includes(token)) names.push(t(key)); |
|
|
|
|
|
} |
|
|
|
|
|
return names; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function buildDayTruckWarnHtml( |
|
|
|
|
|
problems: ProblemDoDto[], |
|
|
|
|
|
skipCount: number, |
|
|
|
|
|
releaseCount: number, |
|
|
|
|
|
t: (key: string, options?: Record<string, unknown>) => string, |
|
|
|
|
|
): { html: string; title: string } { |
|
|
|
|
|
const groups = new Map<string, { shop: string; date: string; weekday: string; lanes: string[] }>(); |
|
|
|
|
|
for (const problem of problems) { |
|
|
|
|
|
const date = formatCheckDate(problem.targetDate); |
|
|
|
|
|
const firstTruck = problem.availableTrucks?.[0]; |
|
|
|
|
|
const shopCode = firstTruck?.shopCode?.trim() || ""; |
|
|
|
|
|
const shopName = firstTruck?.shopName?.trim() || ""; |
|
|
|
|
|
const shop = shortShopLabel( |
|
|
|
|
|
shopCode, |
|
|
|
|
|
shopName, |
|
|
|
|
|
problem.deliveryOrderCode || String(problem.deliveryOrderId), |
|
|
|
|
|
); |
|
|
|
|
|
const key = `${shopCode || problem.deliveryOrderCode}|${date}`; |
|
|
|
|
|
const current = groups.get(key) ?? { |
|
|
|
|
|
shop, |
|
|
|
|
|
date, |
|
|
|
|
|
weekday: weekdayLabelKey(date) ? t(weekdayLabelKey(date)) : "", |
|
|
|
|
|
lanes: [], |
|
|
|
|
|
}; |
|
|
|
|
|
for (const truck of problem.availableTrucks ?? []) { |
|
|
|
|
|
const lane = truck.truckLanceCode?.trim(); |
|
|
|
|
|
if (lane && !current.lanes.includes(lane)) current.lanes.push(lane); |
|
|
|
|
|
} |
|
|
|
|
|
groups.set(key, current); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const rows = Array.from(groups.values()); |
|
|
|
|
|
const visible = rows.slice(0, DAY_TRUCK_WARN_LIMIT); |
|
|
|
|
|
const hiddenCount = rows.length - visible.length; |
|
|
|
|
|
const allSkipped = releaseCount === 0; |
|
|
|
|
|
const sameDate = visible.every((row) => row.date && row.date === visible[0]?.date); |
|
|
|
|
|
const sameWeekday = visible.every((row) => row.weekday && row.weekday === visible[0]?.weekday); |
|
|
|
|
|
const missingDay = sameWeekday && visible[0]?.weekday |
|
|
|
|
|
? visible[0].weekday |
|
|
|
|
|
: t("4F day truck warn day fallback"); |
|
|
|
|
|
const reason = sameDate && sameWeekday && visible[0]?.date |
|
|
|
|
|
? t("4F day truck warn reason", { date: visible[0].date, weekday: visible[0].weekday }) |
|
|
|
|
|
: t("4F day truck warn reason mixed"); |
|
|
|
|
|
const items = visible |
|
|
|
|
|
.map((row) => { |
|
|
|
|
|
const dayNames = weekdayNamesInLanes(row.lanes, t); |
|
|
|
|
|
const prefix = sameDate ? "" : `${row.date} ${row.weekday} `; |
|
|
|
|
|
const line = dayNames.length > 0 |
|
|
|
|
|
? t("4F day truck warn line", { shop: `${prefix}${row.shop}`, lanes: dayNames.join("、") }) |
|
|
|
|
|
: t("4F day truck warn line none", { shop: `${prefix}${row.shop}` }); |
|
|
|
|
|
return `<li style="margin:0 0 4px;">${escapeHtml(line)}</li>`; |
|
|
|
|
|
}) |
|
|
|
|
|
.join(""); |
|
|
|
|
|
const more = hiddenCount > 0 |
|
|
|
|
|
? `<p style="margin:6px 0 0;color:#666;">${escapeHtml(t("4F day truck warn more", { count: hiddenCount }))}</p>` |
|
|
|
|
|
: ""; |
|
|
|
|
|
const action = t(allSkipped ? "4F day truck warn action none" : "4F day truck warn action some", { day: missingDay }); |
|
|
|
|
|
const title = t(allSkipped ? "4F day truck warn title none" : "4F day truck warn title some", { |
|
|
|
|
|
count: skipCount, |
|
|
|
|
|
skip: skipCount, |
|
|
|
|
|
}); |
|
|
|
|
|
return { |
|
|
|
|
|
title, |
|
|
|
|
|
html: ` |
|
|
|
|
|
<p style="margin:0;">${escapeHtml(reason)}</p> |
|
|
|
|
|
<ul style="margin:8px 0 0;padding-left:18px;">${items}</ul> |
|
|
|
|
|
${more} |
|
|
|
|
|
<p style="margin:10px 0 0;font-weight:700;">${escapeHtml(action)}</p> |
|
|
|
|
|
`, |
|
|
|
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 77 | v1.0.0 | 2026-09-08 */ |
|
|
/** FP-MTMS Version Checklist | Functions Ref. No. 77 | v1.0.0 | 2026-09-08 */ |
|
|
@@ -96,6 +230,8 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
const router = useRouter(); |
|
|
const router = useRouter(); |
|
|
const { data: session } = useSession() as { data: SessionWithTokens | null }; |
|
|
const { data: session } = useSession() as { data: SessionWithTokens | null }; |
|
|
const currentUserId = session?.id ? parseInt(session.id) : undefined; |
|
|
const currentUserId = session?.id ? parseInt(session.id) : undefined; |
|
|
|
|
|
const batchReleaseLockRef = useRef(false); |
|
|
|
|
|
const [isBatchReleasing, setIsBatchReleasing] = useState(false); |
|
|
//console.log("🔍 DoSearch - session:", session); |
|
|
//console.log("🔍 DoSearch - session:", session); |
|
|
//console.log("🔍 DoSearch - currentUserId:", currentUserId); |
|
|
//console.log("🔍 DoSearch - currentUserId:", currentUserId); |
|
|
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); |
|
|
const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null); |
|
|
@@ -119,6 +255,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
shopName: "", |
|
|
shopName: "", |
|
|
deliveryOrderLines: "", |
|
|
deliveryOrderLines: "", |
|
|
truckLanceCode: "", |
|
|
truckLanceCode: "", |
|
|
|
|
|
floor: "All", |
|
|
codeTo: "", |
|
|
codeTo: "", |
|
|
statusTo: "", |
|
|
statusTo: "", |
|
|
estimatedArrivalDateTo: "", |
|
|
estimatedArrivalDateTo: "", |
|
|
@@ -127,6 +264,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
shopNameTo: "", |
|
|
shopNameTo: "", |
|
|
deliveryOrderLinesTo: "", |
|
|
deliveryOrderLinesTo: "", |
|
|
truckLanceCodeTo: "", |
|
|
truckLanceCodeTo: "", |
|
|
|
|
|
floorTo: "", |
|
|
}); |
|
|
}); |
|
|
const createClearedSearchParams = useCallback( |
|
|
const createClearedSearchParams = useCallback( |
|
|
(source: SearchBoxInputs): SearchBoxInputs => ({ |
|
|
(source: SearchBoxInputs): SearchBoxInputs => ({ |
|
|
@@ -138,6 +276,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
shopName: "", |
|
|
shopName: "", |
|
|
deliveryOrderLines: "", |
|
|
deliveryOrderLines: "", |
|
|
truckLanceCode: "", |
|
|
truckLanceCode: "", |
|
|
|
|
|
floor: "All", |
|
|
codeTo: "", |
|
|
codeTo: "", |
|
|
statusTo: "", |
|
|
statusTo: "", |
|
|
estimatedArrivalDateTo: source.estimatedArrivalDateTo || "", |
|
|
estimatedArrivalDateTo: source.estimatedArrivalDateTo || "", |
|
|
@@ -146,12 +285,14 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
shopNameTo: "", |
|
|
shopNameTo: "", |
|
|
deliveryOrderLinesTo: "", |
|
|
deliveryOrderLinesTo: "", |
|
|
truckLanceCodeTo: "", |
|
|
truckLanceCodeTo: "", |
|
|
|
|
|
floorTo: "", |
|
|
}), |
|
|
}), |
|
|
[], |
|
|
[], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const [hasSearched, setHasSearched] = useState(false); |
|
|
const [hasSearched, setHasSearched] = useState(false); |
|
|
const [hasResults, setHasResults] = useState(false); |
|
|
const [hasResults, setHasResults] = useState(false); |
|
|
|
|
|
const [dateFieldError, setDateFieldError] = useState(false); |
|
|
|
|
|
|
|
|
const { |
|
|
const { |
|
|
rowSelectionModel, |
|
|
rowSelectionModel, |
|
|
@@ -172,6 +313,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
currentSearchParams.status, |
|
|
currentSearchParams.status, |
|
|
currentSearchParams.estimatedArrivalDate, |
|
|
currentSearchParams.estimatedArrivalDate, |
|
|
currentSearchParams.truckLanceCode, |
|
|
currentSearchParams.truckLanceCode, |
|
|
|
|
|
currentSearchParams.floor, |
|
|
]); |
|
|
]); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -180,11 +322,23 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
{ label: t("Code"), paramName: "code", type: "text" }, |
|
|
{ label: t("Code"), paramName: "code", type: "text" }, |
|
|
{ label: t("Shop Name"), paramName: "shopName", type: "text" }, |
|
|
{ label: t("Shop Name"), paramName: "shopName", type: "text" }, |
|
|
{ label: t("Truck Lance Code"), paramName: "truckLanceCode", type: "text" }, |
|
|
{ label: t("Truck Lance Code"), paramName: "truckLanceCode", type: "text" }, |
|
|
|
|
|
...(activeTab === "TRUCK_X" |
|
|
|
|
|
? [{ |
|
|
|
|
|
label: t("Floor"), |
|
|
|
|
|
paramName: "floor" as const, |
|
|
|
|
|
type: "select-labelled" as const, |
|
|
|
|
|
options: [ |
|
|
|
|
|
{ label: "2/F", value: "2F" }, |
|
|
|
|
|
{ label: "4/F", value: "4F" }, |
|
|
|
|
|
], |
|
|
|
|
|
}] |
|
|
|
|
|
: []), |
|
|
{ |
|
|
{ |
|
|
label: t("Estimated Arrival"), |
|
|
label: t("Estimated Arrival"), |
|
|
paramName: "estimatedArrivalDate", |
|
|
paramName: "estimatedArrivalDate", |
|
|
type: "date", |
|
|
type: "date", |
|
|
preFilledValue: currentSearchParams.estimatedArrivalDate || "", |
|
|
preFilledValue: currentSearchParams.estimatedArrivalDate || "", |
|
|
|
|
|
errorText: dateFieldError ? t("Estimated arrival required") : undefined, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
{ |
|
|
label: t("Status"), |
|
|
label: t("Status"), |
|
|
@@ -197,7 +351,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
] |
|
|
] |
|
|
} |
|
|
} |
|
|
], |
|
|
], |
|
|
[t, currentSearchParams.estimatedArrivalDate], |
|
|
|
|
|
|
|
|
[t, activeTab, currentSearchParams.estimatedArrivalDate, dateFieldError], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const onReset = useCallback(async () => { |
|
|
const onReset = useCallback(async () => { |
|
|
@@ -206,6 +360,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
setTotalCount(0); |
|
|
setTotalCount(0); |
|
|
setHasSearched(false); |
|
|
setHasSearched(false); |
|
|
setHasResults(false); |
|
|
setHasResults(false); |
|
|
|
|
|
setDateFieldError(false); |
|
|
resetSelection(); |
|
|
resetSelection(); |
|
|
setPagingController({ pageNum: 1, pageSize: 10 }); |
|
|
setPagingController({ pageNum: 1, pageSize: 10 }); |
|
|
} |
|
|
} |
|
|
@@ -351,20 +506,19 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
const tabFilter = resolveTabFilter(effectiveTab); |
|
|
const tabFilter = resolveTabFilter(effectiveTab); |
|
|
const tabTruckKeyword = tabFilter.forceTruckKeyword ?? ""; |
|
|
const tabTruckKeyword = tabFilter.forceTruckKeyword ?? ""; |
|
|
const effectiveTruckLanceCode = tabTruckKeyword || query.truckLanceCode || ""; |
|
|
const effectiveTruckLanceCode = tabTruckKeyword || query.truckLanceCode || ""; |
|
|
const shouldValidateTruckLane = effectiveTab !== "TRUCK_X"; |
|
|
|
|
|
|
|
|
const searchFloor = |
|
|
|
|
|
effectiveTab === "TRUCK_X" |
|
|
|
|
|
? query.floor === "2F" || query.floor === "4F" |
|
|
|
|
|
? query.floor |
|
|
|
|
|
: null |
|
|
|
|
|
: tabFilter.floor; |
|
|
|
|
|
const excludeUnassigned = effectiveTab === "2F" || effectiveTab === "4F"; |
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
|
shouldValidateTruckLane && |
|
|
|
|
|
isTruckLaneSearchMissingEta(effectiveTruckLanceCode, 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"), |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
if (deliveryDateRequired(effectiveTab) && !(query.estimatedArrivalDate ?? "").trim()) { |
|
|
|
|
|
setDateFieldError(true); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
setDateFieldError(false); |
|
|
|
|
|
|
|
|
let estArrStartDate = query.estimatedArrivalDate; |
|
|
let estArrStartDate = query.estimatedArrivalDate; |
|
|
const time = "T00:00:00"; |
|
|
const time = "T00:00:00"; |
|
|
@@ -385,8 +539,9 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
pageNum, |
|
|
pageNum, |
|
|
pageSize, |
|
|
pageSize, |
|
|
effectiveTruckLanceCode, |
|
|
effectiveTruckLanceCode, |
|
|
tabFilter.floor, |
|
|
|
|
|
|
|
|
searchFloor, |
|
|
tabFilter.isExtra, |
|
|
tabFilter.isExtra, |
|
|
|
|
|
excludeUnassigned, |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
setSearchAllDos(response.records); |
|
|
setSearchAllDos(response.records); |
|
|
@@ -509,6 +664,10 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const handleBatchRelease = useCallback(async (isWorkbench: boolean) => { |
|
|
const handleBatchRelease = useCallback(async (isWorkbench: boolean) => { |
|
|
|
|
|
if (batchReleaseLockRef.current) return; |
|
|
|
|
|
batchReleaseLockRef.current = true; |
|
|
|
|
|
setIsBatchReleasing(true); |
|
|
|
|
|
try { |
|
|
try { |
|
|
try { |
|
|
if (!currentUserId) { |
|
|
if (!currentUserId) { |
|
|
await Swal.fire({ |
|
|
await Swal.fire({ |
|
|
@@ -523,22 +682,18 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
const tabFilter = resolveTabFilter(activeTab); |
|
|
const tabFilter = resolveTabFilter(activeTab); |
|
|
const tabTruckKeyword = tabFilter.forceTruckKeyword ?? ""; |
|
|
const tabTruckKeyword = tabFilter.forceTruckKeyword ?? ""; |
|
|
const effectiveTruckLanceCode = tabTruckKeyword || currentSearchParams.truckLanceCode || ""; |
|
|
const effectiveTruckLanceCode = tabTruckKeyword || currentSearchParams.truckLanceCode || ""; |
|
|
const shouldValidateTruckLane = activeTab !== "TRUCK_X"; |
|
|
|
|
|
if ( |
|
|
|
|
|
shouldValidateTruckLane && |
|
|
|
|
|
isTruckLaneSearchMissingEta( |
|
|
|
|
|
effectiveTruckLanceCode, |
|
|
|
|
|
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"), |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
const searchFloor = |
|
|
|
|
|
activeTab === "TRUCK_X" |
|
|
|
|
|
? currentSearchParams.floor === "2F" || currentSearchParams.floor === "4F" |
|
|
|
|
|
? currentSearchParams.floor |
|
|
|
|
|
: null |
|
|
|
|
|
: tabFilter.floor; |
|
|
|
|
|
const excludeUnassigned = activeTab === "2F" || activeTab === "4F"; |
|
|
|
|
|
if (deliveryDateRequired(activeTab) && !(currentSearchParams.estimatedArrivalDate ?? "").trim()) { |
|
|
|
|
|
setDateFieldError(true); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
setDateFieldError(false); |
|
|
// 根据当前搜索条件获取所有匹配的记录(不分页) |
|
|
// 根据当前搜索条件获取所有匹配的记录(不分页) |
|
|
let estArrStartDate = currentSearchParams.estimatedArrivalDate; |
|
|
let estArrStartDate = currentSearchParams.estimatedArrivalDate; |
|
|
const time = "T00:00:00"; |
|
|
const time = "T00:00:00"; |
|
|
@@ -574,8 +729,9 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
status, |
|
|
status, |
|
|
estArrStartDate, |
|
|
estArrStartDate, |
|
|
effectiveTruckLanceCode, |
|
|
effectiveTruckLanceCode, |
|
|
tabFilter.floor, |
|
|
|
|
|
|
|
|
searchFloor, |
|
|
tabFilter.isExtra, |
|
|
tabFilter.isExtra, |
|
|
|
|
|
excludeUnassigned, |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
Swal.close(); |
|
|
Swal.close(); |
|
|
@@ -606,7 +762,23 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const dosForRelease = allMatchingDos.filter((row) => idsToRelease.includes(row.id)); |
|
|
|
|
|
|
|
|
let dayTruckProblems: ProblemDoDto[] = []; |
|
|
|
|
|
if (activeTab === "2F" || activeTab === "4F") { |
|
|
|
|
|
try { |
|
|
|
|
|
const truckCheck = await check4FTrucksBatch(idsToRelease); |
|
|
|
|
|
dayTruckProblems = truckCheck?.problems ?? []; |
|
|
|
|
|
} catch (checkError) { |
|
|
|
|
|
console.error("4F day truck check failed:", checkError); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
const skipIdSet = new Set(dayTruckProblems.map((problem) => problem.deliveryOrderId)); |
|
|
|
|
|
const releaseIds = idsToRelease.filter((id) => !skipIdSet.has(id)); |
|
|
|
|
|
const allDayTrucksSkipped = dayTruckProblems.length > 0 && releaseIds.length === 0; |
|
|
|
|
|
const dayTruckWarn = dayTruckProblems.length > 0 |
|
|
|
|
|
? buildDayTruckWarnHtml(dayTruckProblems, skipIdSet.size, releaseIds.length, t) |
|
|
|
|
|
: null; |
|
|
|
|
|
|
|
|
|
|
|
const dosForRelease = allMatchingDos.filter((row) => releaseIds.includes(row.id)); |
|
|
const replenishmentFetchParams = deriveReplenishmentFetchParams( |
|
|
const replenishmentFetchParams = deriveReplenishmentFetchParams( |
|
|
dosForRelease, |
|
|
dosForRelease, |
|
|
currentSearchParams.shopName || "", |
|
|
currentSearchParams.shopName || "", |
|
|
@@ -624,21 +796,32 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const hasReplenishmentPreview = pendingReplenishments.length > 0; |
|
|
const hasReplenishmentPreview = pendingReplenishments.length > 0; |
|
|
|
|
|
const widenReleaseDialog = hasReplenishmentPreview || dayTruckWarn != null; |
|
|
|
|
|
const confirmText = allDayTrucksSkipped |
|
|
|
|
|
? t("4F day truck warn ack") |
|
|
|
|
|
: dayTruckWarn != null && !showMergeExtraOption |
|
|
|
|
|
? t("4F day truck warn release rest") |
|
|
|
|
|
: showMergeExtraOption |
|
|
|
|
|
? t("Confirm merge release") |
|
|
|
|
|
: t("Confirm"); |
|
|
|
|
|
|
|
|
const result = await Swal.fire({ |
|
|
const result = await Swal.fire({ |
|
|
icon: "question", |
|
|
|
|
|
title: t("Batch Release"), |
|
|
|
|
|
width: hasReplenishmentPreview ? BATCH_RELEASE_SWAL_WIDTH_WITH_REPLENISH : undefined, |
|
|
|
|
|
|
|
|
icon: dayTruckWarn != null ? "warning" : "question", |
|
|
|
|
|
title: dayTruckWarn?.title ?? t("Batch Release"), |
|
|
|
|
|
width: widenReleaseDialog ? BATCH_RELEASE_SWAL_WIDTH_WITH_REPLENISH : undefined, |
|
|
html: ` |
|
|
html: ` |
|
|
<div class="do-batch-release-swal-body" style="text-align:left;width:100%;box-sizing:border-box;"> |
|
|
<div class="do-batch-release-swal-body" style="text-align:left;width:100%;box-sizing:border-box;"> |
|
|
<p style="margin:0 0 4px;text-align:left;">${t("Selected Shop(s): ")}${idsToRelease.length}</p> |
|
|
|
|
|
|
|
|
${ |
|
|
|
|
|
dayTruckWarn == null |
|
|
|
|
|
? `<p style="margin:0 0 4px;text-align:left;">${t("Selected Shop(s): ")}${idsToRelease.length}</p> |
|
|
<p style="font-size:0.9em;color:#666;margin:0;text-align:left;"> |
|
|
<p style="font-size:0.9em;color:#666;margin:0;text-align:left;"> |
|
|
|
|
|
|
|
|
${currentSearchParams.code ? `${t("Code")}: ${currentSearchParams.code} ` : ""} |
|
|
${currentSearchParams.code ? `${t("Code")}: ${currentSearchParams.code} ` : ""} |
|
|
${currentSearchParams.shopName ? `${t("Shop Name")}: ${currentSearchParams.shopName} ` : ""} |
|
|
${currentSearchParams.shopName ? `${t("Shop Name")}: ${currentSearchParams.shopName} ` : ""} |
|
|
${currentSearchParams.estimatedArrivalDate ? `${t("Estimated Arrival")}: ${currentSearchParams.estimatedArrivalDate} ` : ""} |
|
|
${currentSearchParams.estimatedArrivalDate ? `${t("Estimated Arrival")}: ${currentSearchParams.estimatedArrivalDate} ` : ""} |
|
|
${status ? `${t("Status")}: ${t(status)} ` : ""} |
|
|
${status ? `${t("Status")}: ${t(status)} ` : ""} |
|
|
</p> |
|
|
|
|
|
|
|
|
</p>` |
|
|
|
|
|
: dayTruckWarn.html |
|
|
|
|
|
} |
|
|
${ |
|
|
${ |
|
|
showMergeExtraOption |
|
|
showMergeExtraOption |
|
|
? `<p style="font-size:0.95em;color:#666;margin-top:16px;">${t("Merge extra orders into lane batch ticket")}</p>` |
|
|
? `<p style="font-size:0.95em;color:#666;margin-top:16px;">${t("Merge extra orders into lane batch ticket")}</p>` |
|
|
@@ -647,21 +830,22 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
${replenishmentSectionHtml} |
|
|
${replenishmentSectionHtml} |
|
|
</div> |
|
|
</div> |
|
|
`, |
|
|
`, |
|
|
showCancelButton: true, |
|
|
|
|
|
showDenyButton: showMergeExtraOption, |
|
|
|
|
|
confirmButtonText: showMergeExtraOption ? t("Confirm merge release") : t("Confirm"), |
|
|
|
|
|
|
|
|
showCancelButton: !allDayTrucksSkipped, |
|
|
|
|
|
showDenyButton: showMergeExtraOption && !allDayTrucksSkipped, |
|
|
|
|
|
confirmButtonText: confirmText, |
|
|
denyButtonText: t("Confirm release without merge"), |
|
|
denyButtonText: t("Confirm release without merge"), |
|
|
cancelButtonText: t("Cancel"), |
|
|
cancelButtonText: t("Cancel"), |
|
|
confirmButtonColor: "#8dba00", |
|
|
confirmButtonColor: "#8dba00", |
|
|
denyButtonColor: "#6366f1", |
|
|
denyButtonColor: "#6366f1", |
|
|
cancelButtonColor: "#F04438", |
|
|
cancelButtonColor: "#F04438", |
|
|
didOpen: (popup) => { |
|
|
didOpen: (popup) => { |
|
|
applyBatchReleaseSwalLayout(popup, { wide: hasReplenishmentPreview }); |
|
|
|
|
|
|
|
|
applyBatchReleaseSwalLayout(popup, { wide: widenReleaseDialog }); |
|
|
}, |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
if (result.isDismissed) return; |
|
|
if (result.isDismissed) return; |
|
|
if (!result.isConfirmed && !result.isDenied) return; |
|
|
if (!result.isConfirmed && !result.isDenied) return; |
|
|
|
|
|
if (releaseIds.length === 0) return; |
|
|
|
|
|
|
|
|
const mergeExtraIntoLaneTicket = showMergeExtraOption ? result.isConfirmed : true; |
|
|
const mergeExtraIntoLaneTicket = showMergeExtraOption ? result.isConfirmed : true; |
|
|
|
|
|
|
|
|
@@ -669,13 +853,13 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
let startRes ; |
|
|
let startRes ; |
|
|
if(isWorkbench){ |
|
|
if(isWorkbench){ |
|
|
startRes = await startWorkbenchBatchReleaseAsyncV2({ |
|
|
startRes = await startWorkbenchBatchReleaseAsyncV2({ |
|
|
ids: idsToRelease, |
|
|
|
|
|
|
|
|
ids: releaseIds, |
|
|
userId: currentUserId, |
|
|
userId: currentUserId, |
|
|
mergeExtraIntoLaneTicket, |
|
|
mergeExtraIntoLaneTicket, |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
else{ |
|
|
else{ |
|
|
startRes = await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId }); |
|
|
|
|
|
|
|
|
startRes = await startBatchReleaseAsync({ ids: releaseIds, userId: currentUserId }); |
|
|
} |
|
|
} |
|
|
//await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 }); |
|
|
//await startBatchReleaseAsync({ ids: idsToRelease, userId: currentUserId ?? 1 }); |
|
|
const jobId = startRes?.entity?.jobId; |
|
|
const jobId = startRes?.entity?.jobId; |
|
|
@@ -760,6 +944,10 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
didOpen: (popup) => applyMainContentAreaSwalOffset(popup), |
|
|
didOpen: (popup) => applyMainContentAreaSwalOffset(popup), |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
} finally { |
|
|
|
|
|
batchReleaseLockRef.current = false; |
|
|
|
|
|
setIsBatchReleasing(false); |
|
|
|
|
|
} |
|
|
}, [t, currentUserId, currentSearchParams, handleSearch, resolveIdsForBatchRelease, activeTab, resolveTabFilter]); |
|
|
}, [t, currentUserId, currentSearchParams, handleSearch, resolveIdsForBatchRelease, activeTab, resolveTabFilter]); |
|
|
|
|
|
|
|
|
const handleTabChange = useCallback( |
|
|
const handleTabChange = useCallback( |
|
|
@@ -776,6 +964,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
setTotalCount(0); |
|
|
setTotalCount(0); |
|
|
setHasSearched(false); |
|
|
setHasSearched(false); |
|
|
setHasResults(false); |
|
|
setHasResults(false); |
|
|
|
|
|
setDateFieldError(false); |
|
|
}, |
|
|
}, |
|
|
[ |
|
|
[ |
|
|
activeTab, |
|
|
activeTab, |
|
|
@@ -813,6 +1002,7 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
<Button |
|
|
<Button |
|
|
name="batch_release" |
|
|
name="batch_release" |
|
|
variant="contained" |
|
|
variant="contained" |
|
|
|
|
|
disabled={isBatchReleasing} |
|
|
onClick={() => handleBatchRelease(true)} |
|
|
onClick={() => handleBatchRelease(true)} |
|
|
> |
|
|
> |
|
|
{t("Workbench Batch Release")} |
|
|
{t("Workbench Batch Release")} |
|
|
@@ -833,6 +1023,12 @@ const DoSearch: React.FC<Props> = ({ filterArgs, searchQuery, onDeliveryOrderSea |
|
|
onReset={onReset} |
|
|
onReset={onReset} |
|
|
/> |
|
|
/> |
|
|
|
|
|
|
|
|
|
|
|
{activeTab === "TRUCK_X" && hasSearched && totalCount > 0 && ( |
|
|
|
|
|
<Alert severity="warning"> |
|
|
|
|
|
{t("Truck X tab warn title")} |
|
|
|
|
|
</Alert> |
|
|
|
|
|
)} |
|
|
|
|
|
|
|
|
<Paper variant="outlined" sx={{ overflow: "hidden" }}> |
|
|
<Paper variant="outlined" sx={{ overflow: "hidden" }}> |
|
|
<StyledDataGrid |
|
|
<StyledDataGrid |
|
|
rows={searchAllDos} |
|
|
rows={searchAllDos} |
|
|
|