mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-09 11:09:48 -04:00
@@ -14,6 +14,7 @@ import React, { useState, useMemo, useCallback } from "react";
|
||||
|
||||
import {
|
||||
Box,
|
||||
ListSubheader,
|
||||
Button,
|
||||
Chip,
|
||||
Dialog,
|
||||
@@ -88,7 +89,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
const bookingsContext = useBookings();
|
||||
const roomsContext = useRooms();
|
||||
const usersContext = useUsers();
|
||||
const bookings = useMemo(
|
||||
const bookings: Booking[] = useMemo(
|
||||
() => bookingsContext.bookings ?? [],
|
||||
[bookingsContext.bookings]
|
||||
);
|
||||
@@ -530,35 +531,53 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
disabled={isViewMode}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: { maxHeight: 400 },
|
||||
},
|
||||
// Add a custom footer with a Done button
|
||||
MenuListProps: {
|
||||
"aria-label": "invitee-list",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
disabled
|
||||
className={
|
||||
remainingSlots < 0
|
||||
? "bookingform-invitee-over"
|
||||
: "bookingform-invitee-remaining"
|
||||
}
|
||||
<ListSubheader
|
||||
disableSticky={false}
|
||||
className="bookingform-invitee-header"
|
||||
>
|
||||
{remainingSlots >= 0
|
||||
? `${remainingSlots} invitee slot${
|
||||
remainingSlots === 1 ? "" : "s"
|
||||
} left`
|
||||
: `${Math.abs(remainingSlots)} over room capacity`}
|
||||
</MenuItem>
|
||||
<span
|
||||
className={
|
||||
"bookingform-invitee-header-label" +
|
||||
(remainingSlots < 0 ? " over" : "")
|
||||
}
|
||||
>
|
||||
{remainingSlots >= 0
|
||||
? `${remainingSlots} invitee slot${
|
||||
remainingSlots === 1 ? "" : "s"
|
||||
} left`
|
||||
: `${Math.abs(remainingSlots)} over room capacity`}
|
||||
</span>
|
||||
</ListSubheader>
|
||||
{users.map((user: import("../interfaces").User) => {
|
||||
const email = user.email ?? user.name;
|
||||
const isSelected = invitees.includes(email);
|
||||
// User unavailable if booked for another event at this time
|
||||
const unavailable = bookings.some((b: Booking) => {
|
||||
if (isEdit && b.id === editBooking?.id) {
|
||||
const unavailable = (bookings as unknown as Booking[]).some(
|
||||
(b) => {
|
||||
if (typeof b === "object" && b !== null && "id" in b) {
|
||||
const booking = b as Booking;
|
||||
if (isEdit && booking.id === editBooking?.id) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
booking.invitees?.includes(email) &&
|
||||
new Date(start) < new Date(booking.end_time) &&
|
||||
new Date(end) > new Date(booking.start_time)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
b.invitees?.includes(email) &&
|
||||
new Date(start) < new Date(b.end_time) &&
|
||||
new Date(end) > new Date(b.start_time)
|
||||
);
|
||||
});
|
||||
);
|
||||
const disableUnselected =
|
||||
!isSelected &&
|
||||
(unavailable || invitees.length >= roomCapacity);
|
||||
|
||||
@@ -28,6 +28,7 @@ import { getRooms } from "../apis/rooms";
|
||||
|
||||
// Context imports
|
||||
import { useBookings } from "../context/BookingContext";
|
||||
import { useUsers } from "../context/UserContext";
|
||||
|
||||
// Utility/helper imports
|
||||
|
||||
@@ -46,6 +47,7 @@ const ConfirmationPage: React.FC = () => {
|
||||
const location = useLocation();
|
||||
// Always call hooks at the top level
|
||||
const { bookings, refreshBookings } = useBookings();
|
||||
const { users } = useUsers();
|
||||
// booking is passed from BookingForm or BookingPage
|
||||
|
||||
/**
|
||||
@@ -93,7 +95,7 @@ const ConfirmationPage: React.FC = () => {
|
||||
return () => {
|
||||
es.close();
|
||||
};
|
||||
}, [bookings, location.state, booking?.id, navigate]);
|
||||
}, [bookings, location.state, booking?.id, navigate, booking]);
|
||||
/**
|
||||
* Subscribe to SSE for live booking status updates.
|
||||
*/
|
||||
@@ -125,7 +127,7 @@ const ConfirmationPage: React.FC = () => {
|
||||
return () => {
|
||||
es.close();
|
||||
};
|
||||
}, [bookings, location.state, booking?.id]);
|
||||
}, [bookings, location.state, booking?.id, booking]);
|
||||
/**
|
||||
* Editing state for toggling between confirmation and edit form.
|
||||
*/
|
||||
@@ -164,8 +166,19 @@ const ConfirmationPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Enrich booking with room details if missing
|
||||
// Enrich booking with room details and invitee names if missing
|
||||
let enrichedBooking = booking;
|
||||
if (
|
||||
enrichedBooking &&
|
||||
Array.isArray(users) &&
|
||||
Array.isArray(enrichedBooking.invitees)
|
||||
) {
|
||||
const inviteeNames = enrichedBooking.invitees.map((email) => {
|
||||
const user = users.find((u) => u.email === email);
|
||||
return user ? user.name : email;
|
||||
});
|
||||
enrichedBooking = { ...enrichedBooking, invitees: inviteeNames };
|
||||
}
|
||||
if (
|
||||
enrichedBooking &&
|
||||
!enrichedBooking.room &&
|
||||
|
||||
@@ -117,3 +117,27 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
/* Sticky header for invitee selection */
|
||||
.bookingform-invitee-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--mui-palette-background-paper, #fff);
|
||||
border-bottom: 1px solid var(--color-horizontal-rule, #eee);
|
||||
}
|
||||
|
||||
.bookingform-invitee-header-label {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bookingform-invitee-header-label.over {
|
||||
color: var(--color-error, #d32f2f);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
/* Text & Foreground Colors */
|
||||
--color-text-primary: #23272f; /* Main text */
|
||||
--color-horizontal-rule: #eee; /* Horizontal rules */
|
||||
--color-text-secondary: #555; /* Secondary text */
|
||||
--color-text-muted: #444; /* Muted/disabled text */
|
||||
--color-text-disabled: #222; /* Disabled/very muted text */
|
||||
@@ -27,24 +28,24 @@
|
||||
--room-icon: rgba(0, 0, 0, 0.54); /* Room icon color */
|
||||
|
||||
/* Room color variables: Color Universal Design (CUD) palette, color-blind friendly */
|
||||
--room-color-1: #e69f00; /* orange */
|
||||
--room-color-2: #56b4e9; /* sky blue */
|
||||
--room-color-3: #009e73; /* bluish green */
|
||||
--room-color-4: #f0e442; /* yellow */
|
||||
--room-color-5: #0072b2; /* blue */
|
||||
--room-color-6: #d55e00; /* vermillion */
|
||||
--room-color-7: #cc79a7; /* reddish purple */
|
||||
--room-color-8: #000000; /* black */
|
||||
--room-color-9: #999933; /* olive */
|
||||
--room-color-10: #882255; /* wine */
|
||||
--room-color-11: #44aa99; /* teal */
|
||||
--room-color-12: #117733; /* green */
|
||||
--room-color-1: #0072b2; /* blue */
|
||||
--room-color-2: #117733; /* green */
|
||||
--room-color-3: #aa3377; /* magenta */
|
||||
--room-color-4: #e69f00; /* orange */
|
||||
--room-color-5: #56b4e9; /* sky blue */
|
||||
--room-color-6: #009e73; /* bluish green */
|
||||
--room-color-7: #f0e442; /* yellow */
|
||||
--room-color-8: #d55e00; /* vermillion */
|
||||
--room-color-9: #cc79a7; /* reddish purple */
|
||||
--room-color-10: #999933; /* olive */
|
||||
--room-color-11: #882255; /* wine */
|
||||
--room-color-12: #44aa99; /* teal */
|
||||
--room-color-13: #aa4499; /* purple */
|
||||
--room-color-14: #332288; /* navy */
|
||||
--room-color-15: #ddcc77; /* sand */
|
||||
--room-color-16: #661100; /* brown */
|
||||
--room-color-17: #6699cc; /* steel blue */
|
||||
--room-color-18: #888888; /* gray */
|
||||
--room-color-19: #aa3377; /* magenta */
|
||||
--room-color-20: #44bb99; /* turquoise */
|
||||
--room-color-19: #44bb99; /* turquoise */
|
||||
--room-color-20: #000000; /* black */
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user