mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-10 01:55:48 -04:00
Adding more fixes to the system.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* @component
|
||||
* @returns {JSX.Element} App root UI
|
||||
*/
|
||||
|
||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* SSE payload for rooms availability stream.
|
||||
*/
|
||||
import type { RoomsAvailabilitySSEPayload } from "../interfaces";
|
||||
|
||||
/**
|
||||
* Connect to the rooms availability SSE stream.
|
||||
* @param url SSE endpoint URL
|
||||
@@ -22,13 +28,13 @@ export function connectRoomsAvailabilityStream({
|
||||
onError,
|
||||
}: {
|
||||
url?: string;
|
||||
onMessage: (data: any) => void;
|
||||
onError?: (error: any) => void;
|
||||
onMessage: (data: RoomsAvailabilitySSEPayload) => void;
|
||||
onError?: (error: unknown) => void;
|
||||
}) {
|
||||
const es = new window.EventSource(url);
|
||||
es.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
const data: RoomsAvailabilitySSEPayload = JSON.parse(event.data);
|
||||
onMessage(data);
|
||||
} catch (e) {
|
||||
if (onError) onError(e);
|
||||
|
||||
@@ -10,34 +10,37 @@
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
import axios from "axios";
|
||||
import type { User } from "../interfaces";
|
||||
|
||||
/**
|
||||
* Fetch all users (invitees).
|
||||
* @returns Array of users
|
||||
*/
|
||||
export async function getUsers() {
|
||||
const res = await axios.get("/users/");
|
||||
export async function getUsers(): Promise<User[]> {
|
||||
const res = await axios.get<User[]>("/users/");
|
||||
return res.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch available users for a given time slot.
|
||||
* @param start_time ISO string
|
||||
* @param end_time ISO string
|
||||
* @param exclude_booking_id Optional booking ID to exclude
|
||||
* @param params Object containing start_time, end_time, and optional exclude_booking_id
|
||||
* @returns Array of available users
|
||||
*/
|
||||
export async function getAvailableUsers({
|
||||
start_time,
|
||||
end_time,
|
||||
exclude_booking_id,
|
||||
}: {
|
||||
export async function getAvailableUsers(params: {
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
exclude_booking_id?: string | number;
|
||||
}) {
|
||||
const params: any = { start_time, end_time };
|
||||
if (exclude_booking_id) params.exclude_booking_id = exclude_booking_id;
|
||||
const res = await axios.get("/users/available/", { params });
|
||||
}): Promise<User[]> {
|
||||
const query: {
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
exclude_booking_id?: string | number;
|
||||
} = {
|
||||
start_time: params.start_time,
|
||||
end_time: params.end_time,
|
||||
};
|
||||
if (params.exclude_booking_id)
|
||||
query.exclude_booking_id = params.exclude_booking_id;
|
||||
const res = await axios.get<User[]>("/users/available/", { params: query });
|
||||
return res.data;
|
||||
}
|
||||
|
||||
@@ -5,19 +5,14 @@
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
*/
|
||||
/**
|
||||
* BookingConfirmation component.
|
||||
*
|
||||
* Displays booking confirmation details for a room reservation.
|
||||
*
|
||||
* @component
|
||||
* @param {BookingConfirmationProps} props - Component props
|
||||
* @returns {JSX.Element} Confirmation UI
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Box, Button } from "@mui/material";
|
||||
import type { BookingConfirmationProps } from "../schemas";
|
||||
import type { BookingConfirmationProps } from "../interfaces";
|
||||
|
||||
/**
|
||||
* BookingConfirmation displays a summary of a confirmed booking.
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
/**
|
||||
* BookingForm.tsx
|
||||
* BookingForm component
|
||||
* Form for creating or editing a conference room booking.
|
||||
* Handles validation, submission, and invitee selection.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* @returns {JSX.Element} Booking form UI
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
@@ -22,7 +20,7 @@ import { roundToStrictlyFutureQuarter } from "../utils/date";
|
||||
import { connectRoomsAvailabilityStream } from "../apis/sse";
|
||||
import { getInvitees } from "../apis/invitees";
|
||||
import { getAvailableUsers } from "../apis/users";
|
||||
import type { ConferenceRoom, Booking } from "../schemas";
|
||||
import type { ConferenceRoom, Booking } from "../interfaces";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -293,7 +291,9 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
}
|
||||
getAvailableUsers(params)
|
||||
.then((users) => {
|
||||
const emails = users.map((u: { email: string }) => u.email);
|
||||
const emails = users.map(
|
||||
(u: import("../interfaces").User) => u.email ?? ""
|
||||
);
|
||||
setAvailableInvitees(Array.from(new Set([...emails, ...invitees])));
|
||||
})
|
||||
.catch(() =>
|
||||
@@ -377,7 +377,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
const filtered = data.bookings.filter((b: Booking) => {
|
||||
const bookingDate = b.start_time?.slice(0, 10);
|
||||
return (
|
||||
String((b as any).room_id) === String(room_id) &&
|
||||
String(b.room_id) === String(room_id) &&
|
||||
(!dateStr || bookingDate === dateStr)
|
||||
);
|
||||
});
|
||||
@@ -612,9 +612,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
<RoomDetailsModal
|
||||
open={roomModalOpen}
|
||||
onClose={() => setRoomModalOpen(false)}
|
||||
room={rooms.find(
|
||||
(r: ConferenceRoom) => String(r.id) === String(room_id)
|
||||
)}
|
||||
room={
|
||||
rooms.find(
|
||||
(r: ConferenceRoom) => String(r.id) === String(room_id)
|
||||
) ?? rooms[0]
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
label="Title (optional)"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
/**
|
||||
* BookingList.tsx
|
||||
* BookingList component
|
||||
* Lists bookings for a selected room or user, including times and invitees.
|
||||
* Handles empty state and sorts bookings chronologically.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* @returns {JSX.Element} Booking list UI
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
@@ -14,7 +12,7 @@ import {
|
||||
formatBookingTime,
|
||||
getInviteeName,
|
||||
} from "../helpers/bookingList";
|
||||
import type { BookingListProps } from "../schemas";
|
||||
import type { BookingListProps } from "../interfaces";
|
||||
|
||||
/**
|
||||
* BookingList component.
|
||||
|
||||
@@ -21,42 +21,12 @@ import Typography from "@mui/material/Typography";
|
||||
import { getEventDisplayText, getRoomClass } from "../helpers/calendar";
|
||||
import { logger } from "../utils/logger";
|
||||
import "../styles/CalendarView.css";
|
||||
|
||||
// Custom event type for calendar events
|
||||
type BookingEvent = {
|
||||
id: string | number;
|
||||
title: string;
|
||||
start: Date | string;
|
||||
end: Date | string;
|
||||
color?: string;
|
||||
resource?: {
|
||||
room_id?: string | number;
|
||||
room_name?: string;
|
||||
invitees?: string[];
|
||||
originalBooking?: any;
|
||||
};
|
||||
};
|
||||
import type { BookingEvent } from "../types";
|
||||
|
||||
/**
|
||||
* Props for CalendarView.
|
||||
*/
|
||||
interface SlotInfo {
|
||||
start: Date;
|
||||
end: Date;
|
||||
allDay: boolean;
|
||||
viewType: string;
|
||||
selectedRoomId?: string | number;
|
||||
}
|
||||
|
||||
interface CalendarViewProps {
|
||||
events: BookingEvent[];
|
||||
onEventClick?: (event: BookingEvent) => void;
|
||||
onSlotSelect?: (slotInfo: SlotInfo) => void;
|
||||
selectedRoomId?: string | number;
|
||||
officeStartHour?: number;
|
||||
officeEndHour?: number;
|
||||
initialDate?: string;
|
||||
}
|
||||
import type { SlotInfo, CalendarViewProps } from "../interfaces";
|
||||
|
||||
// Read business hours from env (default 08:00-18:00)
|
||||
const businessStart = process.env.BUSINESS_START || "08:00";
|
||||
@@ -72,22 +42,14 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
// Custom event content with tooltip
|
||||
const renderEventContent = (arg: { event: BookingEvent }) => {
|
||||
const { event } = arg;
|
||||
const { title, start, end, resource } = event;
|
||||
// Robust room name extraction
|
||||
const room: string =
|
||||
typeof resource?.room_name === "string" &&
|
||||
resource.room_name.trim() !== ""
|
||||
? resource.room_name
|
||||
: typeof resource?.room_id === "string" &&
|
||||
resource.room_id.trim() !== ""
|
||||
? resource.room_id
|
||||
: typeof resource?.room_id === "number"
|
||||
? String(resource.room_id)
|
||||
: resource?.originalBooking?.room?.name
|
||||
? resource.originalBooking.room.name
|
||||
: "Room";
|
||||
const invitees: string[] =
|
||||
resource && Array.isArray(resource.invitees) ? resource.invitees : [];
|
||||
const title = event.title;
|
||||
const start = event.start;
|
||||
const end = event.end;
|
||||
const room_id = event.extendedProps?.room_id;
|
||||
const room: string = event.extendedProps?.room_name ?? "Room";
|
||||
const invitees: string[] = Array.isArray(event.extendedProps?.invitees)
|
||||
? (event.extendedProps?.invitees as string[])
|
||||
: [];
|
||||
const safeTitle: string = typeof title === "string" ? title : "";
|
||||
const safeStart: Date | null = start
|
||||
? new Date(start as string | number | Date)
|
||||
@@ -100,7 +62,9 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
safeStart ?? new Date(),
|
||||
room
|
||||
);
|
||||
const roomClass = getRoomClass(resource?.room_id ?? null);
|
||||
const roomClass = getRoomClass(
|
||||
typeof room_id === "number" ? room_id : parseInt(room_id as string, 10)
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
title={
|
||||
@@ -147,7 +111,6 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
borderRadius: 4,
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
background: undefined, // Remove inline background to allow CSS class to control color
|
||||
}}
|
||||
>
|
||||
<span
|
||||
@@ -205,13 +168,8 @@ const CalendarView: React.FC<CalendarViewProps> = ({
|
||||
eventClick={(info) => {
|
||||
info.jsEvent.preventDefault();
|
||||
if (onEventClick) {
|
||||
// Pass the full event object with custom properties
|
||||
const eventObj =
|
||||
info.event.extendedProps &&
|
||||
Object.keys(info.event.extendedProps).length > 0
|
||||
? { ...info.event, ...info.event.extendedProps }
|
||||
: info.event;
|
||||
onEventClick(eventObj as BookingEvent);
|
||||
// Pass the full event object
|
||||
onEventClick(info.event as unknown as BookingEvent);
|
||||
}
|
||||
}}
|
||||
selectable={true}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* RoomDetailsModal.tsx
|
||||
* Modal dialog for displaying details about a conference room.
|
||||
* Used in booking page for room info popups.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* @component
|
||||
* @returns {JSX.Element} Room details modal UI
|
||||
*/
|
||||
/**
|
||||
* RoomDetailsModal.tsx
|
||||
* Modal dialog for displaying details about a conference room.
|
||||
@@ -17,7 +27,7 @@ import {
|
||||
Typography,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import type { RoomDetailsModalProps } from "../schemas";
|
||||
import type { RoomDetailsModalProps } from "../interfaces";
|
||||
|
||||
const RoomDetailsModal: React.FC<RoomDetailsModalProps> = ({
|
||||
open,
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
/**
|
||||
* RoomList.tsx
|
||||
* RoomList component
|
||||
* Displays a list of available conference rooms, highlighting the selected room.
|
||||
* Used in the landing page for room selection.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
*/
|
||||
/**
|
||||
* RoomList component.
|
||||
* Displays a list of available conference rooms. Selected room is highlighted.
|
||||
* @returns {JSX.Element} Room list UI
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
@@ -24,7 +18,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import MeetingRoomIcon from "@mui/icons-material/MeetingRoom";
|
||||
import { getRoomListItemStyles, getRoomSecondaryText } from "../helpers/room";
|
||||
import type { RoomListProps } from "../schemas";
|
||||
import type { RoomListProps } from "../interfaces";
|
||||
|
||||
/**
|
||||
* Renders a list of conference rooms with their details.
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
// booking.ts
|
||||
// Utility functions for booking-related logic in booking system frontend
|
||||
/**
|
||||
* booking.ts
|
||||
* Utility functions for booking-related logic in booking system frontend.
|
||||
* Provides helpers for extracting room IDs and handling booking objects.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
*/
|
||||
|
||||
// Minimal Booking type for utility functions
|
||||
import type { Booking } from "../schemas";
|
||||
import type { Booking } from "../interfaces";
|
||||
|
||||
/**
|
||||
* Extracts the room ID from a booking object, handling multiple possible shapes.
|
||||
@@ -10,51 +16,9 @@ import type { Booking } from "../schemas";
|
||||
export function getEditBookingRoomId(
|
||||
editBooking: Booking | undefined | null
|
||||
): string {
|
||||
if (!editBooking || typeof editBooking !== "object") return "";
|
||||
let id: unknown = "";
|
||||
// Safely check for non-standard properties using type guards
|
||||
if ("_def" in editBooking && typeof editBooking._def === "object") {
|
||||
const ext = (editBooking._def as any).extendedProps;
|
||||
if (ext && ext.resource) {
|
||||
id = ext.resource.room_id ?? ext.resource.id ?? id;
|
||||
}
|
||||
}
|
||||
if (
|
||||
!id &&
|
||||
"resource" in editBooking &&
|
||||
typeof (editBooking as any).resource === "object"
|
||||
) {
|
||||
id = (editBooking as any).resource.room_id ?? id;
|
||||
id = (editBooking as any).resource.id ?? id;
|
||||
}
|
||||
if (!id && "room_id" in editBooking) {
|
||||
id = (editBooking as any).room_id ?? id;
|
||||
}
|
||||
if (
|
||||
!id &&
|
||||
"room" in editBooking &&
|
||||
typeof (editBooking as any).room === "object"
|
||||
) {
|
||||
id = (editBooking as any).room.id ?? id;
|
||||
}
|
||||
if (!id && "roomId" in editBooking) {
|
||||
id = (editBooking as any).roomId ?? id;
|
||||
}
|
||||
// Fallback: search for any property containing 'room' and is a number
|
||||
if (!id) {
|
||||
for (const k of Object.keys(editBooking)) {
|
||||
// @ts-ignore
|
||||
if (
|
||||
k.toLowerCase().includes("room") &&
|
||||
typeof (editBooking as any)[k] === "number"
|
||||
) {
|
||||
// @ts-ignore
|
||||
id = (editBooking as any)[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return id !== undefined && id !== null ? String(id) : "";
|
||||
if (!editBooking) return "";
|
||||
// Only support standard Booking shape
|
||||
return editBooking.room_id ? String(editBooking.room_id) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
// bookingList.ts
|
||||
// Utility functions for BookingList component
|
||||
/**
|
||||
* bookingList.ts
|
||||
* Utility functions for BookingList component.
|
||||
* Provides helpers for sorting bookings and formatting invitee names.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a booking object with a start_time property.
|
||||
*/
|
||||
import type { Booking as SharedBooking } from "../schemas";
|
||||
import type { BookingType } from "../types";
|
||||
import type { Invitee } from "../interfaces";
|
||||
|
||||
export type Booking = SharedBooking;
|
||||
export type Booking = BookingType;
|
||||
|
||||
/**
|
||||
* Sorts bookings by their start time (ascending).
|
||||
@@ -39,12 +46,6 @@ export function formatBookingTime(dateStr: string): string {
|
||||
/**
|
||||
* Represents an invitee object.
|
||||
*/
|
||||
export interface Invitee {
|
||||
name?: string;
|
||||
username?: string;
|
||||
displayName?: string;
|
||||
user?: { name?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the display name for an invitee.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
// calendar.ts
|
||||
// Utility functions for CalendarView and booking system frontend
|
||||
/**
|
||||
* calendar.ts
|
||||
* Utility functions for CalendarView and booking system frontend.
|
||||
* Provides helpers for event display and room color assignment.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns display text for a calendar event, including start time and title/room.
|
||||
@@ -37,16 +43,10 @@ export function getEventDisplayText(
|
||||
|
||||
/**
|
||||
* Returns a CSS class for a room color, based on room ID.
|
||||
* @param roomId - Room ID (string, number, or null)
|
||||
* @param roomId - Room ID number
|
||||
* @returns CSS class string
|
||||
*/
|
||||
export function getRoomClass(roomId: string | number | null): string {
|
||||
let idx = 0;
|
||||
if (typeof roomId === "number") {
|
||||
idx = roomId % 8;
|
||||
} else if (typeof roomId === "string" && roomId !== "") {
|
||||
const parsed = parseInt(roomId, 10);
|
||||
idx = isNaN(parsed) ? 0 : parsed % 8;
|
||||
}
|
||||
export function getRoomClass(roomId: number): string {
|
||||
let idx = roomId ? roomId % 20 : 0;
|
||||
return `room-color-${idx}`;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
// room.ts
|
||||
// Utility functions for RoomList component
|
||||
import { ConferenceRoom } from "../schemas";
|
||||
/**
|
||||
* room.ts
|
||||
* Utility functions for RoomList component.
|
||||
* Provides helpers for rendering room details and styles.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
*/
|
||||
import { ConferenceRoom } from "../interfaces";
|
||||
import type { RoomListItemStyles } from "../interfaces";
|
||||
|
||||
export function getRoomSecondaryText(room: ConferenceRoom): string {
|
||||
return `Location: ${room.location} | Capacity: ${room.capacity} | Equipment: ${room.equipment}`;
|
||||
}
|
||||
|
||||
interface RoomListItemStyles {
|
||||
backgroundColor?: string;
|
||||
fontWeight?: number;
|
||||
color: string;
|
||||
border?: string;
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
export function getRoomListItemStyles(
|
||||
roomId: number,
|
||||
selectedRoomId?: number
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// validation.ts
|
||||
// Independent field validation helpers for BookingForm and booking system frontend
|
||||
/**
|
||||
* validation.ts
|
||||
* Independent field validation helpers for BookingForm and booking system frontend.
|
||||
* Provides functions for validating room, time, and invitee fields.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
*/
|
||||
|
||||
import type { Booking } from "../schemas";
|
||||
import type { Booking } from "../interfaces";
|
||||
|
||||
/**
|
||||
* Validates room ID field.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Global CSS for the React app, including font and code styles.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
/*
|
||||
* Global Styles
|
||||
|
||||
127
frontend/src/interfaces.ts
Normal file
127
frontend/src/interfaces.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* interfaces.ts
|
||||
* Centralized TypeScript interfaces for frontend application.
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-09
|
||||
*/
|
||||
|
||||
/**
|
||||
* SlotInfo for calendar and booking forms.
|
||||
*/
|
||||
export interface SlotInfo {
|
||||
start: Date;
|
||||
end: Date;
|
||||
room_id?: string | number;
|
||||
allDay?: boolean;
|
||||
viewType?: string;
|
||||
selectedRoomId?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for CalendarView component.
|
||||
*/
|
||||
export interface CalendarViewProps {
|
||||
events: import("./types").BookingEvent[];
|
||||
onEventClick?: (event: import("./types").BookingEvent) => void;
|
||||
onSlotSelect?: (slotInfo: SlotInfo) => void;
|
||||
selectedRoomId?: string | number;
|
||||
officeStartHour?: number;
|
||||
officeEndHour?: number;
|
||||
initialDate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for BookingForm component.
|
||||
*/
|
||||
export interface BookingFormProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
slotInfo: SlotInfo;
|
||||
rooms: ConferenceRoom[];
|
||||
editBooking?: Booking;
|
||||
allInvitees: string[];
|
||||
onBookingSuccess?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invitee object for booking lists.
|
||||
*/
|
||||
export interface Invitee {
|
||||
name?: string;
|
||||
username?: string;
|
||||
displayName?: string;
|
||||
user?: { name?: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* RoomListItemStyles for room list rendering.
|
||||
*/
|
||||
export interface RoomListItemStyles {
|
||||
backgroundColor?: string;
|
||||
fontWeight?: number;
|
||||
color: string;
|
||||
border?: string;
|
||||
borderRadius?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE payload for rooms availability stream.
|
||||
*/
|
||||
export interface RoomsAvailabilitySSEPayload {
|
||||
bookings: Booking[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
export interface User {
|
||||
id: string | number;
|
||||
name: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface Booking {
|
||||
id: string;
|
||||
room_id?: number | string;
|
||||
room?: { name?: string };
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
start?: string | number | Date;
|
||||
end?: string | number | Date;
|
||||
title?: string;
|
||||
invitees?: string[];
|
||||
}
|
||||
|
||||
export interface ConferenceRoom {
|
||||
id: number;
|
||||
name: string;
|
||||
location: string;
|
||||
equipment: string;
|
||||
capacity: number;
|
||||
}
|
||||
|
||||
export interface BookingConfirmationProps {
|
||||
booking: Booking & {
|
||||
room?: { name?: string };
|
||||
invitees?: string[];
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
title?: string;
|
||||
};
|
||||
onEdit: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export interface RoomDetailsModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
room: ConferenceRoom;
|
||||
}
|
||||
|
||||
export interface BookingListProps {
|
||||
bookings: Booking[];
|
||||
onSelect?: (booking: Booking) => void;
|
||||
}
|
||||
|
||||
export interface RoomListProps {
|
||||
rooms: ConferenceRoom[];
|
||||
selectedRoomId?: number;
|
||||
onSelectRoom?: (roomId: number) => void;
|
||||
}
|
||||
@@ -5,9 +5,11 @@
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* @component
|
||||
* @returns {JSX.Element} Booking page UI
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState, useEffect, useRef } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Button,
|
||||
@@ -26,58 +28,90 @@ import CalendarView from "../components/CalendarView";
|
||||
import BookingForm from "../components/BookingForm";
|
||||
import RoomDetailsModal from "../components/RoomDetailsModal";
|
||||
import { logger } from "../utils/logger";
|
||||
import type { Booking, ConferenceRoom, User } from "../interfaces";
|
||||
import type { BookingEvent } from "../types";
|
||||
|
||||
/**
|
||||
* BookingPage component
|
||||
* Renders the booking calendar, handles room selection, booking creation, and SSE updates.
|
||||
* @returns {JSX.Element} Booking page UI
|
||||
*/
|
||||
const BookingPage: React.FC = () => {
|
||||
// State for booking form
|
||||
const [formOpen, setFormOpen] = useState(false);
|
||||
// State for room details modal (page-level)
|
||||
const [roomModalOpen, setRoomModalOpen] = useState(false);
|
||||
// Log mount
|
||||
// Log mount/unmount for analytics
|
||||
/**
|
||||
* Track mount/unmount for analytics.
|
||||
*/
|
||||
useEffect(() => {
|
||||
logger.info("[BookingPage] Mounted");
|
||||
return () => logger.info("[BookingPage] Unmounted");
|
||||
}, []);
|
||||
// Fetch all rooms
|
||||
/**
|
||||
* Fetch all rooms from backend.
|
||||
*/
|
||||
const {
|
||||
data: rooms = [],
|
||||
isLoading: roomsLoading,
|
||||
error: roomsError,
|
||||
}: {
|
||||
data?: ConferenceRoom[];
|
||||
isLoading: boolean;
|
||||
error?: unknown;
|
||||
} = useQuery({
|
||||
queryKey: ["rooms"],
|
||||
queryFn: () => getRooms(),
|
||||
});
|
||||
|
||||
// Real-time bookings state (by room)
|
||||
const [bookingsByRoom, setBookingsByRoom] = useState<Record<number, any[]>>(
|
||||
{}
|
||||
);
|
||||
/**
|
||||
* Real-time bookings state, grouped by room id.
|
||||
*/
|
||||
const [bookingsByRoom, setBookingsByRoom] = useState<
|
||||
Record<number, Booking[]>
|
||||
>({});
|
||||
|
||||
// Fetch all users (invitees)
|
||||
/**
|
||||
* Fetch all users (invitees).
|
||||
*/
|
||||
const {
|
||||
data: allInvitees = [],
|
||||
isLoading: inviteesLoading,
|
||||
error: inviteesError,
|
||||
}: {
|
||||
data?: import("../interfaces").User[];
|
||||
isLoading: boolean;
|
||||
error?: unknown;
|
||||
} = useQuery({
|
||||
queryKey: ["users"],
|
||||
queryFn: () => getUsers(),
|
||||
});
|
||||
const [bookingsLoading, setBookingsLoading] = useState(true);
|
||||
const [bookingsError, setBookingsError] = useState<any>(null);
|
||||
const [bookingsError, setBookingsError] = useState<unknown>(null);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
const [usingPollingFallback, setUsingPollingFallback] = useState(false);
|
||||
|
||||
// Fetch initial bookings and subscribe to SSE
|
||||
/**
|
||||
* Fetch initial bookings and subscribe to SSE or polling fallback.
|
||||
*/
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let pollInterval: NodeJS.Timeout | null = null;
|
||||
/**
|
||||
* Fetch bookings for all rooms and update state.
|
||||
*/
|
||||
async function fetchBookingsAndSet() {
|
||||
setBookingsLoading(true);
|
||||
setBookingsError(null);
|
||||
try {
|
||||
const results: Record<number, any[]> = {};
|
||||
const results: Record<number, Booking[]> = {};
|
||||
await Promise.all(
|
||||
rooms.map(async (room: any) => {
|
||||
rooms.map(async (room: ConferenceRoom) => {
|
||||
try {
|
||||
const bookings = await getRoomBookings(room.id);
|
||||
const bookings: Booking[] = await getRoomBookings(room.id);
|
||||
results[room.id] = bookings;
|
||||
logger.debug(
|
||||
`[BookingPage] Bookings fetched for room ${room.id}`,
|
||||
@@ -103,7 +137,7 @@ const BookingPage: React.FC = () => {
|
||||
if (rooms.length > 0) {
|
||||
fetchBookingsAndSet();
|
||||
logger.info("[BookingPage] Fetching bookings for all rooms");
|
||||
// Fallback: If EventSource/SSE is not supported, use polling
|
||||
// Use SSE if available, otherwise fallback to polling
|
||||
const sseAllowed =
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.EventSource !== "undefined";
|
||||
@@ -113,10 +147,11 @@ const BookingPage: React.FC = () => {
|
||||
eventSourceRef.current = connectRoomsAvailabilityStream({
|
||||
onMessage: (data) => {
|
||||
if (Array.isArray(data.bookings)) {
|
||||
const grouped: Record<number, any[]> = {};
|
||||
data.bookings.forEach((b: any) => {
|
||||
if (!grouped[b.room_id]) grouped[b.room_id] = [];
|
||||
grouped[b.room_id].push(b);
|
||||
const grouped: Record<number, Booking[]> = {};
|
||||
data.bookings.forEach((b: Booking) => {
|
||||
if (!grouped[b.room_id as number])
|
||||
grouped[b.room_id as number] = [];
|
||||
grouped[b.room_id as number].push(b);
|
||||
});
|
||||
setBookingsByRoom(grouped);
|
||||
logger.debug("[BookingPage] SSE update received", grouped);
|
||||
@@ -143,107 +178,84 @@ const BookingPage: React.FC = () => {
|
||||
};
|
||||
}, [rooms, formOpen, roomModalOpen]);
|
||||
|
||||
// Assign a color to each room
|
||||
const roomColors: Record<string, string> = useMemo(() => {
|
||||
const defaultColors = [
|
||||
"#1976d2",
|
||||
"#388e3c",
|
||||
"#fbc02d",
|
||||
"#d32f2f",
|
||||
"#7b1fa2",
|
||||
"#0288d1",
|
||||
"#c2185b",
|
||||
"#ffa000",
|
||||
];
|
||||
const colors: Record<string, string> = {};
|
||||
rooms.forEach((room: any, idx: number) => {
|
||||
colors[room.id] = defaultColors[idx % defaultColors.length];
|
||||
});
|
||||
return colors;
|
||||
}, [rooms]);
|
||||
// Removed unused roomColors variable
|
||||
|
||||
// Combine all bookings into calendar events, sorted chronologically (room id tiebreaker), with color and formatted title
|
||||
// Expose allBookings for event lookup
|
||||
const allBookings: any[] = React.useMemo(() => {
|
||||
/**
|
||||
* Flatten all bookings from all rooms into a single array.
|
||||
*/
|
||||
const allBookings: Booking[] = React.useMemo(() => {
|
||||
if (!bookingsByRoom || rooms.length === 0) return [];
|
||||
return Object.entries(bookingsByRoom).flatMap(
|
||||
([roomId, bookings]) => bookings as any[]
|
||||
([roomId, bookings]) => bookings as Booking[]
|
||||
);
|
||||
}, [bookingsByRoom, rooms]);
|
||||
|
||||
const events: any[] = React.useMemo(() => {
|
||||
/**
|
||||
* Map all bookings to calendar event objects for display.
|
||||
*/
|
||||
const events = React.useMemo(() => {
|
||||
if (!bookingsByRoom || rooms.length === 0) return [];
|
||||
// Map allBookings to calendar events
|
||||
const mappedEvents = Object.entries(bookingsByRoom).flatMap(
|
||||
([roomId, bookings]) =>
|
||||
(bookings as any[]).map((booking) => {
|
||||
(bookings as Booking[]).map((booking) => {
|
||||
logger.info("[BookingPage] Event mapping diagnostic", {
|
||||
bookingId: booking.id,
|
||||
bookingRoomId: booking.room_id,
|
||||
roomIdFromGroup: roomId,
|
||||
});
|
||||
// Normalize roomId for lookup
|
||||
const normalizedRoomId = String(roomId);
|
||||
const room = rooms.find(
|
||||
(r: any) => String(r.id) === normalizedRoomId
|
||||
(r: ConferenceRoom) => String(r.id) === normalizedRoomId
|
||||
);
|
||||
let roomName = room?.name;
|
||||
if (!roomName || roomName.trim() === "") {
|
||||
if (
|
||||
typeof booking.room_name === "string" &&
|
||||
booking.room_name.trim() !== ""
|
||||
typeof booking.room?.name === "string" &&
|
||||
booking.room?.name.trim() !== ""
|
||||
) {
|
||||
roomName = booking.room_name;
|
||||
roomName = booking.room?.name;
|
||||
} else if (
|
||||
typeof booking.room_id === "string" &&
|
||||
booking.room_id.trim() !== ""
|
||||
) {
|
||||
roomName = booking.room_id;
|
||||
roomName = booking.room_id as string;
|
||||
} else if (typeof booking.room_id === "number") {
|
||||
roomName = String(booking.room_id);
|
||||
} else {
|
||||
roomName = "Room";
|
||||
}
|
||||
}
|
||||
const eventColor = roomColors[normalizedRoomId] || "#1976d2";
|
||||
const inviteeNames = booking.invitees ?? [];
|
||||
const start = new Date(booking.start_time);
|
||||
const end = new Date(booking.end_time);
|
||||
const displayTitle =
|
||||
booking.title && booking.title.trim() !== ""
|
||||
? booking.title
|
||||
: roomName;
|
||||
let inviteeNames: string[] = [];
|
||||
if (Array.isArray(booking.invitees)) {
|
||||
inviteeNames = booking.invitees
|
||||
.map((inv: any) => inv?.user?.name)
|
||||
.filter((name: string | undefined) => !!name);
|
||||
}
|
||||
return {
|
||||
id: booking.id,
|
||||
title: displayTitle,
|
||||
start,
|
||||
end,
|
||||
color: eventColor,
|
||||
backgroundColor: eventColor,
|
||||
resource: {
|
||||
room_id: booking.room_id,
|
||||
room_name: roomName,
|
||||
invitees: inviteeNames,
|
||||
originalBooking: booking,
|
||||
},
|
||||
title: booking.title ?? "",
|
||||
start: start ?? new Date(booking.start_time),
|
||||
end: end ?? new Date(booking.end_time),
|
||||
room_id: booking.room_id,
|
||||
room_name: roomName,
|
||||
invitees: inviteeNames,
|
||||
originalBooking: booking,
|
||||
};
|
||||
})
|
||||
);
|
||||
mappedEvents.sort((a, b) => {
|
||||
const aStart = a.start.getTime();
|
||||
const bStart = b.start.getTime();
|
||||
if (aStart !== bStart) return aStart - bStart;
|
||||
return String(a.resource.room_id).localeCompare(
|
||||
String(b.resource.room_id)
|
||||
);
|
||||
});
|
||||
return mappedEvents;
|
||||
}, [bookingsByRoom, rooms, roomColors]);
|
||||
}, [bookingsByRoom, rooms]);
|
||||
// Removed duplicate and broken code block
|
||||
|
||||
const [formSlot, setFormSlot] = useState<any>(null);
|
||||
const [editBooking, setEditBooking] = useState<any>(null);
|
||||
interface SlotInfo {
|
||||
start: Date;
|
||||
end: Date;
|
||||
room_id?: string | number;
|
||||
}
|
||||
const [formSlot, setFormSlot] = useState<SlotInfo | null>(null);
|
||||
const [editBooking, setEditBooking] = useState<Booking | null>(null);
|
||||
// Default to first room, always require a room to be selected
|
||||
const [selectedRoomId, setSelectedRoomId] = useState<any>("");
|
||||
const [selectedRoomId, setSelectedRoomId] = useState<string | number>("");
|
||||
|
||||
// Set default room when rooms load
|
||||
useEffect(() => {
|
||||
@@ -253,7 +265,9 @@ const BookingPage: React.FC = () => {
|
||||
}, [rooms, selectedRoomId]);
|
||||
|
||||
// Handle slot selection from calendar (month/week/day)
|
||||
const handleSelectSlot = (slotInfo: any) => {
|
||||
const handleSelectSlot = (
|
||||
slotInfo: SlotInfo & { viewType?: string; start?: Date | string }
|
||||
) => {
|
||||
logger.debug(
|
||||
"[BookingPage] Slot selected",
|
||||
slotInfo,
|
||||
@@ -311,7 +325,7 @@ const BookingPage: React.FC = () => {
|
||||
|
||||
// 2. Find all bookings for this room on this day
|
||||
const roomBookings = events.filter((e) => {
|
||||
if (e.resource?.room_id !== selectedRoomId) return false;
|
||||
if (e.room_id !== selectedRoomId) return false;
|
||||
const eventStart =
|
||||
e.start &&
|
||||
(typeof e.start === "string" ||
|
||||
@@ -381,23 +395,27 @@ const BookingPage: React.FC = () => {
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const handleSelectEvent = (event: any) => {
|
||||
const originalBooking = event.resource?.originalBooking;
|
||||
const handleSelectEvent = (event: BookingEvent) => {
|
||||
const originalBooking = event.originalBooking;
|
||||
const eventIdStr = String(event.id);
|
||||
if (originalBooking && originalBooking.id) {
|
||||
setEditBooking(originalBooking);
|
||||
} else {
|
||||
// Try to find backend booking by event.id using allBookings (compare as strings)
|
||||
const foundBooking = allBookings.find(
|
||||
(b: any) => String(b.id) === eventIdStr
|
||||
(b: Booking) => String(b.id) === eventIdStr
|
||||
);
|
||||
if (foundBooking) {
|
||||
setEditBooking(foundBooking);
|
||||
} else {
|
||||
setEditBooking({
|
||||
...event,
|
||||
start_time: event.start,
|
||||
end_time: event.end,
|
||||
id: String(event.id),
|
||||
room_id: event.room_id,
|
||||
room: event.room_name ? { name: event.room_name } : undefined,
|
||||
start_time: event.start ? event.start.toString() : "",
|
||||
end_time: event.end ? event.end.toString() : "",
|
||||
title: event.title ?? "",
|
||||
invitees: event.invitees ?? [],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -408,10 +426,10 @@ const BookingPage: React.FC = () => {
|
||||
const refetchBookings = async () => {
|
||||
setBookingsLoading(true);
|
||||
setBookingsError(null);
|
||||
const results: Record<number, any[]> = {};
|
||||
const results: Record<number, Booking[]> = {};
|
||||
try {
|
||||
await Promise.all(
|
||||
rooms.map(async (room: any) => {
|
||||
rooms.map(async (room: ConferenceRoom) => {
|
||||
try {
|
||||
const bookings = await getRoomBookings(room.id);
|
||||
results[room.id] = bookings;
|
||||
@@ -488,7 +506,7 @@ const BookingPage: React.FC = () => {
|
||||
label="Room"
|
||||
onChange={(e) => setSelectedRoomId(e.target.value)}
|
||||
>
|
||||
{rooms.map((room: any) => (
|
||||
{rooms.map((room: ConferenceRoom) => (
|
||||
<MenuItem key={room.id} value={room.id}>
|
||||
{room.name}
|
||||
</MenuItem>
|
||||
@@ -507,11 +525,13 @@ const BookingPage: React.FC = () => {
|
||||
<RoomDetailsModal
|
||||
open={roomModalOpen}
|
||||
onClose={() => setRoomModalOpen(false)}
|
||||
room={rooms.find((r: any) => r.id === selectedRoomId)}
|
||||
room={
|
||||
rooms.find((r: ConferenceRoom) => r.id === selectedRoomId) ?? rooms[0]
|
||||
}
|
||||
/>
|
||||
<CalendarView
|
||||
events={events as any}
|
||||
onEventClick={(event: any) => handleSelectEvent(event)}
|
||||
events={events}
|
||||
onEventClick={handleSelectEvent}
|
||||
onSlotSelect={handleSelectSlot}
|
||||
selectedRoomId={selectedRoomId}
|
||||
/>
|
||||
@@ -519,10 +539,18 @@ const BookingPage: React.FC = () => {
|
||||
<BookingForm
|
||||
open={formOpen}
|
||||
onClose={() => handleFormClose(true)}
|
||||
slotInfo={formSlot}
|
||||
slotInfo={
|
||||
formSlot ?? {
|
||||
start: new Date(),
|
||||
end: new Date(),
|
||||
room_id: selectedRoomId,
|
||||
}
|
||||
}
|
||||
rooms={rooms}
|
||||
editBooking={editBooking}
|
||||
allInvitees={allInvitees.map((u: any) => u.email)}
|
||||
editBooking={editBooking ?? undefined}
|
||||
allInvitees={allInvitees.map(
|
||||
(u: import("../interfaces").User) => u.email ?? u.name
|
||||
)}
|
||||
onBookingSuccess={() => handleFormClose(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* @component
|
||||
* @returns {JSX.Element} Confirmation page UI
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import type { Booking } from "../interfaces";
|
||||
import { Box } from "@mui/material";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -16,23 +19,38 @@ import { connectRoomsAvailabilityStream } from "../apis/sse";
|
||||
import BookingConfirmation from "../components/BookingConfirmation";
|
||||
import BookingForm from "../components/BookingForm";
|
||||
|
||||
// This page expects booking data to be passed via location.state
|
||||
/**
|
||||
* ConfirmationPage component
|
||||
* Shows booking confirmation and allows editing or returning to booking form.
|
||||
* Expects booking data via location.state.
|
||||
* @returns {JSX.Element} Confirmation page UI
|
||||
*/
|
||||
const ConfirmationPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
// booking is passed from BookingForm or BookingPage
|
||||
|
||||
const [booking, setBooking] = useState<any>(location.state?.booking || null);
|
||||
// Subscribe to SSE for live booking status
|
||||
/**
|
||||
* Booking state, updated via SSE for live status.
|
||||
*/
|
||||
const [booking, setBooking] = useState<Booking | null>(
|
||||
location.state?.booking || null
|
||||
);
|
||||
/**
|
||||
* Subscribe to SSE for live booking status updates.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (!booking) return;
|
||||
const es = connectRoomsAvailabilityStream({
|
||||
onMessage: (data) => {
|
||||
if (Array.isArray(data.bookings)) {
|
||||
// Find the current booking by id
|
||||
const updated = data.bookings.find((b: any) => b.id === booking.id);
|
||||
const updated = data.bookings.find(
|
||||
(b: Booking) => b.id === booking?.id
|
||||
);
|
||||
if (updated) {
|
||||
setBooking((prev: any) => ({ ...prev, ...updated }));
|
||||
setBooking((prev: Booking | null) =>
|
||||
prev ? { ...prev, ...updated } : updated
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -44,9 +62,15 @@ const ConfirmationPage: React.FC = () => {
|
||||
es.close();
|
||||
};
|
||||
}, [booking, booking?.id]);
|
||||
/**
|
||||
* Editing state for toggling between confirmation and edit form.
|
||||
*/
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
// Fetch all rooms for editing
|
||||
/**
|
||||
* Fetch all rooms for editing.
|
||||
*/
|
||||
const {
|
||||
data: rooms = [],
|
||||
isLoading: roomsLoading,
|
||||
@@ -88,7 +112,7 @@ const ConfirmationPage: React.FC = () => {
|
||||
slotInfo={{
|
||||
start: booking.start_time,
|
||||
end: booking.end_time,
|
||||
room_id: booking.room?.id,
|
||||
room_id: booking.room_id,
|
||||
}}
|
||||
rooms={rooms}
|
||||
editBooking={{
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* @component
|
||||
* @returns {JSX.Element} Landing page UI
|
||||
*/
|
||||
|
||||
import React, { FC, useEffect, useState } from "react";
|
||||
@@ -24,8 +26,8 @@ import { getRooms } from "../apis/rooms";
|
||||
import { connectRoomsAvailabilityStream } from "../apis/sse";
|
||||
import RoomList from "../components/RoomList";
|
||||
import BookingList from "../components/BookingList";
|
||||
import type { ConferenceRoom } from "../schemas";
|
||||
import type { Booking } from "../schemas";
|
||||
import type { ConferenceRoom } from "../interfaces";
|
||||
import type { Booking } from "../interfaces";
|
||||
import { logger } from "../utils/logger";
|
||||
// import "./LandingPage.css";
|
||||
|
||||
@@ -37,8 +39,17 @@ import { logger } from "../utils/logger";
|
||||
* - Navigation to booking form
|
||||
* Handles data fetching, error states, and loading skeletons.
|
||||
*/
|
||||
/**
|
||||
* LandingPage component
|
||||
* Renders the main UI for conference room booking, including available rooms, today's bookings, and navigation to booking form.
|
||||
* Handles data fetching, error states, and loading skeletons.
|
||||
* @returns {JSX.Element} Landing page UI
|
||||
*/
|
||||
const LandingPage: FC = () => {
|
||||
// Log when LandingPage mounts/unmounts
|
||||
/**
|
||||
* Log when LandingPage mounts/unmounts.
|
||||
*/
|
||||
useEffect(() => {
|
||||
logger.info("[LandingPage] Mounted");
|
||||
return () => {
|
||||
@@ -47,6 +58,9 @@ const LandingPage: FC = () => {
|
||||
}, []);
|
||||
|
||||
// Debug: log before page unload
|
||||
/**
|
||||
* Debug: log before page unload.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const handler = (e: BeforeUnloadEvent) => {
|
||||
// ...removed debug log...
|
||||
@@ -57,6 +71,9 @@ const LandingPage: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Fetch available rooms from backend
|
||||
/**
|
||||
* Fetch available rooms from backend.
|
||||
*/
|
||||
const {
|
||||
data: rooms = [],
|
||||
isLoading: roomsLoading,
|
||||
@@ -99,9 +116,11 @@ const LandingPage: FC = () => {
|
||||
onMessage: (data) => {
|
||||
if (Array.isArray(data.bookings)) {
|
||||
const grouped: Record<number, Booking[]> = {};
|
||||
data.bookings.forEach((b: any) => {
|
||||
if (!grouped[b.room_id]) grouped[b.room_id] = [];
|
||||
grouped[b.room_id].push(b);
|
||||
data.bookings.forEach((b: Booking) => {
|
||||
if (typeof b.room_id === "undefined" || b.room_id === null) return;
|
||||
const roomId = Number(b.room_id);
|
||||
if (!grouped[roomId]) grouped[roomId] = [];
|
||||
grouped[roomId].push(b);
|
||||
});
|
||||
setBookingsByRoom(grouped);
|
||||
logger.debug("[LandingPage] SSE update received", grouped);
|
||||
|
||||
7
frontend/src/react-app-env.d.ts
vendored
7
frontend/src/react-app-env.d.ts
vendored
@@ -1 +1,8 @@
|
||||
/**
|
||||
* react-app-env.d.ts
|
||||
* TypeScript environment declarations for React scripts.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
/// <reference types="react-scripts" />
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* reportWebVitals.ts
|
||||
* Web Vitals performance reporting for React app.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
import { ReportHandler } from "web-vitals";
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* Represents a user/invitee.
|
||||
*/
|
||||
export interface User {
|
||||
id: string | number;
|
||||
name: string;
|
||||
email?: string;
|
||||
}
|
||||
/**
|
||||
* schemas.ts
|
||||
* Centralized TypeScript interfaces for backend data schemas used in the frontend.
|
||||
@@ -11,9 +19,12 @@
|
||||
*/
|
||||
export interface Booking {
|
||||
id: string;
|
||||
room_id?: number | string;
|
||||
room?: { name?: string };
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
start?: string | number | Date;
|
||||
end?: string | number | Date;
|
||||
title?: string;
|
||||
invitees?: string[];
|
||||
}
|
||||
@@ -50,7 +61,7 @@ export interface BookingConfirmationProps {
|
||||
export interface RoomDetailsModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
room: ConferenceRoom | any;
|
||||
room: ConferenceRoom;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* setupTests.ts
|
||||
* Jest setup for custom matchers and DOM assertions.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* BookingForm.css */
|
||||
/*
|
||||
* BookingForm.css
|
||||
* Styles for BookingForm component.
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
.booking-form-room-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* BookingList.css */
|
||||
/*
|
||||
* BookingList.css
|
||||
* Styles for BookingList component.
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
.booking-list {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
/*
|
||||
* CalendarView.css
|
||||
* Styles for the calendar view component, including event colors and non-business hours.
|
||||
* Styles for the calendar view component, including event colors, business hours, and layout.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
*/
|
||||
/* Non-business hours styling for FullCalendar */
|
||||
/* =============================== */
|
||||
/* Non-business hours styling */
|
||||
/* =============================== */
|
||||
.fc-timegrid-col-bg .fc-non-business,
|
||||
.fc-non-business {
|
||||
background: #888 !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
/* Truncate and ellipsis for event content and titles */
|
||||
/* =============================== */
|
||||
/* Event content truncation */
|
||||
/* =============================== */
|
||||
.fc-event-title-ellipsis,
|
||||
.fc-event .fc-event-main > div,
|
||||
.fc-event-title,
|
||||
@@ -33,7 +37,9 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Weekend all-day and time slots: unified non-business color, lighter border */
|
||||
/* =============================== */
|
||||
/* Weekend and weekday styling */
|
||||
/* =============================== */
|
||||
.fc .fc-daygrid-day.fc-day-sat .fc-daygrid-day-frame,
|
||||
.fc .fc-daygrid-day.fc-day-sun .fc-daygrid-day-frame,
|
||||
.fc .fc-timegrid-col.fc-day-sat,
|
||||
@@ -49,13 +55,13 @@
|
||||
opacity: 0.7 !important;
|
||||
}
|
||||
|
||||
/* Weekday non-business hours: same as weekend background */
|
||||
/* Weekday non-business hours */
|
||||
.fc .fc-non-business {
|
||||
background: #888 !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
|
||||
/* Workday all-day and time slots: match all-day workday background */
|
||||
/* Workday all-day and time slots */
|
||||
.fc .fc-daygrid-day .fc-daygrid-day-frame,
|
||||
.fc .fc-timegrid-col {
|
||||
background: #c0c0c0 !important;
|
||||
@@ -67,12 +73,14 @@
|
||||
border-color: #bbb !important;
|
||||
}
|
||||
|
||||
/* Out-of-month days: distinct but not too dark */
|
||||
/* Out-of-month days styling */
|
||||
.fc .fc-daygrid-day.fc-day-other .fc-daygrid-day-frame {
|
||||
background: #aaa !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
/* Calendar cell backgrounds use CSS variables */
|
||||
/* =============================== */
|
||||
/* Calendar cell backgrounds */
|
||||
/* =============================== */
|
||||
:root {
|
||||
--fc-page-bg-color: #a0a0a0;
|
||||
--fc-neutral-bg-color: #e3f2fd;
|
||||
@@ -144,7 +152,6 @@
|
||||
}
|
||||
|
||||
.fc .fc-event {
|
||||
background: #1976d2 !important;
|
||||
color: #fff !important;
|
||||
padding: 2px 6px !important;
|
||||
font-size: 0.95rem !important;
|
||||
@@ -191,3 +198,96 @@
|
||||
.fc-toolbar.fc-header-toolbar {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
/* =============================== */
|
||||
/* Room color classes for events */
|
||||
/* =============================== */
|
||||
/*
|
||||
* .room-color-0 to .room-color-19
|
||||
* Used to assign unique background colors to events based on room ID.
|
||||
* These classes are mapped in the frontend logic (getRoomClass).
|
||||
*/
|
||||
.room-color-0 {
|
||||
background: #1976d2 !important;
|
||||
}
|
||||
.room-color-1 {
|
||||
background: #388e3c !important;
|
||||
}
|
||||
.room-color-2 {
|
||||
background: #fbc02d !important;
|
||||
}
|
||||
.room-color-3 {
|
||||
background: #d32f2f !important;
|
||||
}
|
||||
.room-color-4 {
|
||||
background: #7b1fa2 !important;
|
||||
}
|
||||
.room-color-5 {
|
||||
background: #0288d1 !important;
|
||||
}
|
||||
.room-color-6 {
|
||||
background: #c2185b !important;
|
||||
}
|
||||
.room-color-7 {
|
||||
background: #ffa000 !important;
|
||||
}
|
||||
.room-color-8 {
|
||||
background: #009688 !important;
|
||||
}
|
||||
.room-color-9 {
|
||||
background: #8bc34a !important;
|
||||
}
|
||||
.room-color-10 {
|
||||
background: #e91e63 !important;
|
||||
}
|
||||
.room-color-11 {
|
||||
background: #00bcd4 !important;
|
||||
}
|
||||
.room-color-12 {
|
||||
background: #ff5722 !important;
|
||||
}
|
||||
.room-color-13 {
|
||||
background: #9c27b0 !important;
|
||||
}
|
||||
.room-color-14 {
|
||||
background: #3f51b5 !important;
|
||||
}
|
||||
.room-color-15 {
|
||||
background: #4caf50 !important;
|
||||
}
|
||||
.room-color-16 {
|
||||
background: #ff9800 !important;
|
||||
}
|
||||
.room-color-17 {
|
||||
background: #607d8b !important;
|
||||
}
|
||||
.room-color-18 {
|
||||
background: #795548 !important;
|
||||
}
|
||||
.room-color-19 {
|
||||
background: #b71c1c !important;
|
||||
}
|
||||
|
||||
/* Ensure event text is readable on colored backgrounds */
|
||||
.room-color-0,
|
||||
.room-color-1,
|
||||
.room-color-2,
|
||||
.room-color-3,
|
||||
.room-color-4,
|
||||
.room-color-5,
|
||||
.room-color-6,
|
||||
.room-color-7,
|
||||
.room-color-8,
|
||||
.room-color-9,
|
||||
.room-color-10,
|
||||
.room-color-11,
|
||||
.room-color-12,
|
||||
.room-color-13,
|
||||
.room-color-14,
|
||||
.room-color-15,
|
||||
.room-color-16,
|
||||
.room-color-17,
|
||||
.room-color-18,
|
||||
.room-color-19 {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* CalendarViewInline.css */
|
||||
/*
|
||||
* CalendarViewInline.css
|
||||
* Inline styles for CalendarView component.
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
.calendar-event-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Includes layout, card, and button styles for rooms and bookings.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-05
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
/* Force selected available room to have a visible background */
|
||||
.MuiButtonBase-root.MuiListItemButton-root.Mui-selected {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* RoomList.css */
|
||||
/*
|
||||
* RoomList.css
|
||||
* Styles for RoomList component.
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
.room-list-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* theme.ts
|
||||
* MUI theme configuration for the frontend.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
import { createTheme } from "@mui/material/styles";
|
||||
|
||||
const theme = createTheme({
|
||||
|
||||
32
frontend/src/types.ts
Normal file
32
frontend/src/types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* types.ts
|
||||
* Centralized TypeScript types for frontend application.
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-09
|
||||
*/
|
||||
|
||||
/**
|
||||
* BookingEvent type for calendar events.
|
||||
*/
|
||||
export type BookingEvent = {
|
||||
originalBooking?: import("./interfaces").Booking;
|
||||
id: string | number;
|
||||
title: string;
|
||||
start: Date | string;
|
||||
end: Date | string;
|
||||
color?: string;
|
||||
room_id?: string | number;
|
||||
room_name?: string;
|
||||
invitees?: string[];
|
||||
extendedProps?: {
|
||||
room_id?: string | number;
|
||||
room_name?: string;
|
||||
invitees?: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Booking type alias for shared usage.
|
||||
*/
|
||||
export type BookingType = import("./interfaces").Booking;
|
||||
@@ -1,5 +1,10 @@
|
||||
// date.ts
|
||||
// Utility functions for date and time formatting and manipulation
|
||||
/**
|
||||
* date.ts
|
||||
* Utility functions for date and time formatting and manipulation.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
|
||||
export function formatLocalDateTimeInput(date: Date): string {
|
||||
function pad(n: number): string {
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
// src/utils/logger.ts
|
||||
// Centralized logger using loglevel, with level set from FRONTEND_LOG_LEVEL
|
||||
/**
|
||||
* logger.ts
|
||||
* Centralized logger using loglevel, with level set from FRONTEND_LOG_LEVEL.
|
||||
*
|
||||
* Author: Cliff Hill
|
||||
* Last updated: 2025-09-08
|
||||
*/
|
||||
import log, { LogLevelDesc } from "loglevel";
|
||||
|
||||
const level = (process.env.FRONTEND_LOG_LEVEL || "info") as LogLevelDesc;
|
||||
|
||||
Reference in New Issue
Block a user