diff --git a/src/app/(main)/doworkbench/edit/page.tsx b/src/app/(main)/doworkbench/edit/page.tsx index 1341280..308d185 100644 --- a/src/app/(main)/doworkbench/edit/page.tsx +++ b/src/app/(main)/doworkbench/edit/page.tsx @@ -36,7 +36,7 @@ const Page: React.FC = async ({ searchParams }) => {

}> - + diff --git a/src/app/api/doworkbench/actions.ts b/src/app/api/doworkbench/actions.ts index 707208f..7f40677 100644 --- a/src/app/api/doworkbench/actions.ts +++ b/src/app/api/doworkbench/actions.ts @@ -231,19 +231,23 @@ export async function fetchWorkbenchReleasedDoPickOrdersForSelection( return response ?? []; } +/** When `requiredDeliveryDate` is set (YYYY-MM-DD), filters `delivery_order_pick_order.requiredDeliveryDate`; otherwise calendar today. */ export async function fetchWorkbenchReleasedDoPickOrdersForSelectionToday( shopName?: string, storeId?: string, - truck?: string + truck?: string, + requiredDeliveryDate?: string ): Promise { const params = new URLSearchParams(); if (shopName?.trim()) params.append("shopName", shopName.trim()); if (storeId?.trim()) params.append("storeId", storeId.trim()); if (truck?.trim()) params.append("truck", truck.trim()); + if (requiredDeliveryDate?.trim()) params.append("requiredDate", requiredDeliveryDate.trim()); const query = params.toString(); const url = `${BASE_API_URL}/doPickOrder/workbench/released-today${query ? `?${query}` : ""}`; const response = await serverFetchJson(url, { method: "GET" }); - return response ?? []; + if (response == null) return []; + return Array.isArray(response) ? response : []; } /** Same body as `/doPickOrder/assign-by-lane` but resolves `delivery_order_pick_order`. */ @@ -356,7 +360,21 @@ export async function fetchWorkbenchAvailableLotsByItem(itemId: number) { }, ); } - +/** Single DO; JSON body is one number (same as legacy `batch-release/async-single`). */ +export async function startWorkbenchBatchReleaseAsyncSingleV2(data: { + doId: number; + userId: number; +}): Promise { + const { doId, userId } = data; + return serverFetchJson( + `${BASE_API_URL}/doPickOrder/workbench/batch-release/async-single-v2?userId=${userId}`, + { + method: "POST", + body: JSON.stringify(doId), + headers: { "Content-Type": "application/json" }, + } + ); +} export async function printWorkbenchLotLabel(params: { inventoryLotLineId: number; printerId: number; diff --git a/src/app/api/pickOrder/actions.ts b/src/app/api/pickOrder/actions.ts index 01ebdcc..c2bce38 100644 --- a/src/app/api/pickOrder/actions.ts +++ b/src/app/api/pickOrder/actions.ts @@ -1677,7 +1677,8 @@ export const fetchReleasedDoPickOrdersForSelection = async ( export const fetchReleasedDoPickOrdersForSelectionToday = async ( shopName?: string, storeId?: string, - truck?: string + truck?: string, + _requiredDeliveryDate?: string ): Promise => { const params = new URLSearchParams(); if (shopName?.trim()) params.append("shopName", shopName.trim()); diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index d3ca843..2a1839e 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -68,6 +68,18 @@ export const arrayToDayjs = (arr: ConfigType | (number | undefined)[], showTime: return dayjs(tempArr as ConfigType); }; +/** + * Backend `LocalDate` is often JSON `[year, month, day]` with month 1–12. + * Do not pass that array to `dayjs(arr)` — it uses JS month index 0–11 and shifts the calendar month. + */ +export function requiredDeliveryDateToDayString(value: unknown): string { + if (value == null) return ""; + if (Array.isArray(value) && value.length >= 3 && value.every((x) => typeof x === "number")) { + return arrayToDayjs(value as number[]).format(OUTPUT_DATE_FORMAT); + } + return dayjs(value as string | number | Date).format(OUTPUT_DATE_FORMAT); +} + export const arrayToDateString = (arr: ConfigType | (number | undefined)[], format: "input"|"output" = "output") => { if (format == "output") { return arrayToDayjs(arr).format(OUTPUT_DATE_FORMAT); diff --git a/src/components/DoDetail/DoDetail.tsx b/src/components/DoDetail/DoDetail.tsx index da1d85b..7eec514 100644 --- a/src/components/DoDetail/DoDetail.tsx +++ b/src/components/DoDetail/DoDetail.tsx @@ -9,7 +9,12 @@ import { useCallback, useState } from "react"; import { Button, Stack, Typography, Box, Alert } from "@mui/material"; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import StartIcon from "@mui/icons-material/Start"; -import { releaseDo,startBatchReleaseAsyncSingle, assignPickOrderByStore, releaseAssignedPickOrderByStore } from "@/app/api/do/actions"; +import { releaseDo, startBatchReleaseAsyncSingle, assignPickOrderByStore, releaseAssignedPickOrderByStore } from "@/app/api/do/actions"; +import { + getWorkbenchBatchReleaseProgress, + startWorkbenchBatchReleaseAsyncSingleV2, +} from "@/app/api/doworkbench/actions"; +import Swal from "sweetalert2"; import DoInfoCard from "./DoInfoCard"; import DoLineTable from "./DoLineTable"; import { useSession } from "next-auth/react"; @@ -63,14 +68,20 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId); //userId: currentUserId // Pass user ID from session }) */ - const response = await startBatchReleaseAsyncSingle({ + const response = await startWorkbenchBatchReleaseAsyncSingleV2({ doId: id, userId: currentUserId ?? 0 }) - if (response) { - formProps.setValue("status", response.entity.status) - setSuccessMessage(t("DO released successfully! Pick orders created.")) - } + if (response?.code === "STARTED") { + setSuccessMessage(t("DO released successfully! Pick orders created.")); + router.refresh(); + } else if (response) { + setServerError( + response.message ?? + response.code ?? + t("An error has occurred. Please try again later."), + ); + } } } catch (e) { setServerError(t("An error has occurred. Please try again later.")); diff --git a/src/components/DoDetail/DoDetailWrapper.tsx b/src/components/DoDetail/DoDetailWrapper.tsx index 0f0f6c7..ab1ec2f 100644 --- a/src/components/DoDetail/DoDetailWrapper.tsx +++ b/src/components/DoDetail/DoDetailWrapper.tsx @@ -9,16 +9,19 @@ interface SubComponents { type DoDetailProps = { id?: number; + /** When true (e.g. `/doworkbench/edit`), Release uses workbench `delivery_order_pick_order` async V2 + job progress. */ + workbenchRelease?: boolean; } type Props = DoDetailProps const DoDetailWrapper: React.FC & SubComponents = async ({ id, + workbenchRelease = false, }) => { const doDetail = id ? await fetchDoDetail(id) : undefined - return + return } DoDetailWrapper.Loading = GeneralLoading; diff --git a/src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx b/src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx index 599764b..caa29d5 100644 --- a/src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx +++ b/src/components/DoWorkbench/WorkbenchFloorLanePanel.tsx @@ -47,6 +47,13 @@ const WorkbenchFloorLanePanel: React.FC = ({ onPickOrderAssigned, onSwitc const [ticketFloor, setTicketFloor] = useState<"2/F" | "4/F">("2/F"); const defaultTruckCount = summary4F?.defaultTruckCount ?? 0; + const selectedDeliveryDateYmd = useMemo(() => { + if (selectedDate === "today") return dayjs().format("YYYY-MM-DD"); + if (selectedDate === "tomorrow") return dayjs().add(1, "day").format("YYYY-MM-DD"); + if (selectedDate === "dayAfterTomorrow") return dayjs().add(2, "day").format("YYYY-MM-DD"); + return dayjs().format("YYYY-MM-DD"); + }, [selectedDate]); + const hasLoggedRef = useRef(false); const fullReadyLoggedRef = useRef(false); const pendingRef = useRef(0); @@ -54,7 +61,13 @@ const WorkbenchFloorLanePanel: React.FC = ({ onPickOrderAssigned, onSwitc const workbenchReleasedListBridge = useMemo( () => ({ loadBeforeToday: fetchWorkbenchReleasedDoPickOrdersForSelection, - loadToday: fetchWorkbenchReleasedDoPickOrdersForSelectionToday, + loadToday: ( + shopName?: string, + storeId?: string, + truck?: string, + requiredDeliveryDate?: string + ) => + fetchWorkbenchReleasedDoPickOrdersForSelectionToday(shopName, storeId, truck, requiredDeliveryDate), assignByListItemId: assignByDeliveryOrderPickOrderId, }), [], @@ -358,6 +371,38 @@ const WorkbenchFloorLanePanel: React.FC = ({ onPickOrderAssigned, onSwitc )} + + + + {t("Truck X")} + {/* + + {t("Required Date")}: {selectedDeliveryDateYmd} + + */} + + + {defaultTruckCount === 0 ? ( + renderNoEntry() + ) : ( + + )} + + + + @@ -431,39 +476,28 @@ const WorkbenchFloorLanePanel: React.FC = ({ onPickOrderAssigned, onSwitc - {t("Truck X")} + + {t("Truck X")} + + {t("Before today")} + + - {beforeTodayTruckXCount === 0 && defaultTruckCount === 0 ? renderNoEntry() : ( - - {defaultTruckCount > 0 && ( - - )} - {beforeTodayTruckXCount > 0 && ( - - )} - + {beforeTodayTruckXCount === 0 ? ( + renderNoEntry() + ) : ( + )} @@ -475,6 +509,7 @@ const WorkbenchFloorLanePanel: React.FC = ({ onPickOrderAssigned, onSwitc truck={selectedTruck} isDefaultTruck={isDefaultTruck} defaultDateScope={defaultDateScope} + defaultTruckRequiredDeliveryDate={selectedDeliveryDateYmd} listBridge={workbenchReleasedListBridge} onClose={() => setModalOpen(false)} onAssigned={() => { diff --git a/src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx b/src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx index 46625b7..c663d3d 100644 --- a/src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx +++ b/src/components/DoWorkbench/WorkbenchTicketReleaseTable.tsx @@ -30,7 +30,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import dayjs, { Dayjs } from "dayjs"; -import { arrayToDayjs } from "@/app/utils/formatUtil"; +import { arrayToDayjs, requiredDeliveryDateToDayString } from "@/app/utils/formatUtil"; import { fetchWorkbenchTicketReleaseTable, forceCompleteWorkbenchTicket, @@ -41,14 +41,6 @@ import Swal from "sweetalert2"; import { AUTH } from "@/authorities"; import { SessionWithTokens } from "@/config/authConfig"; -function requiredDeliveryDateToDayString(value: unknown): string { - if (value == null) return ""; - if (Array.isArray(value) && value.length >= 3 && value.every((x) => typeof x === "number")) { - return arrayToDayjs(value as number[]).format("YYYY-MM-DD"); - } - return dayjs(value as string | number | Date).format("YYYY-MM-DD"); -} - function formatTicketDateTime(value: unknown): string { if (!value) return "-"; if (Array.isArray(value)) { diff --git a/src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx b/src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx index 4e748f8..957175f 100644 --- a/src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx +++ b/src/components/FinishedGoodSearch/FGPickOrderTicketReleaseTable.tsx @@ -35,7 +35,7 @@ import { import { useTranslation } from "react-i18next"; import { useSession } from "next-auth/react"; import dayjs, { Dayjs } from "dayjs"; -import { arrayToDayjs } from "@/app/utils/formatUtil"; +import { arrayToDayjs, requiredDeliveryDateToDayString } from "@/app/utils/formatUtil"; import { fetchTicketReleaseTable, getTicketReleaseTable } from "@/app/api/do/actions"; import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; @@ -69,18 +69,6 @@ function shouldLogTicketFilterDebug(): boolean { return (window as unknown as { __FG_TICKET_FILTER_DEBUG__?: boolean }).__FG_TICKET_FILTER_DEBUG__ === true; } -/** - * 後端 LocalDate 常序列化為 [year, month, day](month 為 1–12)。 - * 不可使用 dayjs(array):會被當成 [年, 月索引 0–11, 日],導致月份錯一個月、篩選與畫面日期錯誤。 - */ -function requiredDeliveryDateToDayString(value: unknown): string { - if (value == null) return ""; - if (Array.isArray(value) && value.length >= 3 && value.every((x) => typeof x === "number")) { - return arrayToDayjs(value as number[]).format("YYYY-MM-DD"); - } - return dayjs(value as string | number | Date).format("YYYY-MM-DD"); -} - const FGPickOrderTicketReleaseTable: React.FC = () => { const { t } = useTranslation("ticketReleaseTable"); const { data: session } = useSession() as { data: SessionWithTokens | null }; diff --git a/src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx b/src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx index d08b6a7..aaf8ec4 100644 --- a/src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx +++ b/src/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal.tsx @@ -28,7 +28,7 @@ import { import { useSession } from "next-auth/react"; import { SessionWithTokens } from "@/config/authConfig"; import Swal from "sweetalert2"; -import dayjs from "dayjs"; +import { requiredDeliveryDateToDayString } from "@/app/utils/formatUtil"; /** DO workbench 使用 workbench released API;Finished Good 不傳即可 */ export type ReleasedDoPickListBridge = { @@ -37,10 +37,12 @@ export type ReleasedDoPickListBridge = { storeId?: string, truck?: string ) => Promise; + /** Optional 4th arg: workbench `requiredDeliveryDate` (YYYY-MM-DD) for default truck list; omit = calendar today. */ loadToday: ( shopName?: string, storeId?: string, - truck?: string + truck?: string, + requiredDeliveryDate?: string ) => Promise; assignByListItemId: (userId: number, id: number) => Promise; }; @@ -55,6 +57,8 @@ interface Props { /** Truck X only: today → released-today; before → released (歷史未完工) */ defaultDateScope?: "today" | "before"; listBridge?: ReleasedDoPickListBridge; + /** Workbench: `delivery_order_pick_order.requiredDeliveryDate` for Truck X (select day); used when [defaultDateScope] is today. */ + defaultTruckRequiredDeliveryDate?: string; } const ReleasedDoPickOrderSelectModal: React.FC = ({ @@ -66,6 +70,7 @@ const ReleasedDoPickOrderSelectModal: React.FC = ({ isDefaultTruck, defaultDateScope: defaultDateScopeProp = "today", listBridge, + defaultTruckRequiredDeliveryDate, }) => { const { t } = useTranslation("pickOrder"); const { data: session } = useSession() as { data: SessionWithTokens | null }; @@ -85,7 +90,12 @@ const ReleasedDoPickOrderSelectModal: React.FC = ({ if (isDefaultTruck) { if (defaultDateScopeProp === "today") { - data = await loadTodayFn(undefined, undefined, "車線-X"); + data = await loadTodayFn( + undefined, + undefined, + "車線-X", + defaultTruckRequiredDeliveryDate?.trim() || undefined + ); } else { data = await loadReleased(undefined, undefined, "車線-X"); } @@ -104,7 +114,7 @@ const ReleasedDoPickOrderSelectModal: React.FC = ({ } finally { setLoading(false); } - }, [open, shopSearch, storeId, truck, isDefaultTruck, defaultDateScopeProp, listBridge]); + }, [open, shopSearch, storeId, truck, isDefaultTruck, defaultDateScopeProp, listBridge, defaultTruckRequiredDeliveryDate]); useEffect(() => { loadList(); @@ -117,7 +127,7 @@ const ReleasedDoPickOrderSelectModal: React.FC = ({ title: t("Confirm Assignment"), html: `
-

${t("Date")}: ${item.requiredDeliveryDate ?? "-"}

+

${t("Date")}: ${requiredDeliveryDateToDayString(item.requiredDeliveryDate) || "-"}

${t("Shop")}: ${item.shopName ?? item.shopCode ?? "-"}

${t("Truck")}: ${item.truckLanceCode ?? "-"}

${t("Delivery Order")}: ${(item.deliveryOrderCodes ?? []).join(", ")}

@@ -220,9 +230,7 @@ const ReleasedDoPickOrderSelectModal: React.FC = ({ {list.map((row) => ( - {row.requiredDeliveryDate - ? dayjs(row.requiredDeliveryDate).format("YYYY-MM-DD") - : "-"} + {requiredDeliveryDateToDayString(row.requiredDeliveryDate) || "-"} {row.shopName ?? row.shopCode ?? "-"} {row.truckLanceCode ?? "-"}