Browse Source

update do ticket ui

MergeProblem1
CANCERYS\kw093 1 day ago
parent
commit
e1cd48df21
2 changed files with 169 additions and 91 deletions
  1. +3
    -0
      src/app/api/pickOrder/actions.ts
  2. +166
    -91
      src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx

+ 3
- 0
src/app/api/pickOrder/actions.ts View File

@@ -457,6 +457,7 @@ export interface LaneRow {

export interface LaneBtn {
truckLanceCode: string;
loadingSequence?: number | null;
unassigned: number;
total: number;
}
@@ -636,6 +637,7 @@ export async function assignByLane(
storeId: string,
truckLanceCode: string,
truckDepartureTime?: string,
loadingSequence?: number | null,
requiredDate?: string
): Promise<any> {
const response = await serverFetchJson(
@@ -650,6 +652,7 @@ export async function assignByLane(
storeId,
truckLanceCode,
truckDepartureTime,
loadingSequence,
requiredDate,
}),
}


+ 166
- 91
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx View File

@@ -1,11 +1,11 @@
"use client";

import { Box, Button, Grid, Stack, Typography, Select, MenuItem, FormControl, InputLabel ,Tooltip} from "@mui/material";
import { useCallback, useEffect, useState, useRef } from "react";
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import { fetchStoreLaneSummary,fetchReleasedDoPickOrdersForSelection,fetchReleasedDoPickOrderCountByStore, assignByLane, type StoreLaneSummary } from "@/app/api/pickOrder/actions";
import { fetchStoreLaneSummary,fetchReleasedDoPickOrdersForSelection,fetchReleasedDoPickOrderCountByStore, assignByLane, type StoreLaneSummary, type LaneRow, type LaneBtn } from "@/app/api/pickOrder/actions";
import Swal from "sweetalert2";
import dayjs from "dayjs";
import ReleasedDoPickOrderSelectModal from "./ReleasedDoPickOrderSelectModal";
@@ -15,6 +15,9 @@ interface Props {
onSwitchToDetailTab?: () => void;
}

type LaneSlot4F = { truckDepartureTime: string; lane: LaneBtn };
type TruckGroup4F = { truckLanceCode: string; slots: (LaneSlot4F & { sequenceIndex: number })[] };

const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSwitchToDetailTab }) => {
const { t } = useTranslation("pickOrder");
const { data: session } = useSession() as { data: SessionWithTokens | null };
@@ -41,6 +44,7 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to

const [selectedDate, setSelectedDate] = useState<string>("today");
const [releaseType, setReleaseType] = useState<string>("batch");
const [ticketFloor, setTicketFloor] = useState<"2/F" | "4/F">("2/F");
const startFullTimer = () => {
if (typeof window === "undefined") return;
const key = "__FG_FLOOR_FULL_TIMER_STARTED__" as const;
@@ -166,6 +170,7 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to
storeId: string,
truckDepartureTime: string,
truckLanceCode: string,
loadingSequence: number | null | undefined,
requiredDate: string

) => {
@@ -183,7 +188,7 @@ const [defaultDateScope, setDefaultDateScope] = useState<"today" | "before">("to
}
setIsAssigning(true);
try {
const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, dateParam);
const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, loadingSequence ?? null, dateParam);
if (res.code === "SUCCESS") {
console.log(" Successfully assigned pick order from lane", truckLanceCode);
@@ -229,6 +234,7 @@ const handleLaneButtonClick = useCallback(async (
storeId: string,
truckDepartureTime: string,
truckLanceCode: string,
loadingSequence: number | null | undefined,
requiredDate: string,
unassigned: number,
total: number
@@ -252,6 +258,7 @@ const handleLaneButtonClick = useCallback(async (
<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>
@@ -268,7 +275,7 @@ const handleLaneButtonClick = useCallback(async (

// Only proceed if user confirmed
if (result.isConfirmed) {
await handleAssignByLane(storeId, truckDepartureTime, truckLanceCode, requiredDate);
await handleAssignByLane(storeId, truckDepartureTime, truckLanceCode, loadingSequence, requiredDate);
}
}, [handleAssignByLane, t]);

@@ -290,6 +297,31 @@ const getDateLabel = (offset: number) => {
return flattened;
};

/** 4/F:依車線匯總,同車多筆依 API 出現順序為裝載序(出發時間相同時仍可分序)。 */
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 code = lane.truckLanceCode;
const list = map.get(code);
const slot: LaneSlot4F = { truckDepartureTime: row.truckDepartureTime, lane };
if (list) list.push(slot);
else map.set(code, [slot]);
}
}
return Array.from(map.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([truckLanceCode, slots]) => ({
truckLanceCode,
slots: slots
.slice()
.sort((a, b) => (a.lane.loadingSequence ?? 999) - (b.lane.loadingSequence ?? 999))
.map((s: LaneSlot4F, i: number) => ({ ...s, sequenceIndex: i + 1 })),
}));
}, [summary4F?.rows]);

return (
<Box sx={{ mb: 2 }}>
{/* Date Selector Dropdown and Legend */}
@@ -348,6 +380,21 @@ const getDateLabel = (offset: number) => {
</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"
id="ticket-floor-select"
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>
<Box
sx={{
p: 1,
@@ -362,19 +409,12 @@ const getDateLabel = (offset: number) => {
{t("EDT - Lane Code (Unassigned/Total)")}
</Typography>
</Box>
</Stack>
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}>

</Stack>


{/* Grid containing both floors */}
<Grid container spacing={2}>
{/* 2/F 楼层面板 */}
{ticketFloor === "2/F" && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
{/* Floor Label */}
@@ -416,55 +456,71 @@ const getDateLabel = (offset: number) => {
</Typography>
) : (
<Grid container spacing={1}>
{flattenRows(summary2F.rows).map((item, idx) => (
<Grid item xs={12} sm={6} md={3} key={idx}>
{summary2F.rows.map((row) => (
<Grid item xs={12} key={row.truckDepartureTime}>
<Stack
direction="row"
direction={{ xs: "column", sm: "row" }}
spacing={1}
alignItems="center"
alignItems={{ xs: "stretch", sm: "center" }}
sx={{
border: '1px solid #e0e0e0',
border: "1px solid #e0e0e0",
borderRadius: 0.5,
p: 1,
backgroundColor: '#fff',
height: '100%'
backgroundColor: "#fff",
}}
>
{/* Time on the left */}
<Typography
<Typography
variant="body2"
sx={{
sx={{
fontWeight: 600,
fontSize: '1rem',
minWidth: 50,
whiteSpace: 'nowrap'
fontSize: "1rem",
minWidth: { sm: 60 },
whiteSpace: "nowrap",
pt: { xs: 0, sm: 0.5 },
}}
>
{item.truckDepartureTime}
{row.truckDepartureTime}
</Typography>
{/* Single Button on the right */}
<Button
variant="outlined"
size="medium"
disabled={item.lane.unassigned === 0 || isAssigning}
onClick={() => handleLaneButtonClick("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate, item.lane.unassigned, item.lane.total)}
sx={{
flex: 1,
fontSize: '1.1rem',
py: 1,
px: 1.5,
borderWidth: 1,
borderColor: '#ccc',
fontWeight: 500,
'&:hover': {
borderColor: '#999',
backgroundColor: '#f5f5f5'
}
}}

<Stack
direction="row"
flexWrap="wrap"
sx={{ flex: 1, gap: 1 }}
>
{`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`}
</Button>
{row.lanes.map((lane) => (
<Button
key={`${row.truckDepartureTime}-${lane.truckLanceCode}`}
variant="outlined"
size="medium"
disabled={lane.unassigned === 0 || isAssigning}
onClick={() =>
handleLaneButtonClick(
"2/F",
row.truckDepartureTime,
lane.truckLanceCode,
null,
selectedDate,
lane.unassigned,
lane.total
)
}
sx={{
fontSize: "1.1rem",
py: 1,
px: 1.5,
borderWidth: 1,
borderColor: "#ccc",
fontWeight: 500,
"&:hover": {
borderColor: "#999",
backgroundColor: "#f5f5f5",
},
}}
>
{`${lane.truckLanceCode} (${lane.unassigned}/${lane.total})`}
</Button>
))}
</Stack>
</Stack>
</Grid>
))}
@@ -473,8 +529,8 @@ const getDateLabel = (offset: number) => {
</Box>
</Stack>
</Grid>
{/* 4/F 楼层面板 */}
)}
{ticketFloor === "4/F" && (
<Grid item xs={12}>
<Stack direction="row" spacing={2} alignItems="flex-start">
{/* Floor Label */}
@@ -501,7 +557,7 @@ const getDateLabel = (offset: number) => {
>
{isLoadingSummary ? (
<Typography variant="caption">{t("Loading...")}</Typography>
) : !summary4F?.rows || summary4F.rows.length === 0 ? (
) : truckGroups4F.length === 0 ? (
<Typography
variant="body2"
color="text.secondary"
@@ -516,55 +572,69 @@ const getDateLabel = (offset: number) => {
</Typography>
) : (
<Grid container spacing={1}>
{flattenRows(summary4F.rows).map((item, idx) => (
<Grid item xs={12} sm={6} md={3} key={idx}>
{truckGroups4F.map(({ truckLanceCode, slots }) => (
<Grid item xs={12} key={truckLanceCode}>
<Stack
direction="row"
direction={{ xs: "column", sm: "row" }}
spacing={1}
alignItems="center"
alignItems={{ xs: "stretch", sm: "center" }}
sx={{
border: '1px solid #e0e0e0',
border: "1px solid #e0e0e0",
borderRadius: 0.5,
p: 1,
backgroundColor: '#fff',
height: '100%'
backgroundColor: "#fff",
}}
>
{/* Time on the left */}
<Typography
<Typography
variant="body2"
sx={{
fontWeight: 600,
fontSize: '1rem',
minWidth: 50,
whiteSpace: 'nowrap'
sx={{
fontWeight: 700,
fontSize: "1rem",
minWidth: { sm: 160 },
pt: { xs: 0, sm: 0.5 },
}}
>
{item.truckDepartureTime}
{truckLanceCode}
</Typography>
{/* Single Button on the right */}
<Button
variant="outlined"
size="medium"
disabled={item.lane.unassigned === 0 || isAssigning}
onClick={() => handleLaneButtonClick("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate, item.lane.unassigned, item.lane.total)}
sx={{
flex: 1,
fontSize: '1.1rem',
py: 1,
px: 1.5,
borderWidth: 1,
borderColor: '#ccc',
fontWeight: 500,
'&:hover': {
borderColor: '#999',
backgroundColor: '#f5f5f5'
}
}}
<Stack
direction="row"
flexWrap="wrap"
sx={{ flex: 1, gap: 1 }}
>
{`${item.lane.truckLanceCode} (${item.lane.unassigned}/${item.lane.total})`}
</Button>
{slots.map((slot) => (
<Button
key={`${truckLanceCode}-${slot.sequenceIndex}-${slot.lane.truckLanceCode}-${slot.truckDepartureTime}`}
variant="outlined"
size="medium"
disabled={slot.lane.unassigned === 0 || isAssigning}
onClick={() =>
handleLaneButtonClick(
"4/F",
slot.truckDepartureTime,
slot.lane.truckLanceCode,
slot.lane.loadingSequence ?? null,
selectedDate,
slot.lane.unassigned,
slot.lane.total
)
}
sx={{
fontSize: "1rem",
py: 0.75,
px: 1.25,
borderWidth: 1,
borderColor: "#ccc",
fontWeight: 500,
"&:hover": {
borderColor: "#999",
backgroundColor: "#f5f5f5",
},
}}
>
{`${t("Loading sequence n", { n: slot.lane.loadingSequence ?? slot.sequenceIndex })} (${slot.lane.unassigned}/${slot.lane.total})`}
</Button>
))}
</Stack>
</Stack>
</Grid>
))}
@@ -573,7 +643,7 @@ const getDateLabel = (offset: number) => {
</Box>
</Stack>
</Grid>
)}
{/* 4/F Today default lane*/}
<Grid item xs={12}>
@@ -632,7 +702,9 @@ const getDateLabel = (offset: number) => {
{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 }}>
@@ -699,8 +771,8 @@ const getDateLabel = (offset: number) => {
</Box>
</Stack>
</Grid>
{/* 4/F 未完成已放單 - 與上方相同 UI */}
)}
{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 }}>
@@ -767,7 +839,10 @@ const getDateLabel = (offset: number) => {
</Box>
</Stack>
</Grid>
)}
<Grid item xs={12}>

<Stack direction="row" spacing={2}>
<Typography sx={{ fontWeight: 600, minWidth: 60, pt: 1 }}>{t("Truck X")} </Typography>
<Box


Loading…
Cancel
Save