|
- "use client";
-
- import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel } from "@mui/material";
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
- import { useTranslation } from "react-i18next";
- import { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
- import type { StoreLaneSummary, LaneRow, LaneBtn } from "@/app/api/pickOrder/actions";
- import {
- assignByDeliveryOrderPickOrderId,
- assignWorkbenchByLane,
- fetchWorkbenchReleasedDoPickOrdersForSelection,
- fetchWorkbenchReleasedDoPickOrdersForSelectionToday,
- fetchWorkbenchStoreLaneSummary,
- } from "@/app/api/doworkbench/actions";
- import Swal from "sweetalert2";
- import dayjs from "dayjs";
- import ReleasedDoPickOrderSelectModal from "@/components/FinishedGoodSearch/ReleasedDoPickOrderSelectModal";
-
- interface Props {
- onPickOrderAssigned?: () => void;
- onSwitchToDetailTab?: () => void;
- initialReleaseType?: string;
- }
-
- type LaneSlot4F = { truckDepartureTime: string; lane: LaneBtn };
- type TruckGroup4F = { truckLanceCode: string; slots: (LaneSlot4F & { sequenceIndex: number })[] };
-
- const WorkbenchFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSwitchToDetailTab, initialReleaseType = "batch" }) => {
- const { t } = useTranslation("pickOrder");
- const { data: session } = useSession() as { data: SessionWithTokens | null };
- const currentUserId = session?.id ? parseInt(session.id) : undefined;
- const [selectedStore, setSelectedStore] = useState<string>("2/F");
- const [selectedTruck, setSelectedTruck] = useState<string>("");
- const [modalOpen, setModalOpen] = useState(false);
- const [truckCounts2F, setTruckCounts2F] = useState<{ truck: string; count: number }[]>([]);
- const [truckCounts4F, setTruckCounts4F] = useState<{ truck: string; count: number }[]>([]);
- const [summary2F, setSummary2F] = useState<StoreLaneSummary | null>(null);
- const [summary4F, setSummary4F] = useState<StoreLaneSummary | null>(null);
- const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("today");
- const [isLoadingSummary, setIsLoadingSummary] = useState(false);
- const [isAssigning, setIsAssigning] = useState(false);
- const [isDefaultTruck, setIsDefaultTruck] = useState(false);
- const [beforeTodayTruckXCount, setBeforeTodayTruckXCount] = useState(0);
- const [selectedDate, setSelectedDate] = useState<string>("today");
- const [releaseType, setReleaseType] = useState<string>(initialReleaseType);
- const [ticketFloor, setTicketFloor] = useState<"2/F" | "4/F">("2/F");
- const defaultTruckCount = summary4F?.defaultTruckCount ?? 0;
-
- const hasLoggedRef = useRef(false);
- const fullReadyLoggedRef = useRef(false);
- const pendingRef = useRef(0);
-
- const workbenchReleasedListBridge = useMemo(
- () => ({
- loadBeforeToday: fetchWorkbenchReleasedDoPickOrdersForSelection,
- loadToday: fetchWorkbenchReleasedDoPickOrdersForSelectionToday,
- assignByListItemId: assignByDeliveryOrderPickOrderId,
- }),
- [],
- );
-
- const startFullTimer = () => {
- if (typeof window === "undefined") return;
- const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
- if (!(window as any)[key]) {
- (window as any)[key] = true;
- console.time("[FG] FloorLanePanel full ready");
- }
- };
- const tryEndFullTimer = () => {
- if (typeof window === "undefined") return;
- const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
- if ((window as any)[key] && !fullReadyLoggedRef.current && pendingRef.current === 0) {
- fullReadyLoggedRef.current = true;
- console.timeEnd("[FG] FloorLanePanel full ready");
- delete (window as any)[key];
- }
- };
-
- const loadSummaries = useCallback(async () => {
- setIsLoadingSummary(true);
- pendingRef.current += 1;
- startFullTimer();
- try {
- let dateParam: string | undefined;
- if (selectedDate === "today") dateParam = dayjs().format("YYYY-MM-DD");
- else if (selectedDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
- else if (selectedDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
- const [s2, s4] = await Promise.all([
- fetchWorkbenchStoreLaneSummary("2/F", dateParam, releaseType),
- fetchWorkbenchStoreLaneSummary("4/F", dateParam, releaseType),
- ]);
- setSummary2F(s2);
- setSummary4F(s4);
- } catch (error) {
- console.error("Error loading summaries:", error);
- } finally {
- setIsLoadingSummary(false);
- pendingRef.current -= 1;
- tryEndFullTimer();
- if (!hasLoggedRef.current) {
- hasLoggedRef.current = true;
- }
- }
- }, [selectedDate, releaseType]);
-
- useEffect(() => {
- void loadSummaries();
- }, [loadSummaries]);
-
- useEffect(() => {
- const loadCounts = async () => {
- pendingRef.current += 1;
- startFullTimer();
- try {
- const [list2F, list4F] = await Promise.all([
- fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "2/F"),
- fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, "4/F"),
- ]);
- const groupByTruck = (list: { truckLanceCode?: string | null }[]) => {
- const map: Record<string, number> = {};
- list.forEach((item) => {
- const lane = item.truckLanceCode || "-";
- map[lane] = (map[lane] || 0) + 1;
- });
- return Object.entries(map)
- .map(([truck, count]) => ({ truck, count }))
- .sort((a, b) => a.truck.localeCompare(b.truck));
- };
- setTruckCounts2F(groupByTruck(list2F));
- setTruckCounts4F(groupByTruck(list4F));
- } catch (e) {
- console.error("Error loading counts:", e);
- setTruckCounts2F([]);
- setTruckCounts4F([]);
- } finally {
- pendingRef.current -= 1;
- tryEndFullTimer();
- }
- };
- void loadCounts();
- }, [loadSummaries]);
-
- useEffect(() => {
- const loadBeforeTodayTruckX = async () => {
- pendingRef.current += 1;
- startFullTimer();
- try {
- const list = await fetchWorkbenchReleasedDoPickOrdersForSelection(undefined, undefined, "車線-X");
- setBeforeTodayTruckXCount(list.length);
- } catch {
- setBeforeTodayTruckXCount(0);
- } finally {
- pendingRef.current -= 1;
- tryEndFullTimer();
- }
- };
- void loadBeforeTodayTruckX();
- }, []);
-
- const handleAssignByLane = useCallback(
- async (storeId: string, truckDepartureTime: string, truckLanceCode: string, requiredDate: string) => {
- if (!currentUserId) return;
- let dateParam: string | undefined;
- if (requiredDate === "today") dateParam = dayjs().format("YYYY-MM-DD");
- else if (requiredDate === "tomorrow") dateParam = dayjs().add(1, "day").format("YYYY-MM-DD");
- else if (requiredDate === "dayAfterTomorrow") dateParam = dayjs().add(2, "day").format("YYYY-MM-DD");
- setIsAssigning(true);
- try {
- const res = await assignWorkbenchByLane({
- userId: currentUserId,
- storeId,
- truckLanceCode,
- truckDepartureTime,
- requiredDate: dateParam,
- });
- console.log("assignByLane result:", res);
- if (res.code === "SUCCESS") {
- window.dispatchEvent(new CustomEvent("pickOrderAssigned"));
- void loadSummaries();
- onPickOrderAssigned?.();
- onSwitchToDetailTab?.();
- }
- } catch {
- await Swal.fire({ icon: "error", title: t("Error"), text: t("Error occurred during assignment."), confirmButtonText: t("Confirm"), confirmButtonColor: "#8dba00" });
- } finally {
- setIsAssigning(false);
- }
- },
- [currentUserId, loadSummaries, onPickOrderAssigned, onSwitchToDetailTab, t],
- );
-
- const handleLaneButtonClick = useCallback(
- async (
- storeId: string,
- truckDepartureTime: string,
- truckLanceCode: string,
- loadingSequence: number | null | undefined,
- requiredDate: string,
- unassigned: number,
- total: number,
- ) => {
- let dateDisplay = requiredDate;
- if (requiredDate === "today") dateDisplay = dayjs().format("YYYY-MM-DD");
- else if (requiredDate === "tomorrow") dateDisplay = dayjs().add(1, "day").format("YYYY-MM-DD");
- else if (requiredDate === "dayAfterTomorrow") dateDisplay = dayjs().add(2, "day").format("YYYY-MM-DD");
- const result = await Swal.fire({
- title: t("Confirm Assignment"),
- html: `<div style="text-align: left; padding: 10px 0;">
- <p><strong>${t("Store")}:</strong> ${storeId}</p>
- <p><strong>${t("Lane Code")}:</strong> ${truckLanceCode}</p>
- ${loadingSequence != null ? `<p><strong>${t("Loading Sequence")}:</strong> ${loadingSequence}</p>` : ""}
- <p><strong>${t("Departure Time")}:</strong> ${truckDepartureTime}</p>
- <p><strong>${t("Required Date")}:</strong> ${dateDisplay}</p>
- <p><strong>${t("Available Orders")}:</strong> ${unassigned}/${total}</p>
- </div>`,
- icon: "question",
- showCancelButton: true,
- confirmButtonText: t("Confirm"),
- cancelButtonText: t("Cancel"),
- confirmButtonColor: "#8dba00",
- cancelButtonColor: "#F04438",
- });
- if (result.isConfirmed) {
- await handleAssignByLane(storeId, truckDepartureTime, truckLanceCode, requiredDate);
- }
- },
- [handleAssignByLane, t],
- );
-
- const getDateLabel = (offset: number) => dayjs().add(offset, "day").format("YYYY-MM-DD");
-
- const truckGroups4F = useMemo((): TruckGroup4F[] => {
- const rows = summary4F?.rows as LaneRow[] | undefined;
- if (!rows?.length) return [];
- const map = new Map<string, LaneSlot4F[]>();
- for (const row of rows) {
- for (const lane of row.lanes) {
- const list = map.get(lane.truckLanceCode);
- const slot: LaneSlot4F = { truckDepartureTime: row.truckDepartureTime, lane };
- if (list) list.push(slot);
- else map.set(lane.truckLanceCode, [slot]);
- }
- }
- return Array.from(map.entries()).map(([truckLanceCode, slots]) => ({
- truckLanceCode,
- slots: slots
- .slice()
- .sort((a, b) => (a.lane.loadingSequence ?? 999) - (b.lane.loadingSequence ?? 999))
- .map((s, i) => ({ ...s, sequenceIndex: i + 1 })),
- }));
- }, [summary4F?.rows]);
-
- const renderNoEntry = () => (
- <Typography variant="body2" color="text.secondary" sx={{ fontWeight: 600, fontSize: "1rem", textAlign: "center", py: 1 }}>
- {t("No entries available")}
- </Typography>
- );
-
- return (
- <Box sx={{ mb: 2 }}>
- <Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: "flex-start" }}>
- <Box sx={{ maxWidth: 300 }}>
- <FormControl fullWidth size="small">
- <InputLabel id="date-select-label">{t("Select Date")}</InputLabel>
- <Select labelId="date-select-label" value={selectedDate} label={t("Select Date")} onChange={(e) => setSelectedDate(e.target.value)}>
- <MenuItem value="today">{t("Today")} ({getDateLabel(0)})</MenuItem>
- <MenuItem value="tomorrow">{t("Tomorrow")} ({getDateLabel(1)})</MenuItem>
- <MenuItem value="dayAfterTomorrow">{t("Day After Tomorrow")} ({getDateLabel(2)})</MenuItem>
- </Select>
- </FormControl>
- </Box>
- <Box sx={{ minWidth: 140, maxWidth: 300 }}>
- <FormControl fullWidth size="small">
- <InputLabel id="release-type-select-label">{t("Release Type")}</InputLabel>
- <Select labelId="release-type-select-label" value={releaseType} label={t("Release Type")} onChange={(e) => setReleaseType(e.target.value)}>
- <MenuItem value="batch">{t("Batch")}</MenuItem>
- <MenuItem value="single">{t("Single")}</MenuItem>
- </Select>
- </FormControl>
- </Box>
- <Box sx={{ minWidth: 120, maxWidth: 200 }}>
- <FormControl fullWidth size="small">
- <InputLabel id="ticket-floor-select-label">{t("Floor ticket")}</InputLabel>
- <Select labelId="ticket-floor-select-label" value={ticketFloor} label={t("Floor ticket")} onChange={(e) => setTicketFloor(e.target.value as "2/F" | "4/F")}>
- <MenuItem value="2/F">{t("2F ticket")}</MenuItem>
- <MenuItem value="4/F">{t("4F ticket")}</MenuItem>
- </Select>
- </FormControl>
- </Box>
- </Stack>
-
- <Grid container spacing={2}>
- {ticketFloor === "2/F" && (
- <Grid item xs={12}>
- <Stack direction="row" spacing={2} alignItems="flex-start">
- <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography>
- <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
- {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !summary2F?.rows?.length ? renderNoEntry() : (
- <Grid container spacing={1}>
- {summary2F.rows.map((row) => (
- <Grid item xs={12} key={row.truckDepartureTime}>
- <Stack direction={{ xs: "column", sm: "row" }} spacing={1} alignItems={{ xs: "stretch", sm: "center" }} sx={{ border: "1px solid #e0e0e0", borderRadius: 0.5, p: 1, backgroundColor: "#fff" }}>
- <Typography variant="body2" sx={{ fontWeight: 600, minWidth: { sm: 60 } }}>{row.truckDepartureTime}</Typography>
- <Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}>
- {row.lanes.map((lane) => (
- <Button key={`${row.truckDepartureTime}-${lane.truckLanceCode}`} variant="outlined" disabled={lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick("2/F", row.truckDepartureTime, lane.truckLanceCode, null, selectedDate, lane.unassigned, lane.total)}>
- {`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
- </Button>
- ))}
- </Stack>
- </Stack>
- </Grid>
- ))}
- </Grid>
- )}
- </Box>
- </Stack>
- </Grid>
- )}
-
- {ticketFloor === "4/F" && (
- <Grid item xs={12}>
- <Stack direction="row" spacing={2} alignItems="flex-start">
- <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography>
- <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
- {isLoadingSummary ? <Typography variant="caption">{t("Loading...")}</Typography> : !truckGroups4F.length ? renderNoEntry() : (
- <Grid container spacing={1}>
- {truckGroups4F.map(({ truckLanceCode, slots }) => (
- <Grid item xs={12} key={truckLanceCode}>
- <Stack direction={{ xs: "column", sm: "row" }} spacing={1} alignItems={{ xs: "stretch", sm: "center" }} sx={{ border: "1px solid #e0e0e0", borderRadius: 0.5, p: 1, backgroundColor: "#fff" }}>
- <Typography variant="body2" sx={{ fontWeight: 700, minWidth: { sm: 160 } }}>{truckLanceCode}</Typography>
- <Stack direction="row" flexWrap="wrap" sx={{ gap: 1 }}>
- {slots.map((slot) => {
- const handlerName = (slot.lane.handlerName ?? "").trim();
- return (
- <Button key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.truckDepartureTime}`} variant="outlined" disabled={slot.lane.unassigned === 0 || isAssigning} onClick={() => void handleLaneButtonClick("4/F", slot.truckDepartureTime, slot.lane.truckLanceCode, slot.lane.loadingSequence ?? null, selectedDate, slot.lane.unassigned, slot.lane.total)}>
- {`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})${handlerName ? ` ${handlerName}` : ""}`}
- </Button>
- );
- })}
- </Stack>
- </Stack>
- </Grid>
- ))}
- </Grid>
- )}
- </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 }}>
- {t("Not yet finished released do pick orders")}
- </Typography>
- <Typography variant="body2" color="text.secondary">
- {t("Released orders not yet completed - click lane to select and assign")}
- </Typography>
- </Box>
- </Grid>
-
- {ticketFloor === "2/F" && (
- <Grid item xs={12}>
- <Stack direction="row" spacing={2} alignItems="flex-start">
- <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>2/F</Typography>
- <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
- {truckCounts2F.length === 0 ? renderNoEntry() : (
- <Grid container spacing={1}>
- {truckCounts2F.map(({ truck, count }) => (
- <Grid item xs={6} sm={4} md={3} key={`2F-${truck}`} sx={{ display: "flex" }}>
- <Button
- variant="outlined"
- onClick={() => {
- setIsDefaultTruck(false);
- setSelectedStore("2/F");
- setSelectedTruck(truck);
- setModalOpen(true);
- }}
- sx={{ flex: 1 }}
- >
- {`${truck} (${count})`}
- </Button>
- </Grid>
- ))}
- </Grid>
- )}
- </Box>
- </Stack>
- </Grid>
- )}
-
- {ticketFloor === "4/F" && (
- <Grid item xs={12}>
- <Stack direction="row" spacing={2} alignItems="flex-start">
- <Typography variant="h6" sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>4/F</Typography>
- <Box sx={{ border: "1px solid #e0e0e0", borderRadius: 1, p: 1, backgroundColor: "#fafafa", flex: 1 }}>
- {truckCounts4F.length === 0 ? renderNoEntry() : (
- <Grid container spacing={1}>
- {truckCounts4F.map(({ truck, count }) => (
- <Grid item xs={6} sm={4} md={3} key={`4F-${truck}`} sx={{ display: "flex" }}>
- <Button
- variant="outlined"
- onClick={() => {
- setIsDefaultTruck(false);
- setSelectedStore("4/F");
- setSelectedTruck(truck);
- setModalOpen(true);
- }}
- sx={{ flex: 1 }}
- >
- {`${truck} (${count})`}
- </Button>
- </Grid>
- ))}
- </Grid>
- )}
- </Box>
- </Stack>
- </Grid>
- )}
-
- <Grid item xs={12}>
- <Stack direction="row" spacing={2} alignItems="flex-start">
- <Typography sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{t("Truck X")}</Typography>
- <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>
- )}
- </Box>
- </Stack>
- </Grid>
-
- <ReleasedDoPickOrderSelectModal
- open={modalOpen}
- storeId={selectedStore}
- truck={selectedTruck}
- isDefaultTruck={isDefaultTruck}
- defaultDateScope={defaultDateScope}
- listBridge={workbenchReleasedListBridge}
- onClose={() => setModalOpen(false)}
- onAssigned={() => {
- void loadSummaries();
- onPickOrderAssigned?.();
- onSwitchToDetailTab?.();
- }}
- />
- </Grid>
- </Box>
- );
- };
-
- export default WorkbenchFloorLanePanel;
|