This commit is contained in:
Marshal
2026-06-13 08:51:57 +00:00
parent b2f13c95c5
commit e3cd369b01

View File

@@ -1,8 +1,9 @@
import { useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import type { ReactNode } from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
Accordion,
ActionIcon,
Alert,
Badge,
Box,
@@ -25,6 +26,8 @@ import {
Boxes,
CalendarDays,
CheckCircle2,
ChevronLeft,
ChevronRight,
Clock,
FileSignature,
Hourglass,
@@ -551,22 +554,35 @@ export default function BatchScheduleDetailPage() {
[data],
);
// Open today's day, any day with bookings, and pending-contract; else the last day.
const openDays = useMemo(() => {
if (!dayGroups.length && !data?.pendingContract.bookings.length) return [];
const todayEat = new Intl.DateTimeFormat("en-CA", {
timeZone: "Africa/Addis_Ababa",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date());
const open = dayGroups
.filter((d) => d.totalBookings > 0 || d.date === todayEat)
.map((d) => d.date);
if (!open.length && dayGroups.length) open.push(dayGroups[dayGroups.length - 1].date);
if (data?.pendingContract.bookings.length) open.push("pending-contract");
return open;
}, [dayGroups, data]);
const todayEat = useMemo(
() =>
new Intl.DateTimeFormat("en-CA", {
timeZone: "Africa/Addis_Ababa",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).format(new Date()),
[],
);
// Date-stepper: which day is currently shown. Default to today, else the first
// day with bookings, else the first day. Keep the selection if still valid.
const [selectedDate, setSelectedDate] = useState<string | null>(null);
useEffect(() => {
if (!dayGroups.length) return;
if (selectedDate && dayGroups.some((d) => d.date === selectedDate)) return;
const preferred =
dayGroups.find((d) => d.date === todayEat) ??
dayGroups.find((d) => d.totalBookings > 0) ??
dayGroups[0];
setSelectedDate(preferred.date);
}, [dayGroups, selectedDate, todayEat]);
const selectedIndex = Math.max(
0,
dayGroups.findIndex((d) => d.date === selectedDate),
);
const selectedDay = dayGroups[selectedIndex];
const handleRunAllocation = () => {
runAllocation
@@ -815,79 +831,119 @@ export default function BatchScheduleDetailPage() {
</Box>
</Group>
<Accordion
multiple
defaultValue={openDays}
variant="separated"
radius="md"
mt="md"
className="bb-window-accordion"
>
{dayGroups.map((day) => (
<Accordion.Item key={day.date} value={day.date}>
<Accordion.Control>
<Group justify="space-between" wrap="nowrap" pr="md" gap="sm">
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
<Box
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 34,
height: 34,
borderRadius: 10,
flexShrink: 0,
background: day.totalBookings ? "#FEF1D5" : "var(--mantine-color-gray-0)",
border: day.totalBookings
? "1px solid #FBD171"
: "1px solid var(--mantine-color-gray-2)",
color: day.totalBookings ? "#B26C09" : "var(--mantine-color-gray-5)",
}}
>
<CalendarDays size={16} />
</Box>
<Box style={{ minWidth: 0 }}>
<Text fw={700} size="sm" truncate>
{day.dateLabel}
</Text>
<Text size="xs" c="dimmed">
{day.totalBookings
? `${day.totalBookings} booking${day.totalBookings === 1 ? "" : "s"} · ${day.windows.length} windows`
: `${day.windows.length} windows · no bookings`}
</Text>
</Box>
</Group>
<Group gap={6} wrap="nowrap">
{day.hasIssues ? (
<Badge
variant="light"
color="red"
size="sm"
leftSection={<AlertTriangle size={10} />}
>
Issues
</Badge>
) : null}
<WindowCountChips counts={day.counts} />
</Group>
{dayGroups.length && selectedDay ? (
<>
{/* Date stepper — page back/forward through each day in the range */}
<Group justify="center" align="center" wrap="nowrap" gap="md" mt="md">
<ActionIcon
variant="light"
color="#F2A516"
size="xl"
radius="xl"
aria-label="Previous day"
disabled={selectedIndex <= 0}
onClick={() => setSelectedDate(dayGroups[selectedIndex - 1]?.date ?? null)}
>
<ChevronLeft size={20} />
</ActionIcon>
<Paper
withBorder
radius="xl"
px="xl"
py="xs"
style={{
flex: 1,
maxWidth: 360,
textAlign: "center",
background: selectedDay.totalBookings ? "#FEF1D5" : "white",
borderColor: selectedDay.totalBookings
? "#FBD171"
: "var(--mantine-color-gray-2)",
}}
>
<Group justify="center" gap={8} wrap="nowrap">
<CalendarDays size={15} color="#B26C09" />
<Text
fw={800}
style={{ color: selectedDay.totalBookings ? "#8A5304" : "#0f172a" }}
>
{selectedDay.dateLabel}
</Text>
{selectedDay.date === todayEat ? (
<Badge size="xs" variant="light" color="#F2A516">
Today
</Badge>
) : null}
</Group>
</Accordion.Control>
<Accordion.Panel>
<Accordion
multiple
defaultValue={openWindowKeys}
variant="separated"
radius="md"
className="bb-window-accordion"
>
{day.windows.map((window) => (
<WindowAccordionItem key={window.key} window={window} />
))}
</Accordion>
</Accordion.Panel>
</Accordion.Item>
))}
{data.pendingContract.bookings.length ? (
<Text size="xs" c="dimmed" mt={2}>
{selectedDay.totalBookings
? `${selectedDay.totalBookings} booking${selectedDay.totalBookings === 1 ? "" : "s"} · ${selectedDay.windows.length} windows`
: `${selectedDay.windows.length} windows · no bookings`}
</Text>
</Paper>
<ActionIcon
variant="light"
color="#F2A516"
size="xl"
radius="xl"
aria-label="Next day"
disabled={selectedIndex >= dayGroups.length - 1}
onClick={() => setSelectedDate(dayGroups[selectedIndex + 1]?.date ?? null)}
>
<ChevronRight size={20} />
</ActionIcon>
</Group>
<Group justify="space-between" align="center" mt="sm">
<Text size="xs" c="dimmed">
Day {selectedIndex + 1} of {dayGroups.length}
</Text>
<Group gap={6} wrap="nowrap">
{selectedDay.hasIssues ? (
<Badge
variant="light"
color="red"
size="sm"
leftSection={<AlertTriangle size={10} />}
>
Issues
</Badge>
) : null}
<WindowCountChips counts={selectedDay.counts} />
</Group>
</Group>
<Accordion
key={selectedDay.date}
multiple
defaultValue={openWindowKeys}
variant="separated"
radius="md"
mt="md"
className="bb-window-accordion"
>
{selectedDay.windows.map((window) => (
<WindowAccordionItem key={window.key} window={window} />
))}
</Accordion>
</>
) : (
<Text size="sm" c="dimmed" ta="center" py="lg">
No batch windows for this schedule.
</Text>
)}
{data.pendingContract.bookings.length ? (
<Accordion
multiple
defaultValue={["pending-contract"]}
variant="separated"
radius="md"
mt="md"
className="bb-window-accordion"
>
<Accordion.Item value="pending-contract">
<Accordion.Control>
<Group justify="space-between" wrap="nowrap" pr="md" gap="sm">
@@ -927,8 +983,8 @@ export default function BatchScheduleDetailPage() {
<BookingTable bookings={data.pendingContract.bookings} />
</Accordion.Panel>
</Accordion.Item>
) : null}
</Accordion>
</Accordion>
) : null}
</Paper>
{/* Train composition */}