From 707dc76c8be6cef75411ebb89b2fad49d85e1dc8 Mon Sep 17 00:00:00 2001 From: Cliff Hill Date: Mon, 8 Sep 2025 15:37:51 -0400 Subject: [PATCH] Refactored to use schema. Signed-off-by: Cliff Hill --- .../src/components/BookingConfirmation.tsx | 19 +-- frontend/src/components/BookingForm.tsx | 116 +++++------------- frontend/src/components/BookingList.tsx | 22 +--- frontend/src/components/RoomDetailsModal.tsx | 6 +- frontend/src/components/RoomList.tsx | 22 +--- frontend/src/pages/LandingPage.tsx | 6 +- frontend/src/schemas.ts | 71 +++++++++++ 7 files changed, 109 insertions(+), 153 deletions(-) create mode 100644 frontend/src/schemas.ts diff --git a/frontend/src/components/BookingConfirmation.tsx b/frontend/src/components/BookingConfirmation.tsx index 940a317a..ddd99f52 100644 --- a/frontend/src/components/BookingConfirmation.tsx +++ b/frontend/src/components/BookingConfirmation.tsx @@ -17,24 +17,7 @@ import React from "react"; import { Box, Button } from "@mui/material"; -import type { Booking } from "./BookingList"; -/** - * Props for BookingConfirmation. - * @property {Booking} booking - The booking to confirm - * @property {() => void} onEdit - Callback to edit booking - * @property {() => void} onBack - Callback to go back - */ -interface BookingConfirmationProps { - booking: Booking & { - room?: { name?: string }; - invitees?: string[]; - start_time: string; - end_time: string; - title?: string; - }; - onEdit: () => void; - onBack: () => void; -} +import type { BookingConfirmationProps } from "../schemas"; /** * BookingConfirmation displays a summary of a confirmed booking. diff --git a/frontend/src/components/BookingForm.tsx b/frontend/src/components/BookingForm.tsx index c05961aa..1d1c549c 100644 --- a/frontend/src/components/BookingForm.tsx +++ b/frontend/src/components/BookingForm.tsx @@ -8,6 +8,7 @@ */ import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; + import { getRoomBookings, createBooking, @@ -17,6 +18,7 @@ import { import { connectRoomsAvailabilityStream } from "../apis/sse"; import { getInvitees, addInvitee, removeInvitee } from "../apis/invitees"; import { getAvailableUsers } from "../apis/users"; +import type { ConferenceRoom } from "../schemas"; import { Box, Button, @@ -38,6 +40,17 @@ import InfoIcon from "@mui/icons-material/InfoOutlined"; import RoomDetailsModal from "./RoomDetailsModal"; import { logger } from "../utils/logger"; +// Remove import of BookingFormProps from schemas, and define BookingFormProps locally with correct types using ConferenceRoom +interface BookingFormProps { + open: boolean; + onClose: () => void; + slotInfo: any; + rooms: ConferenceRoom[]; + editBooking?: any; + onBookingSuccess?: () => void; + allInvitees: string[]; +} + // BookingForm: form for selecting room, date, time, title, and invitees // Format Date as 'YYYY-MM-DDTHH:mm' for datetime-local input @@ -53,16 +66,6 @@ function formatLocalDateTimeInput(date: Date): string { return `${yyyy}-${mm}-${dd}T${hh}:${min}`; } -interface BookingFormProps { - open: boolean; - onClose: () => void; - slotInfo: any; - rooms: any[]; - editBooking?: any; - onBookingSuccess?: () => void; - allInvitees: string[]; -} - // Extract room_id from editBooking function getEditBookingRoomId(editBooking: any): string { // Try FullCalendar event object structure first @@ -104,7 +107,7 @@ const BookingForm: React.FC = ({ editBooking, onBookingSuccess, allInvitees, -}) => { +}: BookingFormProps) => { const navigate = useNavigate(); // State and refs const [roomModalOpen, setRoomModalOpen] = useState(false); @@ -259,72 +262,6 @@ const BookingForm: React.FC = ({ const [title, setTitle] = useState(isEdit ? editBooking.title : ""); // Compute initial start/end for new bookings // For new bookings, use slotInfo.start/end if provided, otherwise next quarter hour from now - // Helper: Find next available 30-min slot for a room, checking every quarter hour - async function getNextAvailableSlot( - roomId: string - ): Promise<{ start: Date; end: Date } | null> { - const now = new Date(); - const dateStr = now.toISOString().slice(0, 10); - try { - const bookings = await getRoomBookings(roomId, dateStr); - // Find the earliest available quarter-hour after now or after the last booking ends - let earliest = now; - if (bookings.length > 0) { - // Find the latest end_time among bookings that end after now - const futureBookings = bookings.filter( - (b: { end_time: string }) => new Date(b.end_time) > now - ); - if (futureBookings.length > 0) { - const latestEnd = futureBookings.reduce( - (max: Date, b: { end_time: string }) => { - const end = new Date(b.end_time); - return end > max ? end : max; - }, - now - ); - if (latestEnd > earliest) earliest = latestEnd; - } - } - // Only round up for initial candidate - let candidate = new Date(earliest.getTime()); - candidate.setSeconds(0, 0); - if (candidate.getMinutes() % 15 !== 0) { - candidate = roundToStrictlyFutureQuarter(candidate); - } - for (let i = 0; i < 96; i++) { - // 96 quarter-hours in 24 hours - const candidateEnd = new Date(candidate.getTime() + 30 * 60000); - // Find the first overlapping booking - const conflict = bookings.find( - (b: { start_time: string; end_time: string }) => { - return ( - candidate.toISOString() < b.end_time && - candidateEnd.toISOString() > b.start_time - ); - } - ); - if (!conflict) { - return { start: candidate, end: candidateEnd }; - } - // Move candidate to the end of the overlapped booking - candidate = new Date(conflict.end_time); - // After moving, round up to the next quarter hour if needed - if (candidate.getMinutes() % 15 !== 0) { - candidate = roundToStrictlyFutureQuarter(candidate); - } - } - } catch (e) { - // fallback: just use next quarter hour - const candidate = roundToStrictlyFutureQuarter(now); - return { - start: candidate, - end: new Date(candidate.getTime() + 30 * 60000), - }; - } - return null; - } - - // Initial start time logic const [start, setStart] = useState(""); const [end, setEnd] = useState(""); const userChangedStart = useRef(false); @@ -359,15 +296,18 @@ const BookingForm: React.FC = ({ (bookings) => { let candidate = roundToStrictlyFutureQuarter(now); for (let i = 0; i < 96; i++) { - const candidateEnd = new Date(candidate.getTime() + 30 * 60000); + const candidateCopy = new Date(candidate); + const candidateEndCopy = new Date( + candidateCopy.getTime() + 30 * 60000 + ); const conflict = bookings.find( (b: { start_time: string; end_time: string }) => - candidate.toISOString() < b.end_time && - candidateEnd.toISOString() > b.start_time + candidateCopy.toISOString() < b.end_time && + candidateEndCopy.toISOString() > b.start_time ); if (!conflict) { - setStart(candidate.toISOString()); - setEnd(candidateEnd.toISOString()); + setStart(candidateCopy.toISOString()); + setEnd(candidateEndCopy.toISOString()); return; } candidate = new Date(conflict.end_time); @@ -381,7 +321,7 @@ const BookingForm: React.FC = ({ } ); } - }, [room_id, isEdit, slotInfo]); + }, [room_id, isEdit, slotInfo, editBooking?.end, editBooking?.start]); const [invitees, setInvitees] = useState([]); const [availableInvitees, setAvailableInvitees] = useState(allInvitees); @@ -407,7 +347,7 @@ const BookingForm: React.FC = ({ ? getEditBookingRoomId(editBooking) : null; await Promise.all( - rooms.map(async (room) => { + rooms.map(async (room: ConferenceRoom) => { try { const date = new Date(start).toISOString().slice(0, 10); const bookings = await getRoomBookings(room.id, date); @@ -835,7 +775,7 @@ const BookingForm: React.FC = ({ required > {rooms && rooms.length > 0 ? ( - rooms.map((room: any) => { + rooms.map((room: ConferenceRoom) => { const currentBookingRoomId = isEdit ? getEditBookingRoomId(editBooking) : null; @@ -1019,12 +959,12 @@ const BookingForm: React.FC = ({ sx={ disableUnselected ? { - color: "#444", // Much darker grey for readability - opacity: 1, // No opacity reduction + color: "#444", + opacity: 1, fontWeight: "normal", bgcolor: "inherit", cursor: "not-allowed", - textDecoration: "line-through", // Optional: visually indicate disabled + textDecoration: "line-through", } : { fontWeight: isSelected ? "bold" : "normal", diff --git a/frontend/src/components/BookingList.tsx b/frontend/src/components/BookingList.tsx index b4dcaea5..87c2494a 100644 --- a/frontend/src/components/BookingList.tsx +++ b/frontend/src/components/BookingList.tsx @@ -6,28 +6,10 @@ * Author: Cliff Hill * Last updated: 2025-09-05 */ + import React from "react"; import { List, Typography, ListItemButton } from "@mui/material"; - -/** - * Booking type definition. - */ -export interface Booking { - id: string; - room?: { name?: string }; - start_time: string; - end_time: string; - title?: string; - invitees?: string[]; -} - -/** - * Props for BookingList. - */ -interface BookingListProps { - bookings: Booking[]; - onSelect?: (booking: Booking) => void; -} +import type { BookingListProps } from "../schemas"; /** * BookingList component. diff --git a/frontend/src/components/RoomDetailsModal.tsx b/frontend/src/components/RoomDetailsModal.tsx index fce208ef..2140e27d 100644 --- a/frontend/src/components/RoomDetailsModal.tsx +++ b/frontend/src/components/RoomDetailsModal.tsx @@ -17,11 +17,7 @@ import { Box, } from "@mui/material"; -export interface RoomDetailsModalProps { - open: boolean; - onClose: () => void; - room: any; -} +import type { RoomDetailsModalProps } from "../schemas"; const RoomDetailsModal: React.FC = ({ open, diff --git a/frontend/src/components/RoomList.tsx b/frontend/src/components/RoomList.tsx index 54727ac9..125db0a7 100644 --- a/frontend/src/components/RoomList.tsx +++ b/frontend/src/components/RoomList.tsx @@ -10,6 +10,7 @@ * RoomList component. * Displays a list of available conference rooms. Selected room is highlighted. */ + import React from "react"; import { Card, @@ -22,26 +23,7 @@ import { } from "@mui/material"; import MeetingRoomIcon from "@mui/icons-material/MeetingRoom"; import { FC } from "react"; - -/** - * ConferenceRoom type definition. - */ -export interface ConferenceRoom { - id: number; - name: string; - location: string; - equipment: string; - capacity: number; -} - -/** - * Props for RoomList. - */ -interface RoomListProps { - rooms: ConferenceRoom[]; - selectedRoomId?: number; - onSelectRoom?: (roomId: number) => void; -} +import type { RoomListProps } from "../schemas"; /** * Renders a list of conference rooms with their details. diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 9350d282..adf22621 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -20,8 +20,10 @@ import { useNavigate } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { getRooms } from "../apis/rooms"; import { connectRoomsAvailabilityStream } from "../apis/sse"; -import RoomList, { ConferenceRoom } from "../components/RoomList"; -import BookingList, { Booking } from "../components/BookingList"; +import RoomList from "../components/RoomList"; +import BookingList from "../components/BookingList"; +import type { ConferenceRoom } from "../schemas"; +import type { Booking } from "../schemas"; import EventNoteIcon from "@mui/icons-material/EventNote"; import "./LandingPage.css"; import { logger } from "../utils/logger"; diff --git a/frontend/src/schemas.ts b/frontend/src/schemas.ts new file mode 100644 index 00000000..627a4374 --- /dev/null +++ b/frontend/src/schemas.ts @@ -0,0 +1,71 @@ +/** + * schemas.ts + * Centralized TypeScript interfaces for backend data schemas used in the frontend. + * + * Author: Cliff Hill + * Last updated: 2025-09-08 + */ + +/** + * Represents a booking for a conference room. + */ +export interface Booking { + id: string; + room?: { name?: string }; + start_time: string; + end_time: string; + title?: string; + invitees?: string[]; +} + +/** + * Represents a conference room. + */ +export interface ConferenceRoom { + id: number; + name: string; + location: string; + equipment: string; + capacity: number; +} + +/** + * Props for Booking confirmation. + */ +export interface BookingConfirmationProps { + booking: Booking & { + room?: { name?: string }; + invitees?: string[]; + start_time: string; + end_time: string; + title?: string; + }; + onEdit: () => void; + onBack: () => void; +} + +/** + * Props for Room details modal. + */ +export interface RoomDetailsModalProps { + open: boolean; + onClose: () => void; + room: ConferenceRoom | any; +} + +/** + * Props for Booking list. + */ +export interface BookingListProps { + bookings: Booking[]; + onSelect?: (booking: Booking) => void; +} + +/** + * Props for Room list. + */ +export interface RoomListProps { + rooms: ConferenceRoom[]; + selectedRoomId?: number; + onSelectRoom?: (roomId: number) => void; +}