mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
style: ui fix in scheduling
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
|
Modal,
|
||||||
|
SimpleGrid,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { UseFormReturn } from "react-hook-form";
|
import { UseFormReturn } from "react-hook-form";
|
||||||
import { BookingFormInputValues, BookingFormValues } from "./schema";
|
import { BookingFormInputValues, BookingFormValues } from "./schema";
|
||||||
@@ -52,6 +54,7 @@ interface DayData {
|
|||||||
export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const [currentDate, setCurrentDate] = useState(new Date());
|
const [currentDate, setCurrentDate] = useState(new Date());
|
||||||
|
const [selectedDayForModal, setSelectedDayForModal] = useState<DayData | null>(null);
|
||||||
|
|
||||||
const selectedDate = form.watch("scheduledDate");
|
const selectedDate = form.watch("scheduledDate");
|
||||||
const selectedScheduleId = form.watch("trainScheduleId");
|
const selectedScheduleId = form.watch("trainScheduleId");
|
||||||
@@ -148,10 +151,35 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
|||||||
|
|
||||||
const weeksCount = Math.ceil(days.length / 7);
|
const weeksCount = Math.ceil(days.length / 7);
|
||||||
|
|
||||||
|
const handleDayClick = (day: DayData) => {
|
||||||
|
if (day.schedules.length > 1) {
|
||||||
|
setSelectedDayForModal(day);
|
||||||
|
} else if (day.schedules.length === 1) {
|
||||||
|
form.setValue("scheduledDate", day.dateString, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
form.setValue("trainScheduleId", day.schedules[0].id, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectScheduleFromModal = (scheduleId: string) => {
|
||||||
|
if (selectedDayForModal) {
|
||||||
|
form.setValue("scheduledDate", selectedDayForModal.dateString, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
form.setValue("trainScheduleId", scheduleId, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
setSelectedDayForModal(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group gap={24} align="flex-start" wrap="nowrap">
|
<Group gap={24} align="flex-start" wrap="wrap" style={{ width: "100%" }}>
|
||||||
{/* ── Calendar Card ───────────────────────────────────── */}
|
{/* ── Calendar Card ───────────────────────────────────── */}
|
||||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
<Box style={{ flex: 1, minWidth: 300 }}>
|
||||||
<Card p={0} style={{ overflow: "hidden" }}>
|
<Card p={0} style={{ overflow: "hidden" }}>
|
||||||
{/* Card header */}
|
{/* Card header */}
|
||||||
<Group
|
<Group
|
||||||
@@ -244,15 +272,7 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
|||||||
<DayCell
|
<DayCell
|
||||||
key={di}
|
key={di}
|
||||||
day={d}
|
day={d}
|
||||||
selectedScheduleId={selectedScheduleId}
|
onDayClick={handleDayClick}
|
||||||
onSelectSchedule={(scheduleId, dateString) => {
|
|
||||||
form.setValue("scheduledDate", dateString, {
|
|
||||||
shouldValidate: true,
|
|
||||||
});
|
|
||||||
form.setValue("trainScheduleId", scheduleId, {
|
|
||||||
shouldValidate: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -263,7 +283,7 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* ── Side Panel ──────────────────────────────────────── */}
|
{/* ── Side Panel ──────────────────────────────────────── */}
|
||||||
<Stack gap={20} w={340} style={{ flexShrink: 0 }}>
|
<Stack gap={20} style={{ width: 340, flexShrink: 0 }}>
|
||||||
{/* Booking summary card */}
|
{/* Booking summary card */}
|
||||||
<Card p={0} style={{ overflow: "hidden" }}>
|
<Card p={0} style={{ overflow: "hidden" }}>
|
||||||
<Group
|
<Group
|
||||||
@@ -361,17 +381,81 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
{/* ── Schedule Selection Modal ──────────────────────────── */}
|
||||||
|
<Modal
|
||||||
|
opened={!!selectedDayForModal}
|
||||||
|
onClose={() => setSelectedDayForModal(null)}
|
||||||
|
title={selectedDayForModal ? format(new Date(selectedDayForModal.dateString + "T00:00:00"), "EEE, MMM d yyyy") : ""}
|
||||||
|
centered
|
||||||
|
size="sm"
|
||||||
|
styles={{
|
||||||
|
header: { borderBottom: `1px solid ${theme.colors["edr-border"][0]}` },
|
||||||
|
body: { padding: 24 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap={12}>
|
||||||
|
<Text fz={13} c="edr-muted" fw={500}>
|
||||||
|
Choose a departure time
|
||||||
|
</Text>
|
||||||
|
{selectedDayForModal?.schedules.map((schedule) => (
|
||||||
|
<Button
|
||||||
|
key={schedule.id}
|
||||||
|
variant="outline"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => handleSelectScheduleFromModal(schedule.id)}
|
||||||
|
style={{ height: 64, justifyContent: "flex-start" }}
|
||||||
|
styles={{
|
||||||
|
inner: { justifyContent: "flex-start" },
|
||||||
|
root: {
|
||||||
|
borderColor: theme.colors["edr-border"][0],
|
||||||
|
transition: "all 150ms ease",
|
||||||
|
"&:hover": {
|
||||||
|
borderColor: theme.colors["edr-green"][5],
|
||||||
|
backgroundColor: theme.colors["edr-soft"][0],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap={16} w="100%">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
borderRadius: theme.radius.md,
|
||||||
|
backgroundColor: theme.colors["edr-soft"][0],
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Train size={24} color={theme.colors["edr-green"][5]} />
|
||||||
|
</Box>
|
||||||
|
<Stack gap={3} style={{ flex: 1, alignItems: "flex-start" }}>
|
||||||
|
<Text fw={700} fz={18} c="edr-text.0">
|
||||||
|
{format(new Date(schedule.scheduleDate), "HH:mm")}
|
||||||
|
</Text>
|
||||||
|
{schedule.trainNumber && (
|
||||||
|
<Text fz={12} c="edr-muted">
|
||||||
|
Train {schedule.trainNumber}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DayCellProps {
|
interface DayCellProps {
|
||||||
day: DayData;
|
day: DayData;
|
||||||
selectedScheduleId: string;
|
onDayClick: (day: DayData) => void;
|
||||||
onSelectSchedule: (scheduleId: string, dateString: string) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function DayCell({ day: d, selectedScheduleId, onSelectSchedule }: DayCellProps) {
|
function DayCell({ day: d, selectedScheduleId, onDayClick, onSelectSchedule }: DayCellProps) {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
if (!d.isCurrentMonth) {
|
if (!d.isCurrentMonth) {
|
||||||
@@ -400,125 +484,113 @@ function DayCell({ day: d, selectedScheduleId, onSelectSchedule }: DayCellProps)
|
|||||||
: "transparent";
|
: "transparent";
|
||||||
|
|
||||||
const cellBorder = d.isSelectedDate
|
const cellBorder = d.isSelectedDate
|
||||||
? `1.5px solid ${theme.colors["edr-green"][5]}`
|
? `2px solid ${theme.colors["edr-green"][5]}`
|
||||||
: d.hasSchedule
|
: d.hasSchedule
|
||||||
? "1px solid #E7E8E5"
|
? `1px solid ${theme.colors["edr-border"][0]}`
|
||||||
: "none";
|
: "none";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
onClick={() => d.hasSchedule && onDayClick(d)}
|
||||||
style={{
|
style={{
|
||||||
height: 92,
|
height: 92,
|
||||||
borderRadius: theme.radius.md,
|
borderRadius: theme.radius.md,
|
||||||
backgroundColor: cellBg,
|
backgroundColor: cellBg,
|
||||||
border: cellBorder,
|
border: cellBorder,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
padding: "4px 6px 6px",
|
padding: "8px 10px 10px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: 4,
|
gap: 4,
|
||||||
|
cursor: d.hasSchedule ? "pointer" : "default",
|
||||||
|
transition: "all 150ms ease",
|
||||||
|
boxShadow: d.hasSchedule && !d.isSelectedDate ? "0 1px 3px rgba(0, 0, 0, 0.05)" : "none",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (d.hasSchedule && !d.isSelectedDate) {
|
||||||
|
e.currentTarget.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.1)";
|
||||||
|
e.currentTarget.style.borderColor = theme.colors["edr-border"][0];
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (d.hasSchedule && !d.isSelectedDate) {
|
||||||
|
e.currentTarget.style.boxShadow = "0 1px 3px rgba(0, 0, 0, 0.05)";
|
||||||
|
e.currentTarget.style.borderColor = theme.colors["edr-border"][0];
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Day number + check icon */}
|
{/* Day number + check icon */}
|
||||||
<Group justify="space-between" align="center" style={{ flexShrink: 0 }}>
|
<Group justify="space-between" align="center" style={{ flexShrink: 0 }}>
|
||||||
<Text
|
<Stack gap={0}>
|
||||||
fz={14}
|
<Text
|
||||||
fw={d.hasSchedule ? 800 : 600}
|
fz={16}
|
||||||
c={
|
fw={800}
|
||||||
d.isToday && !d.isSelectedDate
|
c={
|
||||||
? "edr-green.6"
|
d.isToday && !d.isSelectedDate
|
||||||
: d.hasSchedule
|
? "edr-green.6"
|
||||||
? "edr-text.0"
|
: d.hasSchedule
|
||||||
: "edr-muted"
|
? "edr-text.0"
|
||||||
}
|
: "edr-muted"
|
||||||
>
|
}
|
||||||
{d.day}
|
>
|
||||||
</Text>
|
{d.day}
|
||||||
|
</Text>
|
||||||
|
{d.isToday && !d.isSelectedDate && (
|
||||||
|
<Text fz={9} fw={700} c="edr-green.6" style={{ letterSpacing: "0.04em" }}>
|
||||||
|
TODAY
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
{d.isSelectedDate && (
|
{d.isSelectedDate && (
|
||||||
<Check
|
<Box
|
||||||
size={15}
|
style={{
|
||||||
color={theme.colors["edr-green"][5]}
|
width: 24,
|
||||||
strokeWidth={2.5}
|
height: 24,
|
||||||
/>
|
borderRadius: "50%",
|
||||||
|
backgroundColor: theme.colors["edr-green"][5],
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
size={14}
|
||||||
|
color="white"
|
||||||
|
strokeWidth={3}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* Departure chips */}
|
{/* Schedule times */}
|
||||||
{d.hasSchedule && (
|
{d.hasSchedule && (
|
||||||
<Stack gap={3} style={{ flex: 1, overflow: "hidden" }}>
|
<Stack gap={3} style={{ flex: 1, overflow: "hidden", minWidth: 0 }}>
|
||||||
{d.schedules.slice(0, 2).map((s) => {
|
{d.schedules.slice(0, 2).map((s) => (
|
||||||
const isChipSelected = s.id === selectedScheduleId;
|
<Group key={s.id} gap={6} align="center" style={{ minWidth: 0 }}>
|
||||||
const isFull = s.remainingWagons <= 0;
|
|
||||||
const canSelect = !isFull;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
<Box
|
||||||
key={s.id}
|
|
||||||
onClick={() =>
|
|
||||||
canSelect && onSelectSchedule(s.id, d.dateString)
|
|
||||||
}
|
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
width: 4,
|
||||||
alignItems: "center",
|
height: 4,
|
||||||
gap: 4,
|
borderRadius: "50%",
|
||||||
borderRadius: 7,
|
backgroundColor: theme.colors["edr-green"][5],
|
||||||
padding: "4px 6px",
|
flexShrink: 0,
|
||||||
cursor: canSelect ? "pointer" : "default",
|
|
||||||
backgroundColor: isChipSelected
|
|
||||||
? theme.colors["edr-green"][5]
|
|
||||||
: isFull
|
|
||||||
? theme.colors["edr-red-soft"][0]
|
|
||||||
: theme.colors["edr-soft"][0],
|
|
||||||
border: `1px solid ${
|
|
||||||
isChipSelected
|
|
||||||
? theme.colors["edr-green"][5]
|
|
||||||
: isFull
|
|
||||||
? "#EFCFCA"
|
|
||||||
: "#BFE3D4"
|
|
||||||
}`,
|
|
||||||
}}
|
}}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
fz={12}
|
||||||
|
fw={700}
|
||||||
|
c="edr-text.0"
|
||||||
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
>
|
>
|
||||||
<Box
|
{format(new Date(s.scheduleDate), "HH:mm")}
|
||||||
style={{
|
</Text>
|
||||||
width: 6,
|
</Group>
|
||||||
height: 6,
|
))}
|
||||||
borderRadius: "50%",
|
{d.schedules.length > 2 && (
|
||||||
backgroundColor: isChipSelected
|
<Text fz={11} fw={600} c="edr-green.7" style={{ paddingTop: 2 }}>
|
||||||
? "white"
|
+{d.schedules.length - 2} more
|
||||||
: isFull
|
</Text>
|
||||||
? theme.colors["edr-red"][0]
|
)}
|
||||||
: theme.colors["edr-green"][5],
|
|
||||||
flexShrink: 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
fz={11}
|
|
||||||
fw={700}
|
|
||||||
style={{ flex: 1, minWidth: 0, overflow: "hidden" }}
|
|
||||||
truncate
|
|
||||||
c={
|
|
||||||
isChipSelected ? "white" : isFull ? "edr-red.0" : "edr-green.7"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isFull
|
|
||||||
? "Full"
|
|
||||||
: s.trainNumber
|
|
||||||
? s.trainNumber
|
|
||||||
: `${s.remainingWagons} wgn`}
|
|
||||||
</Text>
|
|
||||||
<ChevronRight
|
|
||||||
size={12}
|
|
||||||
color={
|
|
||||||
isChipSelected
|
|
||||||
? "white"
|
|
||||||
: isFull
|
|
||||||
? theme.colors["edr-muted"][0]
|
|
||||||
: theme.colors["edr-green"][7]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user