diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx index b9551c321..0a4dcb6ce 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx @@ -1,7 +1,7 @@ -import { Grid, Stack } from "@mantine/core"; -import { useNavigate } from "react-router-dom"; import type { Currency } from "@/pages/billing/invoices.mock"; import { formatCurrency } from "@/pages/billing/invoices.mock"; +import { Grid, Stack } from "@mantine/core"; +import { useNavigate } from "react-router-dom"; import { FreightVolumeSection, HelloSection, @@ -49,7 +49,7 @@ export default function MyPortalPage() { outstandingInvoicesLength={outstandingInvoices.length} totalOutstanding={totalOutstanding} deliveredCount={dashboard?.deliveredCount.toString()} - completionRate={dashboard?.completionRate} + completionRate={dashboard?.completionRate?.toString()} spendYtd={ dashboard ? formatCurrency( diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/InvoicesSection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/InvoicesSection.tsx index fcdc14095..4e2c30343 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/InvoicesSection.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/InvoicesSection.tsx @@ -1,24 +1,24 @@ -import { Box, Group, Stack, Text } from "@mantine/core"; -import { CheckCircle2, ChevronRight, Clock3, Zap } from "lucide-react"; -import { format } from "date-fns"; -import { memo } from "react"; -import { Link } from "react-router-dom"; import type { Currency, InvoiceStatus } from "@/pages/billing/invoices.mock"; import { formatCurrency } from "@/pages/billing/invoices.mock"; +import { Box, Group, Stack, Text } from "@mantine/core"; +import { format } from "date-fns"; +import { CheckCircle2, ChevronRight, Clock3, Zap } from "lucide-react"; +import { memo } from "react"; +import { Link } from "react-router-dom"; import { cv, INVOICE_BADGE } from "../constants"; import { Card } from "./Card"; import { EmptyState } from "./EmptyState"; interface InvoicesSectionProps { invoices: Array<{ - id: string; + id: number; number: string; bookingReference: string; amount: number; currency: Currency; status: InvoiceStatus; dueDate: string; - paidDate?: string; + paidDate: string | null; }>; } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-scheduling.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-scheduling.tsx index 181e611be..3a989e60c 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-scheduling.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-scheduling.tsx @@ -1,38 +1,39 @@ +import { api } from "@/services/api"; +import type { Freight } from "@edr/types"; import { Box, Button, Card, Group, + Modal, Stack, Text, - useMantineTheme, + useMantineTheme } from "@mantine/core"; -import { UseFormReturn } from "react-hook-form"; -import { BookingFormInputValues, BookingFormValues } from "./schema"; +import { useQuery } from "@tanstack/react-query"; import { + addMonths, + eachDayOfInterval, + endOfMonth, + endOfWeek, + format, + isSameMonth, + isToday, + startOfMonth, + startOfWeek, +} from "date-fns"; +import { + Calendar as CalendarIcon, + Check, ChevronLeft, ChevronRight, - Check, - Train, - Route, Package, - Calendar as CalendarIcon, + Route, + Train, } from "lucide-react"; -import type { Freight } from "@edr/types"; import React, { useMemo, useState } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { api } from "@/services/api"; -import { - format, - startOfMonth, - endOfMonth, - startOfWeek, - endOfWeek, - eachDayOfInterval, - isToday, - isSameMonth, - addMonths, -} from "date-fns"; +import { UseFormReturn } from "react-hook-form"; +import { BookingFormInputValues, BookingFormValues } from "./schema"; interface StepSchedulingProps { form: UseFormReturn; @@ -52,6 +53,7 @@ interface DayData { export function StepScheduling({ form, referenceData }: StepSchedulingProps) { const theme = useMantineTheme(); const [currentDate, setCurrentDate] = useState(new Date()); + const [selectedDayForModal, setSelectedDayForModal] = useState(null); const selectedDate = form.watch("scheduledDate"); const selectedScheduleId = form.watch("trainScheduleId"); @@ -148,10 +150,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 ( - + {/* ── Calendar Card ───────────────────────────────────── */} - + {/* Card header */} { - form.setValue("scheduledDate", dateString, { - shouldValidate: true, - }); - form.setValue("trainScheduleId", scheduleId, { - shouldValidate: true, - }); - }} + onDayClick={handleDayClick} /> ))} @@ -263,7 +282,7 @@ export function StepScheduling({ form, referenceData }: StepSchedulingProps) { {/* ── Side Panel ──────────────────────────────────────── */} - + {/* Booking summary card */} + + {/* ── Schedule Selection Modal ──────────────────────────── */} + 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 }, + }} + > + + + Choose a departure time + + {selectedDayForModal?.schedules.map((schedule) => ( + + ))} + + ); } 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, onDayClick, }: DayCellProps) { const theme = useMantineTheme(); if (!d.isCurrentMonth) { @@ -400,125 +483,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 ( 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 */} - - {d.day} - + + + {d.day} + + {d.isToday && !d.isSelectedDate && ( + + TODAY + + )} + {d.isSelectedDate && ( - + + + )} - {/* Departure chips */} + {/* Schedule times */} {d.hasSchedule && ( - - {d.schedules.slice(0, 2).map((s) => { - const isChipSelected = s.id === selectedScheduleId; - const isFull = s.remainingWagons <= 0; - const canSelect = !isFull; - - return ( + + {d.schedules.slice(0, 2).map((s) => ( + - 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, }} + /> + - - - {isFull - ? "Full" - : s.trainNumber - ? s.trainNumber - : `${s.remainingWagons} wgn`} - - - - ); - })} + {format(new Date(s.scheduleDate), "HH:mm")} + + + ))} + {d.schedules.length > 2 && ( + + +{d.schedules.length - 2} more + + )} )}