mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
style: ui fix in scheduling
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
useMantineTheme,
|
||||
Modal,
|
||||
SimpleGrid,
|
||||
} from "@mantine/core";
|
||||
import { UseFormReturn } from "react-hook-form";
|
||||
import { BookingFormInputValues, BookingFormValues } from "./schema";
|
||||
@@ -52,6 +54,7 @@ interface DayData {
|
||||
export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
||||
const theme = useMantineTheme();
|
||||
const [currentDate, setCurrentDate] = useState(new Date());
|
||||
const [selectedDayForModal, setSelectedDayForModal] = useState<DayData | null>(null);
|
||||
|
||||
const selectedDate = form.watch("scheduledDate");
|
||||
const selectedScheduleId = form.watch("trainScheduleId");
|
||||
@@ -148,10 +151,35 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
||||
|
||||
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 (
|
||||
<Group gap={24} align="flex-start" wrap="nowrap">
|
||||
<Group gap={24} align="flex-start" wrap="wrap" style={{ width: "100%" }}>
|
||||
{/* ── Calendar Card ───────────────────────────────────── */}
|
||||
<Box style={{ flex: 1, minWidth: 0 }}>
|
||||
<Box style={{ flex: 1, minWidth: 300 }}>
|
||||
<Card p={0} style={{ overflow: "hidden" }}>
|
||||
{/* Card header */}
|
||||
<Group
|
||||
@@ -244,15 +272,7 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
||||
<DayCell
|
||||
key={di}
|
||||
day={d}
|
||||
selectedScheduleId={selectedScheduleId}
|
||||
onSelectSchedule={(scheduleId, dateString) => {
|
||||
form.setValue("scheduledDate", dateString, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
form.setValue("trainScheduleId", scheduleId, {
|
||||
shouldValidate: true,
|
||||
});
|
||||
}}
|
||||
onDayClick={handleDayClick}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
@@ -263,7 +283,7 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
||||
</Box>
|
||||
|
||||
{/* ── Side Panel ──────────────────────────────────────── */}
|
||||
<Stack gap={20} w={340} style={{ flexShrink: 0 }}>
|
||||
<Stack gap={20} style={{ width: 340, flexShrink: 0 }}>
|
||||
{/* Booking summary card */}
|
||||
<Card p={0} style={{ overflow: "hidden" }}>
|
||||
<Group
|
||||
@@ -361,17 +381,81 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) {
|
||||
</Stack>
|
||||
</Box>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
interface DayCellProps {
|
||||
day: DayData;
|
||||
selectedScheduleId: string;
|
||||
onSelectSchedule: (scheduleId: string, dateString: string) => void;
|
||||
onDayClick: (day: DayData) => void;
|
||||
}
|
||||
|
||||
function DayCell({ day: d, selectedScheduleId, onSelectSchedule }: DayCellProps) {
|
||||
function DayCell({ day: d, selectedScheduleId, onDayClick, onSelectSchedule }: DayCellProps) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
if (!d.isCurrentMonth) {
|
||||
@@ -400,125 +484,113 @@ function DayCell({ day: d, selectedScheduleId, onSelectSchedule }: DayCellProps)
|
||||
: "transparent";
|
||||
|
||||
const cellBorder = d.isSelectedDate
|
||||
? `1.5px solid ${theme.colors["edr-green"][5]}`
|
||||
? `2px solid ${theme.colors["edr-green"][5]}`
|
||||
: d.hasSchedule
|
||||
? "1px solid #E7E8E5"
|
||||
? `1px solid ${theme.colors["edr-border"][0]}`
|
||||
: "none";
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={() => d.hasSchedule && onDayClick(d)}
|
||||
style={{
|
||||
height: 92,
|
||||
borderRadius: theme.radius.md,
|
||||
backgroundColor: cellBg,
|
||||
border: cellBorder,
|
||||
overflow: "hidden",
|
||||
padding: "4px 6px 6px",
|
||||
padding: "8px 10px 10px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
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 */}
|
||||
<Group justify="space-between" align="center" style={{ flexShrink: 0 }}>
|
||||
<Text
|
||||
fz={14}
|
||||
fw={d.hasSchedule ? 800 : 600}
|
||||
c={
|
||||
d.isToday && !d.isSelectedDate
|
||||
? "edr-green.6"
|
||||
: d.hasSchedule
|
||||
? "edr-text.0"
|
||||
: "edr-muted"
|
||||
}
|
||||
>
|
||||
{d.day}
|
||||
</Text>
|
||||
<Stack gap={0}>
|
||||
<Text
|
||||
fz={16}
|
||||
fw={800}
|
||||
c={
|
||||
d.isToday && !d.isSelectedDate
|
||||
? "edr-green.6"
|
||||
: d.hasSchedule
|
||||
? "edr-text.0"
|
||||
: "edr-muted"
|
||||
}
|
||||
>
|
||||
{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 && (
|
||||
<Check
|
||||
size={15}
|
||||
color={theme.colors["edr-green"][5]}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
width: 24,
|
||||
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>
|
||||
|
||||
{/* Departure chips */}
|
||||
{/* Schedule times */}
|
||||
{d.hasSchedule && (
|
||||
<Stack gap={3} style={{ flex: 1, overflow: "hidden" }}>
|
||||
{d.schedules.slice(0, 2).map((s) => {
|
||||
const isChipSelected = s.id === selectedScheduleId;
|
||||
const isFull = s.remainingWagons <= 0;
|
||||
const canSelect = !isFull;
|
||||
|
||||
return (
|
||||
<Stack gap={3} style={{ flex: 1, overflow: "hidden", minWidth: 0 }}>
|
||||
{d.schedules.slice(0, 2).map((s) => (
|
||||
<Group key={s.id} gap={6} align="center" style={{ minWidth: 0 }}>
|
||||
<Box
|
||||
key={s.id}
|
||||
onClick={() =>
|
||||
canSelect && onSelectSchedule(s.id, d.dateString)
|
||||
}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 4,
|
||||
borderRadius: 7,
|
||||
padding: "4px 6px",
|
||||
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"
|
||||
}`,
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.colors["edr-green"][5],
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
fz={12}
|
||||
fw={700}
|
||||
c="edr-text.0"
|
||||
style={{ flex: 1, minWidth: 0 }}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: isChipSelected
|
||||
? "white"
|
||||
: isFull
|
||||
? 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>
|
||||
);
|
||||
})}
|
||||
{format(new Date(s.scheduleDate), "HH:mm")}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
{d.schedules.length > 2 && (
|
||||
<Text fz={11} fw={600} c="edr-green.7" style={{ paddingTop: 2 }}>
|
||||
+{d.schedules.length - 2} more
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user