mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-08 18:33:12 -04:00
Refactored to use schema.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
71
frontend/src/schemas.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user