From 0d120bb8a876e56f0819189d1382f195a2f46a30 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Tue, 9 Sep 2025 12:54:35 -0400 Subject: [PATCH] Adding more fixes to the system. Signed-off-by: Cliff Hill --- frontend/src/App.tsx | 2 + frontend/src/apis/sse.ts | 12 +- frontend/src/apis/users.ts | 31 +-- .../src/components/BookingConfirmation.tsx | 9 +- frontend/src/components/BookingForm.tsx | 22 +- frontend/src/components/BookingList.tsx | 8 +- frontend/src/components/CalendarView.tsx | 72 ++---- frontend/src/components/RoomDetailsModal.tsx | 12 +- frontend/src/components/RoomList.tsx | 12 +- frontend/src/helpers/booking.ts | 60 +---- frontend/src/helpers/bookingList.ts | 21 +- frontend/src/helpers/calendar.ts | 22 +- frontend/src/helpers/room.ts | 21 +- frontend/src/helpers/validation.ts | 12 +- frontend/src/index.css | 2 +- frontend/src/interfaces.ts | 127 ++++++++++ frontend/src/pages/BookingPage.tsx | 218 ++++++++++-------- frontend/src/pages/ConfirmationPage.tsx | 38 ++- frontend/src/pages/LandingPage.tsx | 29 ++- frontend/src/react-app-env.d.ts | 7 + frontend/src/reportWebVitals.ts | 7 + frontend/src/schemas.ts | 13 +- frontend/src/setupTests.ts | 7 + frontend/src/styles/BookingForm.css | 7 +- frontend/src/styles/BookingList.css | 7 +- frontend/src/styles/CalendarView.css | 118 +++++++++- frontend/src/styles/CalendarViewInline.css | 7 +- frontend/src/styles/LandingPage.css | 2 +- frontend/src/styles/RoomList.css | 7 +- frontend/src/theme.ts | 7 + frontend/src/types.ts | 32 +++ frontend/src/utils/date.ts | 9 +- frontend/src/utils/logger.ts | 9 +- 33 files changed, 653 insertions(+), 316 deletions(-) create mode 100644 frontend/src/interfaces.ts create mode 100644 frontend/src/types.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0ab60801..2ff4688e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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"; diff --git a/frontend/src/apis/sse.ts b/frontend/src/apis/sse.ts index b03ee31d..cea01d9b 100644 --- a/frontend/src/apis/sse.ts +++ b/frontend/src/apis/sse.ts @@ -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); diff --git a/frontend/src/apis/users.ts b/frontend/src/apis/users.ts index 5fe3c2d8..35e282cc 100644 --- a/frontend/src/apis/users.ts +++ b/frontend/src/apis/users.ts @@ -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 { + const res = await axios.get("/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 { + 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("/users/available/", { params: query }); return res.data; } diff --git a/frontend/src/components/BookingConfirmation.tsx b/frontend/src/components/BookingConfirmation.tsx index 3a359e43..5c18ed50 100644 --- a/frontend/src/components/BookingConfirmation.tsx +++ b/frontend/src/components/BookingConfirmation.tsx @@ -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. diff --git a/frontend/src/components/BookingForm.tsx b/frontend/src/components/BookingForm.tsx index 05f39fd1..a8507d4e 100644 --- a/frontend/src/components/BookingForm.tsx +++ b/frontend/src/components/BookingForm.tsx @@ -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 = ({ } 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 = ({ 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 = ({ 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] + } /> 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 = ({ // 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 = ({ 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 ( = ({ borderRadius: 4, paddingLeft: 4, paddingRight: 4, - background: undefined, // Remove inline background to allow CSS class to control color }} > = ({ 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} diff --git a/frontend/src/components/RoomDetailsModal.tsx b/frontend/src/components/RoomDetailsModal.tsx index 5fc8feda..6f7c7a1e 100644 --- a/frontend/src/components/RoomDetailsModal.tsx +++ b/frontend/src/components/RoomDetailsModal.tsx @@ -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 = ({ open, diff --git a/frontend/src/components/RoomList.tsx b/frontend/src/components/RoomList.tsx index e2fa8004..2e70abfd 100644 --- a/frontend/src/components/RoomList.tsx +++ b/frontend/src/components/RoomList.tsx @@ -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. diff --git a/frontend/src/helpers/booking.ts b/frontend/src/helpers/booking.ts index 74dd5042..bbf9f41a 100644 --- a/frontend/src/helpers/booking.ts +++ b/frontend/src/helpers/booking.ts @@ -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) : ""; } /** diff --git a/frontend/src/helpers/bookingList.ts b/frontend/src/helpers/bookingList.ts index 1550a9f7..6e434436 100644 --- a/frontend/src/helpers/bookingList.ts +++ b/frontend/src/helpers/bookingList.ts @@ -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. diff --git a/frontend/src/helpers/calendar.ts b/frontend/src/helpers/calendar.ts index ac53e78a..b490c389 100644 --- a/frontend/src/helpers/calendar.ts +++ b/frontend/src/helpers/calendar.ts @@ -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}`; } diff --git a/frontend/src/helpers/room.ts b/frontend/src/helpers/room.ts index 1933ee21..1709a786 100644 --- a/frontend/src/helpers/room.ts +++ b/frontend/src/helpers/room.ts @@ -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 diff --git a/frontend/src/helpers/validation.ts b/frontend/src/helpers/validation.ts index cbb2dc4c..caf138f4 100644 --- a/frontend/src/helpers/validation.ts +++ b/frontend/src/helpers/validation.ts @@ -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. diff --git a/frontend/src/index.css b/frontend/src/index.css index 97fb73e3..ac212f2b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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 diff --git a/frontend/src/interfaces.ts b/frontend/src/interfaces.ts new file mode 100644 index 00000000..4a35e579 --- /dev/null +++ b/frontend/src/interfaces.ts @@ -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; +} diff --git a/frontend/src/pages/BookingPage.tsx b/frontend/src/pages/BookingPage.tsx index 699f4001..51320c12 100644 --- a/frontend/src/pages/BookingPage.tsx +++ b/frontend/src/pages/BookingPage.tsx @@ -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>( - {} - ); + /** + * Real-time bookings state, grouped by room id. + */ + const [bookingsByRoom, setBookingsByRoom] = useState< + Record + >({}); // 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(null); + const [bookingsError, setBookingsError] = useState(null); const eventSourceRef = useRef(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 = {}; + const results: Record = {}; 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 = {}; - data.bookings.forEach((b: any) => { - if (!grouped[b.room_id]) grouped[b.room_id] = []; - grouped[b.room_id].push(b); + const grouped: Record = {}; + 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 = useMemo(() => { - const defaultColors = [ - "#1976d2", - "#388e3c", - "#fbc02d", - "#d32f2f", - "#7b1fa2", - "#0288d1", - "#c2185b", - "#ffa000", - ]; - const colors: Record = {}; - 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(null); - const [editBooking, setEditBooking] = useState(null); + interface SlotInfo { + start: Date; + end: Date; + room_id?: string | number; + } + const [formSlot, setFormSlot] = useState(null); + const [editBooking, setEditBooking] = useState(null); // Default to first room, always require a room to be selected - const [selectedRoomId, setSelectedRoomId] = useState(""); + const [selectedRoomId, setSelectedRoomId] = useState(""); // 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 = {}; + const results: Record = {}; 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) => ( {room.name} @@ -507,11 +525,13 @@ const BookingPage: React.FC = () => { setRoomModalOpen(false)} - room={rooms.find((r: any) => r.id === selectedRoomId)} + room={ + rooms.find((r: ConferenceRoom) => r.id === selectedRoomId) ?? rooms[0] + } /> handleSelectEvent(event)} + events={events} + onEventClick={handleSelectEvent} onSlotSelect={handleSelectSlot} selectedRoomId={selectedRoomId} /> @@ -519,10 +539,18 @@ const BookingPage: React.FC = () => { 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)} /> )} diff --git a/frontend/src/pages/ConfirmationPage.tsx b/frontend/src/pages/ConfirmationPage.tsx index 6d21f02d..65db7c74 100644 --- a/frontend/src/pages/ConfirmationPage.tsx +++ b/frontend/src/pages/ConfirmationPage.tsx @@ -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(location.state?.booking || null); - // Subscribe to SSE for live booking status + /** + * Booking state, updated via SSE for live status. + */ + const [booking, setBooking] = useState( + 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={{ diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 27b7842e..b25960d2 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -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 = {}; - 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); diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts index 6431bc5f..a0719c63 100644 --- a/frontend/src/react-app-env.d.ts +++ b/frontend/src/react-app-env.d.ts @@ -1 +1,8 @@ +/** + * react-app-env.d.ts + * TypeScript environment declarations for React scripts. + * + * Author: Cliff Hill + * Last updated: 2025-09-08 + */ /// diff --git a/frontend/src/reportWebVitals.ts b/frontend/src/reportWebVitals.ts index 5fa3583b..5511c489 100644 --- a/frontend/src/reportWebVitals.ts +++ b/frontend/src/reportWebVitals.ts @@ -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) => { diff --git a/frontend/src/schemas.ts b/frontend/src/schemas.ts index 627a4374..01f48734 100644 --- a/frontend/src/schemas.ts +++ b/frontend/src/schemas.ts @@ -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; } /** diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 1dd407a6..dbb492a2 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -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) diff --git a/frontend/src/styles/BookingForm.css b/frontend/src/styles/BookingForm.css index f9143231..cb9f506f 100644 --- a/frontend/src/styles/BookingForm.css +++ b/frontend/src/styles/BookingForm.css @@ -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; diff --git a/frontend/src/styles/BookingList.css b/frontend/src/styles/BookingList.css index 7e6ffb38..a7e8ffeb 100644 --- a/frontend/src/styles/BookingList.css +++ b/frontend/src/styles/BookingList.css @@ -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; } diff --git a/frontend/src/styles/CalendarView.css b/frontend/src/styles/CalendarView.css index f5da9c03..2f461f45 100644 --- a/frontend/src/styles/CalendarView.css +++ b/frontend/src/styles/CalendarView.css @@ -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; +} diff --git a/frontend/src/styles/CalendarViewInline.css b/frontend/src/styles/CalendarViewInline.css index e8933418..62fe421f 100644 --- a/frontend/src/styles/CalendarViewInline.css +++ b/frontend/src/styles/CalendarViewInline.css @@ -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%; diff --git a/frontend/src/styles/LandingPage.css b/frontend/src/styles/LandingPage.css index d398dd98..43a717bf 100644 --- a/frontend/src/styles/LandingPage.css +++ b/frontend/src/styles/LandingPage.css @@ -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 { diff --git a/frontend/src/styles/RoomList.css b/frontend/src/styles/RoomList.css index 33020c4f..0aa841d7 100644 --- a/frontend/src/styles/RoomList.css +++ b/frontend/src/styles/RoomList.css @@ -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; diff --git a/frontend/src/theme.ts b/frontend/src/theme.ts index 37fc90d2..90c3ecdc 100644 --- a/frontend/src/theme.ts +++ b/frontend/src/theme.ts @@ -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({ diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 00000000..547913d9 --- /dev/null +++ b/frontend/src/types.ts @@ -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; diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts index 4fec8d49..d5cb9415 100644 --- a/frontend/src/utils/date.ts +++ b/frontend/src/utils/date.ts @@ -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 { diff --git a/frontend/src/utils/logger.ts b/frontend/src/utils/logger.ts index 4b201d61..f869d4f6 100644 --- a/frontend/src/utils/logger.ts +++ b/frontend/src/utils/logger.ts @@ -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;