Refactored to use schema.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-09-08 15:37:51 -04:00
parent 7203469634
commit 707dc76c8b
7 changed files with 109 additions and 153 deletions

View File

@@ -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.

View File

@@ -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<BookingFormProps> = ({
editBooking,
onBookingSuccess,
allInvitees,
}) => {
}: BookingFormProps) => {
const navigate = useNavigate();
// State and refs
const [roomModalOpen, setRoomModalOpen] = useState(false);
@@ -259,72 +262,6 @@ const BookingForm: React.FC<BookingFormProps> = ({
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<string>("");
const [end, setEnd] = useState<string>("");
const userChangedStart = useRef(false);
@@ -359,15 +296,18 @@ const BookingForm: React.FC<BookingFormProps> = ({
(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<BookingFormProps> = ({
}
);
}
}, [room_id, isEdit, slotInfo]);
}, [room_id, isEdit, slotInfo, editBooking?.end, editBooking?.start]);
const [invitees, setInvitees] = useState<string[]>([]);
const [availableInvitees, setAvailableInvitees] =
useState<string[]>(allInvitees);
@@ -407,7 +347,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
? 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<BookingFormProps> = ({
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<BookingFormProps> = ({
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",

View File

@@ -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.

View File

@@ -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<RoomDetailsModalProps> = ({
open,

View File

@@ -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.

View File

@@ -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";

71
frontend/src/schemas.ts Normal file
View File

@@ -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;
}