| @@ -36,7 +36,7 @@ const Page: React.FC<Props> = async ({ searchParams }) => { | |||
| </p> | |||
| <I18nProvider namespaces={["do", "common"]}> | |||
| <Suspense fallback={<DoDetail.Loading />}> | |||
| <DoDetail id={parseInt(id)} /> | |||
| <DoDetail id={parseInt(id)} workbenchRelease /> | |||
| </Suspense> | |||
| </I18nProvider> | |||
| </> | |||
| @@ -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<ReleasedDoPickOrderListItem[]> { | |||
| 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<ReleasedDoPickOrderListItem[]>(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<WorkbenchMessageResponse> { | |||
| const { doId, userId } = data; | |||
| return serverFetchJson<WorkbenchMessageResponse>( | |||
| `${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; | |||
| @@ -1677,7 +1677,8 @@ export const fetchReleasedDoPickOrdersForSelection = async ( | |||
| export const fetchReleasedDoPickOrdersForSelectionToday = async ( | |||
| shopName?: string, | |||
| storeId?: string, | |||
| truck?: string | |||
| truck?: string, | |||
| _requiredDeliveryDate?: string | |||
| ): Promise<ReleasedDoPickOrderListItem[]> => { | |||
| const params = new URLSearchParams(); | |||
| if (shopName?.trim()) params.append("shopName", shopName.trim()); | |||
| @@ -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); | |||
| @@ -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.")); | |||
| @@ -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<Props> & SubComponents = async ({ | |||
| id, | |||
| workbenchRelease = false, | |||
| }) => { | |||
| const doDetail = id ? await fetchDoDetail(id) : undefined | |||
| return <DoDetail id={id} defaultValues={doDetail}/> | |||
| return <DoDetail id={id} defaultValues={doDetail} workbenchRelease={workbenchRelease} /> | |||
| } | |||
| DoDetailWrapper.Loading = GeneralLoading; | |||
| @@ -47,6 +47,13 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ 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<Props> = ({ 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<Props> = ({ onPickOrderAssigned, onSwitc | |||
| </Grid> | |||
| )} | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Stack sx={{ minWidth: 60, pt: 1 }} spacing={0.25}> | |||
| <Typography sx={{ fontWeight: 600 }}>{t("Truck X")}</Typography> | |||
| {/* | |||
| <Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.2 }}> | |||
| {t("Required Date")}: {selectedDeliveryDateYmd} | |||
| </Typography> | |||
| */} | |||
| </Stack> | |||
| <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}> | |||
| {defaultTruckCount === 0 ? ( | |||
| renderNoEntry() | |||
| ) : ( | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => { | |||
| setSelectedStore(""); | |||
| setSelectedTruck("車線-X"); | |||
| setIsDefaultTruck(true); | |||
| setDefaultDateScope("today"); | |||
| setModalOpen(true); | |||
| }} | |||
| > | |||
| {/*{`${selectedDeliveryDateYmd} (${defaultTruckCount})`}*/} | |||
| {t("車線-X")}({defaultTruckCount}) | |||
| </Button> | |||
| )} | |||
| </Box> | |||
| </Stack> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ py: 2, mt: 1, mb: 0.5, borderTop: "1px solid #e0e0e0" }}> | |||
| <Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 0.5 }}> | |||
| @@ -431,39 +476,28 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSwitc | |||
| <Grid item xs={12}> | |||
| <Stack direction="row" spacing={2} alignItems="flex-start"> | |||
| <Typography sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{t("Truck X")}</Typography> | |||
| <Stack sx={{ minWidth: 60, pt: 1 }} spacing={0.25}> | |||
| <Typography sx={{ fontWeight: 600 }}>{t("Truck X")}</Typography> | |||
| <Typography variant="caption" color="text.secondary" sx={{ lineHeight: 1.2 }}> | |||
| {t("Before today")} | |||
| </Typography> | |||
| </Stack> | |||
| <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}> | |||
| {beforeTodayTruckXCount === 0 && defaultTruckCount === 0 ? renderNoEntry() : ( | |||
| <Stack direction="row" spacing={1}> | |||
| {defaultTruckCount > 0 && ( | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => { | |||
| setSelectedStore(""); | |||
| setSelectedTruck("車線-X"); | |||
| setIsDefaultTruck(true); | |||
| setDefaultDateScope("today"); | |||
| setModalOpen(true); | |||
| }} | |||
| > | |||
| {`${t("Today")} (${defaultTruckCount})`} | |||
| </Button> | |||
| )} | |||
| {beforeTodayTruckXCount > 0 && ( | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => { | |||
| setSelectedStore("4/F"); | |||
| setSelectedTruck("車線-X"); | |||
| setIsDefaultTruck(true); | |||
| setDefaultDateScope("before"); | |||
| setModalOpen(true); | |||
| }} | |||
| > | |||
| {`${t("車線-X")} (${beforeTodayTruckXCount})`} | |||
| </Button> | |||
| )} | |||
| </Stack> | |||
| {beforeTodayTruckXCount === 0 ? ( | |||
| renderNoEntry() | |||
| ) : ( | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => { | |||
| setSelectedStore("4/F"); | |||
| setSelectedTruck("車線-X"); | |||
| setIsDefaultTruck(true); | |||
| setDefaultDateScope("before"); | |||
| setModalOpen(true); | |||
| }} | |||
| > | |||
| {`${t("車線-X")} (${beforeTodayTruckXCount})`} | |||
| </Button> | |||
| )} | |||
| </Box> | |||
| </Stack> | |||
| @@ -475,6 +509,7 @@ const WorkbenchFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSwitc | |||
| truck={selectedTruck} | |||
| isDefaultTruck={isDefaultTruck} | |||
| defaultDateScope={defaultDateScope} | |||
| defaultTruckRequiredDeliveryDate={selectedDeliveryDateYmd} | |||
| listBridge={workbenchReleasedListBridge} | |||
| onClose={() => setModalOpen(false)} | |||
| onAssigned={() => { | |||
| @@ -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)) { | |||
| @@ -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 }; | |||
| @@ -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<ReleasedDoPickOrderListItem[]>; | |||
| /** 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<ReleasedDoPickOrderListItem[]>; | |||
| assignByListItemId: (userId: number, id: number) => Promise<PostPickOrderResponse>; | |||
| }; | |||
| @@ -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<Props> = ({ | |||
| @@ -66,6 +70,7 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| 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<Props> = ({ | |||
| 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<Props> = ({ | |||
| } 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<Props> = ({ | |||
| title: t("Confirm Assignment"), | |||
| html: ` | |||
| <div style="text-align: left;"> | |||
| <p><strong>${t("Date")}:</strong> ${item.requiredDeliveryDate ?? "-"}</p> | |||
| <p><strong>${t("Date")}:</strong> ${requiredDeliveryDateToDayString(item.requiredDeliveryDate) || "-"}</p> | |||
| <p><strong>${t("Shop")}:</strong> ${item.shopName ?? item.shopCode ?? "-"}</p> | |||
| <p><strong>${t("Truck")}:</strong> ${item.truckLanceCode ?? "-"}</p> | |||
| <p><strong>${t("Delivery Order")}:</strong> ${(item.deliveryOrderCodes ?? []).join(", ")}</p> | |||
| @@ -220,9 +230,7 @@ const ReleasedDoPickOrderSelectModal: React.FC<Props> = ({ | |||
| {list.map((row) => ( | |||
| <TableRow key={row.id} hover> | |||
| <TableCell> | |||
| {row.requiredDeliveryDate | |||
| ? dayjs(row.requiredDeliveryDate).format("YYYY-MM-DD") | |||
| : "-"} | |||
| {requiredDeliveryDateToDayString(row.requiredDeliveryDate) || "-"} | |||
| </TableCell> | |||
| <TableCell>{row.shopName ?? row.shopCode ?? "-"}</TableCell> | |||
| <TableCell>{row.truckLanceCode ?? "-"}</TableCell> | |||