Working on the calendar stuff.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-09-08 21:35:08 -04:00
parent 7a1bfd6687
commit 2d10c91732
11 changed files with 612 additions and 514 deletions
+252 -369
View File
@@ -8,14 +8,8 @@
*/
import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import {
getRoomBookings,
createBooking,
updateBooking,
deleteBooking,
} from "../apis/bookings";
import { getEditBookingRoomId, formatBookingError } from "../helpers/booking";
import { getRoomBookings, deleteBooking } from "../apis/bookings";
import { getEditBookingRoomId } from "../helpers/booking";
import {
validateRoomId,
validateStart,
@@ -24,14 +18,11 @@ import {
validateInviteeEmail,
validateRoomAvailability,
} from "../helpers/validation";
import {
formatLocalDateTimeInput,
roundToStrictlyFutureQuarter,
} from "../utils/date";
import { roundToStrictlyFutureQuarter } from "../utils/date";
import { connectRoomsAvailabilityStream } from "../apis/sse";
import { getInvitees, addInvitee, removeInvitee } from "../apis/invitees";
import { getInvitees } from "../apis/invitees";
import { getAvailableUsers } from "../apis/users";
import type { ConferenceRoom } from "../schemas";
import type { ConferenceRoom, Booking } from "../schemas";
import {
Box,
Button,
@@ -52,14 +43,23 @@ import CircularProgress from "@mui/material/CircularProgress";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import RoomDetailsModal from "./RoomDetailsModal";
import { logger } from "../utils/logger";
import { SelectChangeEvent } from "@mui/material/Select";
// Remove import of BookingFormProps from schemas, and define BookingFormProps locally with correct types using ConferenceRoom
interface SlotInfo {
start: Date | string;
end: Date | string;
room_id?: string | number;
allDay?: boolean;
viewType?: string;
}
interface BookingFormProps {
open: boolean;
onClose: () => void;
slotInfo: any;
slotInfo: SlotInfo;
rooms: ConferenceRoom[];
editBooking?: any;
editBooking?: Booking;
onBookingSuccess?: () => void;
allInvitees: string[];
}
@@ -77,10 +77,36 @@ const BookingForm: React.FC<BookingFormProps> = ({
onBookingSuccess,
allInvitees,
}: BookingFormProps) => {
const navigate = useNavigate();
// Calculate initial start/end before state declarations to avoid ReferenceError
// Calculate initial start/end before state declarations to avoid ReferenceError
const initialStart = editBooking?.start_time
? editBooking.start_time
: slotInfo?.start
? typeof slotInfo.start === "string"
? slotInfo.start
: slotInfo.start.toISOString()
: "";
const initialEnd = editBooking?.end_time
? editBooking.end_time
: slotInfo?.end
? typeof slotInfo.end === "string"
? slotInfo.end
: slotInfo.end.toISOString()
: "";
// State and refs
const [roomModalOpen, setRoomModalOpen] = useState(false);
const isEdit = !!editBooking;
// View mode: if editing and start_time is in the past
const now = new Date();
const bookingStart = editBooking?.start_time
? new Date(editBooking.start_time)
: undefined;
const isViewMode: boolean = Boolean(
isEdit && bookingStart && bookingStart < now
);
// Submission state
const [submitting, setSubmitting] = useState(false);
@@ -104,199 +130,92 @@ const BookingForm: React.FC<BookingFormProps> = ({
}
});
// Re-validate form on any relevant change to keep errors and button state in sync
// (moved below state declarations)
const [start, setStart] = useState<string>(initialStart);
const [end, setEnd] = useState<string>(initialEnd);
const [title, setTitle] = useState<string>(editBooking?.title || "");
// Track interval between start and end
const [intervalMinutes, setIntervalMinutes] = useState(() => {
if (isEdit && editBooking?.start && editBooking?.end) {
const startDate = new Date(editBooking.start);
const endDate = new Date(editBooking.end);
return Math.max(
1,
Math.round((endDate.getTime() - startDate.getTime()) / 60000)
);
}
if (slotInfo?.start && slotInfo?.end) {
const startDate = new Date(slotInfo.start);
const endDate = new Date(slotInfo.end);
return Math.max(
1,
Math.round((endDate.getTime() - startDate.getTime()) / 60000)
);
}
return 30;
});
useEffect(() => {
if (editBooking && editBooking.id) {
const currentId = String(editBooking.id);
const intendedRoomId = getEditBookingRoomId(editBooking);
// Only set room if switching bookings or on initial mount, and only if user hasn't changed it
if (
lastBookingId.current !== currentId ||
(intendedRoomId &&
intendedRoomId !== room_id &&
!userChangedRoom.current)
) {
setRoomId(intendedRoomId);
lastBookingId.current = currentId;
userChangedRoom.current = false;
}
} else if (!editBooking && slotInfo && slotInfo.room_id) {
if (lastBookingId.current !== null && !userChangedRoom.current) {
setRoomId(String(slotInfo.room_id));
lastBookingId.current = null;
userChangedRoom.current = false;
}
} else if (
!editBooking &&
(!room_id || !rooms.some((r) => String(r.id) === String(room_id))) &&
rooms &&
rooms.length > 0 &&
!userChangedRoom.current
) {
setRoomId(String(rooms[0].id));
lastBookingId.current = null;
userChangedRoom.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editBooking, slotInfo, rooms, room_id]);
const handleRoomChange = (e: any) => {
userChangedRoom.current = true;
setRoomId(e.target.value);
};
// Only set room_id from editBooking or rooms on initial mount
// (do not overwrite user changes after initial load)
// If you want to reset when switching bookings, you can add logic for that case only
// Keep room_id in sync with editBooking or rooms (no debug log)
useEffect(() => {
if (editBooking && editBooking.id) {
const currentId = String(editBooking.id);
const intendedRoomId = getEditBookingRoomId(editBooking);
// Only set room if switching bookings or on initial mount, and only if user hasn't changed it
if (
lastBookingId.current !== currentId ||
(intendedRoomId &&
intendedRoomId !== room_id &&
!userChangedRoom.current)
) {
setRoomId(intendedRoomId);
lastBookingId.current = currentId;
userChangedRoom.current = false;
}
} else if (!editBooking && slotInfo && slotInfo.room_id) {
if (lastBookingId.current !== null && !userChangedRoom.current) {
setRoomId(String(slotInfo.room_id));
lastBookingId.current = null;
userChangedRoom.current = false;
}
} else if (
!editBooking &&
(!room_id || !rooms.some((r) => String(r.id) === String(room_id))) &&
rooms &&
rooms.length > 0 &&
!userChangedRoom.current
) {
setRoomId(String(rooms[0].id));
lastBookingId.current = null;
userChangedRoom.current = false;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editBooking, slotInfo, rooms, room_id]);
// Fallback: if room_id is still empty but rooms exist, set to first room
useEffect(() => {
if (!room_id && rooms && rooms.length > 0) {
setRoomId(String(rooms[0].id));
}
}, [room_id, rooms]);
// Place these hooks after all state and helper declarations
// (Removed duplicate debug log)
useEffect(() => {
if (!room_id && rooms && rooms.length > 0) {
setRoomId(String(rooms[0].id));
}
}, [room_id, rooms]);
useEffect(() => {
if (!room_id && rooms && rooms.length > 0) {
setRoomId(String(rooms[0].id));
}
}, [room_id, rooms]);
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
const [start, setStart] = useState<string>("");
const [end, setEnd] = useState<string>("");
const userChangedStart = useRef(false);
// On mount, room change, or after booking, always fetch latest bookings before suggesting next available slot
// Effect for edit mode: populate start/end from editBooking or slotInfo
useEffect(() => {
if (userChangedStart.current) return;
if (isEdit) {
setStart(editBooking?.start || "");
// Always set end to 30 minutes after start for initial booking
if (editBooking?.start) {
const startDate = new Date(editBooking.start);
setEnd(new Date(startDate.getTime() + 30 * 60000).toISOString());
} else {
setEnd(editBooking?.end || "");
}
return;
}
if (!open || !isEdit) return;
userChangedStart.current = false;
const rawStart = editBooking?.start_time
? typeof editBooking.start_time === "string"
? editBooking.start_time
: new Date(editBooking.start_time).toISOString()
: slotInfo?.start
? typeof slotInfo.start === "string"
? slotInfo.start
: new Date(slotInfo.start).toISOString()
: "";
const rawEnd = editBooking?.end_time
? typeof editBooking.end_time === "string"
? editBooking.end_time
: new Date(editBooking.end_time).toISOString()
: slotInfo?.end
? typeof slotInfo.end === "string"
? slotInfo.end
: new Date(slotInfo.end).toISOString()
: "";
setStart(rawStart);
setEnd(rawEnd);
}, [open, isEdit, editBooking, slotInfo]);
// Effect for new mode: populate start/end only after room_id is set
useEffect(() => {
if (!open || isEdit || !room_id) return;
userChangedStart.current = false;
const now = new Date();
if (slotInfo?.start) {
const slotStart = new Date(slotInfo.start);
if (slotStart > now) {
setStart(slotInfo.start);
// Always set end to 30 minutes after start
setStart(
typeof slotInfo.start === "string"
? slotInfo.start
: slotInfo.start.toISOString()
);
setEnd(new Date(slotStart.getTime() + 30 * 60000).toISOString());
return;
}
}
if (room_id) {
// Always fetch latest bookings before suggesting slot
getRoomBookings(room_id, now.toISOString().slice(0, 10)).then(
(bookings) => {
let candidate = roundToStrictlyFutureQuarter(now);
for (let i = 0; i < 96; i++) {
const candidateCopy = new Date(candidate);
const candidateEndCopy = new Date(
candidateCopy.getTime() + 30 * 60000
);
const conflict = bookings.find(
(b: { start_time: string; end_time: string }) =>
candidateCopy.toISOString() < b.end_time &&
candidateEndCopy.toISOString() > b.start_time
);
if (!conflict) {
setStart(candidateCopy.toISOString());
setEnd(candidateEndCopy.toISOString());
return;
}
candidate = new Date(conflict.end_time);
if (candidate.getMinutes() % 15 !== 0) {
candidate = roundToStrictlyFutureQuarter(candidate);
}
getRoomBookings(room_id, now.toISOString().slice(0, 10)).then(
(bookings) => {
let candidate = roundToStrictlyFutureQuarter(now);
for (let i = 0; i < 96; i++) {
const candidateCopy = new Date(candidate);
const candidateEndCopy = new Date(
candidateCopy.getTime() + 30 * 60000
);
const conflict = bookings.find(
(b: { start_time: string; end_time: string }) =>
candidateCopy.toISOString() < b.end_time &&
candidateEndCopy.toISOString() > b.start_time
);
if (!conflict) {
setStart(candidateCopy.toISOString());
setEnd(candidateEndCopy.toISOString());
return;
}
candidate = new Date(conflict.end_time);
if (candidate.getMinutes() % 15 !== 0) {
candidate = roundToStrictlyFutureQuarter(candidate);
}
// fallback: just use next quarter hour
setStart(candidate.toISOString());
setEnd(new Date(candidate.getTime() + 30 * 60000).toISOString());
}
);
}
}, [room_id, isEdit, slotInfo, editBooking?.end, editBooking?.start]);
setStart(candidate.toISOString());
setEnd(new Date(candidate.getTime() + 30 * 60000).toISOString());
}
);
}, [open, isEdit, room_id, slotInfo]);
const [invitees, setInvitees] = useState<string[]>([]);
const [availableInvitees, setAvailableInvitees] =
useState<string[]>(allInvitees);
const [loadingInvitees, setLoadingInvitees] = useState(false);
// Get current room object and capacity
const currentRoom = rooms.find((r: any) => String(r.id) === String(room_id));
const currentRoom = rooms.find(
(r: ConferenceRoom) => String(r.id) === String(room_id)
);
const roomCapacity = currentRoom?.capacity || Infinity;
const remainingSlots = roomCapacity - invitees.length;
@@ -322,7 +241,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
const bookings = await getRoomBookings(room.id, date);
const startTime = new Date(start).toISOString();
const endTime = new Date(end).toISOString();
const hasConflict = bookings.some((b: any) => {
const hasConflict = bookings.some((b: Booking) => {
if (isEdit && b.id === editBooking?.id) return false;
return startTime < b.end_time && endTime > b.start_time;
});
@@ -342,14 +261,16 @@ const BookingForm: React.FC<BookingFormProps> = ({
};
fetchConflicts();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [start, end, rooms, isEdit, editBooking?.id]);
}, [start, end, rooms, isEdit, editBooking?.id, setStart, setEnd]);
// Fetch invitees for editBooking
useEffect(() => {
if (isEdit && editBooking?.id) {
getInvitees(editBooking.id)
.then((inviteesData) => {
setInvitees(inviteesData.map((i: any) => i.user_email));
setInvitees(
inviteesData.map((i: { user_email: string }) => i.user_email)
);
})
.catch(() => setInvitees([]));
}
@@ -368,11 +289,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
end_time: new Date(end).toISOString(),
};
if (isEdit && editBooking?.id) {
(params as any).exclude_booking_id = editBooking.id;
(params as Record<string, unknown>).exclude_booking_id = editBooking.id;
}
getAvailableUsers(params)
.then((users) => {
const emails = users.map((u: any) => u.email);
const emails = users.map((u: { email: string }) => u.email);
setAvailableInvitees(Array.from(new Set([...emails, ...invitees])));
})
.catch(() =>
@@ -389,6 +310,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
// Independent field validation
function validateFields() {
if (isViewMode) {
setErrors({});
return true;
}
// ...existing code...
const newErrors: { [key: string]: string } = {};
// Room ID
const roomIdError = validateRoomId(room_id);
@@ -422,7 +348,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
end,
roomBookings,
isEdit,
editBooking
editBooking ?? null
);
if (availabilityErrors.roomError)
newErrors.room_id = availabilityErrors.roomError;
@@ -439,7 +365,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
}
// Real-time bookings state via SSE
const [roomBookings, setRoomBookings] = useState<any[]>([]);
const [roomBookings, setRoomBookings] = useState<Booking[]>([]);
useEffect(() => {
const es = connectRoomsAvailabilityStream({
onMessage: (data) => {
@@ -448,10 +374,10 @@ const BookingForm: React.FC<BookingFormProps> = ({
const dateStr = start
? new Date(start).toISOString().slice(0, 10)
: null;
const filtered = data.bookings.filter((b: any) => {
const filtered = data.bookings.filter((b: Booking) => {
const bookingDate = b.start_time?.slice(0, 10);
return (
String(b.room_id) === String(room_id) &&
String((b as any).room_id) === String(room_id) &&
(!dateStr || bookingDate === dateStr)
);
});
@@ -475,6 +401,10 @@ const BookingForm: React.FC<BookingFormProps> = ({
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (isViewMode) {
onClose();
return;
}
if (submitting) return;
setSubmitting(true);
setSubmitError(null);
@@ -482,118 +412,36 @@ const BookingForm: React.FC<BookingFormProps> = ({
setSubmitting(false);
return;
}
// Deduplicate invitee emails before submitting
const uniqueInvitees = Array.from(new Set(invitees));
// Helper to check if room changed
const originalRoomId = isEdit ? getEditBookingRoomId(editBooking) : null;
const roomChanged = isEdit && String(room_id) !== String(originalRoomId);
async function updateInvitees(bookingId: string) {
const currentInvitees = await getInvitees(bookingId).then((data) =>
data.map((i: any) => i.user_email)
);
for (const email of currentInvitees) {
if (!uniqueInvitees.includes(email)) {
await removeInvitee(bookingId, email);
}
}
for (const email of uniqueInvitees) {
if (!currentInvitees.includes(email)) {
await addInvitee(bookingId, email);
}
}
}
let bookingId = isEdit ? editBooking.id : null;
let payload = {
room_id,
start_time: start,
end_time: end,
title: title,
};
// DEBUG: Log payload before submission
console.log("Booking payload submitted:", payload);
let res;
let triedRoomFirst = false;
// ...existing code...
try {
if (isEdit) {
// Default: invitees first, then booking
try {
await updateInvitees(bookingId);
res = { data: await updateBooking(bookingId, payload) };
} catch (err: any) {
const detail = err?.response?.data?.detail || err.message;
// If room changed and capacity error, try booking first then invitees
if (
roomChanged &&
typeof detail === "string" &&
detail.toLowerCase().includes("room capacity") &&
!triedRoomFirst
) {
triedRoomFirst = true;
// Try booking update first, then invitees
try {
res = { data: await updateBooking(bookingId, payload) };
await updateInvitees(bookingId);
} catch (err2: any) {
const detail2 = err2?.response?.data?.detail || err2.message;
setSubmitError(formatBookingError(detail2));
logger.error(
"[BookingForm] Booking submit error (room-first retry)",
detail2
);
logger.error(
"[BookingForm] Booking submit error (room-first retry)",
detail2
);
return;
}
} else {
setSubmitError(formatBookingError(detail));
logger.error("[BookingForm] Booking submit error", detail);
logger.error("[BookingForm] Booking submit error", detail);
logger.error("[BookingForm] Booking submit error", detail);
return;
}
}
} else {
// For new booking, create booking first, then add invitees
const bookingData = await createBooking(payload);
bookingId = bookingData.id;
for (const email of uniqueInvitees) {
await addInvitee(bookingId, email);
}
res = { data: bookingData };
}
// On success, redirect to confirmation page with booking data
const bookingData = {
...res?.data,
room: rooms.find((r: any) => String(r.id) === String(room_id)),
invitees: uniqueInvitees,
// Prepare payload for booking creation
const payload = {
room_id,
start_time: new Date(start).toISOString(),
end_time: new Date(end).toISOString(),
title,
};
navigate("/confirmation", { state: { booking: bookingData } });
} catch (err: any) {
// Show a user-friendly error for booking overlap/conflict or capacity
let detail = "";
if (err?.response) {
// Backend responded, try to extract error detail
detail =
err.response.data?.detail ||
err.response.statusText ||
"Server error.";
} else if (err?.message) {
// Network/transport error
if (err.message === "Network Error") {
detail =
"Unable to reach the server. Please check your connection or try again later.";
} else {
detail = err.message;
}
// Call createBooking API
const result = await import("../apis/bookings").then((mod) =>
mod.createBooking(payload)
);
logger.info("[BookingForm] Booking created", result);
if (onBookingSuccess) {
onBookingSuccess();
} else {
detail = "An unknown error occurred.";
onClose();
}
setSubmitError(formatBookingError(detail));
logger.error("[BookingForm] Booking submit error", detail);
} catch (err: unknown) {
let detail = "";
if (typeof err === "object" && err !== null && "response" in err) {
// @ts-ignore
detail = err.response?.data?.detail || err.message;
} else if (typeof err === "object" && err !== null && "message" in err) {
// @ts-ignore
detail = err.message;
}
logger.error("[BookingForm] Booking create error", detail);
setSubmitError(detail || "Failed to create booking.");
} finally {
setSubmitting(false);
}
@@ -605,36 +453,44 @@ const BookingForm: React.FC<BookingFormProps> = ({
if (!isEdit) return;
if (!window.confirm("Delete this booking?")) return;
try {
logger.info("[BookingForm] Deleting booking", { id: editBooking.id });
logger.info("[BookingForm] Deleting booking", {
id: editBooking?.id,
editBooking,
});
if (!editBooking?.id) {
setSubmitError("No booking ID provided for deletion.");
setSubmitting(false);
return;
}
setSubmitting(true);
await deleteBooking(editBooking.id);
setSubmitting(false);
// Booking deleted successfully
if (onBookingSuccess) {
onBookingSuccess();
} else {
onClose();
}
} catch (err: any) {
const detail = err?.response?.data?.detail || err.message;
// Failed to delete booking
} catch (err: unknown) {
setSubmitting(false);
let detail = "";
if (typeof err === "object" && err !== null && "response" in err) {
// @ts-ignore
detail = err.response?.data?.detail || err.message;
} else if (typeof err === "object" && err !== null && "message" in err) {
// @ts-ignore
detail = err.message;
}
logger.error("[BookingForm] Booking delete error", detail);
logger.error("[BookingForm] Booking delete error", detail);
/**
* BookingForm component for selecting room, date, time, title, and invitees.
* Handles both creation and editing of bookings.
*
* @param {object} props - Component props
* @param {boolean} props.open - Whether the form dialog is open
* @param {function} props.onClose - Callback to close the form
* @param {function} props.onBookingSuccess - Callback on successful booking
* @param {object[]} props.rooms - List of available rooms
* @param {object[]} props.invitees - List of available invitees
* @param {object|null} [props.editBooking] - Booking to edit, or null for new
* @param {object|null} [props.formSlot] - Slot info for new booking
* @param {boolean} [props.isEdit] - True if editing an existing booking
*/
setSubmitError(detail || "Failed to delete booking.");
}
};
const handleRoomChange = (e: SelectChangeEvent) => {
userChangedRoom.current = true;
setRoomId(String(e.target.value));
};
return (
<Dialog
open={open}
@@ -660,7 +516,13 @@ const BookingForm: React.FC<BookingFormProps> = ({
<CircularProgress />
</Box>
)}
<DialogTitle>{isEdit ? "Update Booking" : "New Booking"}</DialogTitle>
<DialogTitle>
{isViewMode
? "View Booking"
: isEdit
? "Update Booking"
: "New Booking"}
</DialogTitle>
<form onSubmit={handleSubmit}>
<DialogContent>
{submitError && (
@@ -693,11 +555,12 @@ const BookingForm: React.FC<BookingFormProps> = ({
onChange={handleRoomChange}
input={
<OutlinedInput
label={"Room "} // Match label width to visible label + icon + space
inputProps={{ "aria-label": "Room" }}
label={"Room "}
inputProps={{ "aria-label": "Room", readOnly: isViewMode }}
/>
}
required
disabled={!!isViewMode}
>
{rooms && rooms.length > 0 ? (
rooms.map((room: ConferenceRoom) => {
@@ -749,7 +612,9 @@ const BookingForm: React.FC<BookingFormProps> = ({
<RoomDetailsModal
open={roomModalOpen}
onClose={() => setRoomModalOpen(false)}
room={rooms.find((r: any) => String(r.id) === String(room_id))}
room={rooms.find(
(r: ConferenceRoom) => String(r.id) === String(room_id)
)}
/>
<TextField
label="Title (optional)"
@@ -757,24 +622,29 @@ const BookingForm: React.FC<BookingFormProps> = ({
onChange={(e) => setTitle(e.target.value)}
fullWidth
margin="normal"
InputProps={{ readOnly: !!isViewMode }}
disabled={!!isViewMode}
/>
<TextField
label="Start Time"
type="datetime-local"
value={start ? formatLocalDateTimeInput(new Date(start)) : ""}
value={(() => {
if (!start) return "";
const d = new Date(start);
if (isNaN(d.getTime())) return "";
// Format as YYYY-MM-DDTHH:mm (local time)
const pad = (n: number) => n.toString().padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
d.getDate()
)}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
})()}
onChange={(e) => {
userChangedStart.current = true;
const value = e.target.value;
if (value) {
const [datePart, timePart] = value.split("T");
const [year, month, day] = datePart.split("-").map(Number);
const [hour, minute] = timePart.split(":").map(Number);
const localDate = new Date(year, month - 1, day, hour, minute);
// Calculate new end time using current interval
setStart(localDate.toISOString());
const newEnd = new Date(
localDate.getTime() + intervalMinutes * 60000
);
setStart(new Date(value).toISOString());
const localDate = new Date(value);
const newEnd = new Date(localDate.getTime() + 30 * 60000);
setEnd(newEnd.toISOString());
} else {
setStart("");
@@ -782,33 +652,35 @@ const BookingForm: React.FC<BookingFormProps> = ({
}
}}
error={!!errors.start}
helperText={errors.start}
helperText={""}
fullWidth
margin="normal"
required
InputProps={{ readOnly: !!isViewMode }}
disabled={!!isViewMode}
/>
{/* Always render the error message area for start time, even if untouched */}
<Box color="error.main" fontSize={13} mb={1}>
{errors.start || (!start ? "Start time is required." : "")}
{errors.start}
</Box>
<TextField
label="End Time"
type="datetime-local"
value={end ? formatLocalDateTimeInput(new Date(end)) : ""}
value={(() => {
if (!end) return "";
const d = new Date(end);
if (isNaN(d.getTime())) return "";
// Format as YYYY-MM-DDTHH:mm (local time)
const pad = (n: number) => n.toString().padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
d.getDate()
)}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
})()}
onChange={(e) => {
const value = e.target.value;
const date = new Date(value);
if (!isNaN(date.getTime())) {
setEnd(date.toISOString());
// Update interval to match new end time
if (start) {
const startDate = new Date(start);
const newInterval = Math.max(
1,
Math.round((date.getTime() - startDate.getTime()) / 60000)
);
setIntervalMinutes(newInterval);
}
} else {
setEnd("");
}
@@ -817,6 +689,8 @@ const BookingForm: React.FC<BookingFormProps> = ({
margin="normal"
required
error={!!errors.end}
InputProps={{ readOnly: !!isViewMode }}
disabled={!!isViewMode}
/>
{/* Always render the error message area for end time, even if untouched */}
<Box color="error.main" fontSize={13} mb={1}>
@@ -829,13 +703,15 @@ const BookingForm: React.FC<BookingFormProps> = ({
multiple
value={invitees}
onChange={(e) =>
setInvitees(
typeof e.target.value === "string"
? e.target.value.split(",")
: e.target.value
)
isViewMode
? undefined
: setInvitees(
typeof e.target.value === "string"
? e.target.value.split(",")
: e.target.value
)
}
input={<OutlinedInput label="Invitees" />}
input={<OutlinedInput label="Invitees" readOnly={!!isViewMode} />}
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{(selected as string[]).map((value, idx) => (
@@ -847,6 +723,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
))}
</Box>
)}
disabled={!!isViewMode}
>
{/* Remaining slots as a non-selectable MenuItem at the top */}
<MenuItem
@@ -927,24 +804,30 @@ const BookingForm: React.FC<BookingFormProps> = ({
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={() => onClose()}>Cancel</Button>
{isEdit && (
<Button color="error" onClick={handleDelete}>
{!isViewMode && <Button onClick={() => onClose()}>Cancel</Button>}
{isEdit && !isViewMode && (
<Button
color="error"
onClick={() => {
logger.info("[BookingForm] Delete button clicked", {
id: editBooking?.id,
});
handleDelete();
}}
>
Delete
</Button>
)}
<Button
type="submit"
variant="contained"
disabled={submitting || Object.keys(errors).length > 0}
disabled={
isViewMode ? false : submitting || Object.keys(errors).length > 0
}
>
{/* Show invitee capacity error if present */}
{errors.invitees && (
<Box color="error.main" fontSize={13} mb={1} mt={1}>
{errors.invitees}
</Box>
)}
{submitting
{isViewMode
? "Close"
: submitting
? isEdit
? "Updating..."
: "Booking..."
+78 -23
View File
@@ -18,18 +18,41 @@ import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { EventInput } from "@fullcalendar/core";
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;
};
};
/**
* Props for CalendarView.
*/
interface SlotInfo {
start: Date;
end: Date;
allDay: boolean;
viewType: string;
selectedRoomId?: string | number;
}
interface CalendarViewProps {
events: EventInput[];
onEventClick?: (event: EventInput) => void;
onSlotSelect?: (slotInfo: any) => void;
selectedRoomId?: any;
events: BookingEvent[];
onEventClick?: (event: BookingEvent) => void;
onSlotSelect?: (slotInfo: SlotInfo) => void;
selectedRoomId?: string | number;
officeStartHour?: number;
officeEndHour?: number;
initialDate?: string;
@@ -47,28 +70,49 @@ const CalendarView: React.FC<CalendarViewProps> = ({
initialDate,
}) => {
// Custom event content with tooltip
const renderEventContent = (arg: any) => {
const renderEventContent = (arg: { event: BookingEvent }) => {
const { event } = arg;
const { title, start, end, extendedProps } = event;
const room =
extendedProps?.resource?.room_name ||
extendedProps?.resource?.roomId ||
"Unknown";
const invitees = extendedProps?.resource?.invitees || [];
const displayText = getEventDisplayText(title, start, room);
const roomClass = getRoomClass(extendedProps?.resource?.room_id);
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 safeTitle: string = typeof title === "string" ? title : "";
const safeStart: Date | null = start
? new Date(start as string | number | Date)
: null;
const safeEnd: Date | null = end
? new Date(end as string | number | Date)
: null;
const displayText = getEventDisplayText(
safeTitle,
safeStart ?? new Date(),
room
);
const roomClass = getRoomClass(resource?.room_id ?? null);
return (
<Tooltip
title={
<div>
<Typography variant="subtitle2">
{title && title.trim() !== "" ? title : room}
{safeTitle && safeTitle.trim() !== "" ? safeTitle : room}
</Typography>
<Typography variant="body2">
<b>Start:</b> {start ? new Date(start).toLocaleString() : "-"}
<b>Start:</b> {safeStart ? safeStart.toLocaleString() : "-"}
</Typography>
<Typography variant="body2">
<b>End:</b> {end ? new Date(end).toLocaleString() : "-"}
<b>End:</b> {safeEnd ? safeEnd.toLocaleString() : "-"}
</Typography>
<Typography variant="body2">
<b>Room:</b> {room}
@@ -103,6 +147,7 @@ const CalendarView: React.FC<CalendarViewProps> = ({
borderRadius: 4,
paddingLeft: 4,
paddingRight: 4,
background: undefined, // Remove inline background to allow CSS class to control color
}}
>
<span
@@ -125,7 +170,7 @@ const CalendarView: React.FC<CalendarViewProps> = ({
};
// Always show full 24h grid in week/day view
const calendarRef = React.useRef<any>(null);
const calendarRef = React.useRef<FullCalendar | null>(null);
let slotMinTime = "00:00:00";
let slotMaxTime = "24:00:00";
const businessHours = [
@@ -135,6 +180,10 @@ const CalendarView: React.FC<CalendarViewProps> = ({
endTime: businessEnd,
},
];
// Diagnostic: log events passed to FullCalendar
React.useEffect(() => {
logger.info("[CalendarView] Events passed to FullCalendar:", events);
}, [events]);
return (
<div className="calendarContainer">
<FullCalendar
@@ -143,7 +192,7 @@ const CalendarView: React.FC<CalendarViewProps> = ({
timeZone="local"
initialView="dayGridMonth"
initialDate={initialDate}
events={events}
events={events.map((e) => ({ ...e, id: String(e.id) }))}
height="auto"
dayMaxEvents={true}
headerToolbar={{
@@ -152,12 +201,17 @@ const CalendarView: React.FC<CalendarViewProps> = ({
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
eventContent={renderEventContent}
eventClassNames={undefined}
eventClick={(info) => {
info.jsEvent.preventDefault();
if (onEventClick) {
onEventClick(
info.event.extendedProps.originalBooking || info.event
);
// 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);
}
}}
selectable={true}
@@ -176,7 +230,8 @@ const CalendarView: React.FC<CalendarViewProps> = ({
}
}}
// @ts-ignore: dateClick is a valid prop for FullCalendar, but types may be missing
dateClick={(info: any) => {
// @ts-ignore: dateClick is a valid prop for FullCalendar, but types may be missing
dateClick={(info: { date: Date }) => {
if (onSlotSelect) {
// Simulate a slot selection for the whole day in month view
const start = info.date;
+48 -20
View File
@@ -1,30 +1,55 @@
// booking.ts
// Utility functions for booking-related logic
// Utility functions for booking-related logic in booking system frontend
export function getEditBookingRoomId(editBooking: any): string {
let id =
editBooking?._def?.extendedProps?.resource?.room_id ??
editBooking?._def?.extendedProps?.resource?.id ??
editBooking?.resource?.room_id ??
editBooking?.room_id ??
editBooking?.room?.id ??
(typeof editBooking === "object" && "roomId" in editBooking
? editBooking.roomId
: undefined) ??
(typeof editBooking === "object" &&
// Minimal Booking type for utility functions
import type { Booking } from "../schemas";
/**
* Extracts the room ID from a booking object, handling multiple possible shapes.
*/
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 &&
editBooking.room &&
"id" in editBooking.room
? editBooking.room.id
: undefined) ??
"";
if (!id && typeof editBooking === "object") {
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[k] === "number"
typeof (editBooking as any)[k] === "number"
) {
id = editBooking[k];
// @ts-ignore
id = (editBooking as any)[k];
break;
}
}
@@ -32,6 +57,9 @@ export function getEditBookingRoomId(editBooking: any): string {
return id !== undefined && id !== null ? String(id) : "";
}
/**
* Formats backend error details into user-friendly messages for booking actions.
*/
export function formatBookingError(detail: string): string {
if (!detail) return "An unknown error occurred.";
const lower = detail.toLowerCase();
+41 -7
View File
@@ -1,23 +1,57 @@
// bookingList.ts
// Utility functions for BookingList component
export function sortBookingsByStartTime(bookings: any[]): any[] {
/**
* Represents a booking object with a start_time property.
*/
import type { Booking as SharedBooking } from "../schemas";
export type Booking = SharedBooking;
/**
* Sorts bookings by their start time (ascending).
* @param bookings - Array of Booking objects
* @returns Sorted array of Booking objects
*/
export function sortBookingsByStartTime(bookings: Booking[]): Booking[] {
return [...bookings].sort(
(a, b) =>
new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
);
}
/**
* Formats a booking time string to a human-readable time.
* @param dateStr - ISO date string
* @returns Formatted time string
*/
export function formatBookingTime(dateStr: string): string {
const d = new Date(dateStr);
return d.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
return isNaN(d.getTime())
? "Invalid time"
: d.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
}
export function getInviteeName(inv: any): string {
/**
* Represents an invitee object.
*/
export interface Invitee {
name?: string;
username?: string;
displayName?: string;
user?: { name?: string };
}
/**
* Extracts the display name for an invitee.
* @param inv - Invitee object or string
* @returns Invitee name string
*/
export function getInviteeName(inv: Invitee | string): string {
if (typeof inv === "string") return inv;
if (inv && typeof inv === "object") {
if (typeof inv.name === "string") return inv.name;
+44 -12
View File
@@ -1,20 +1,52 @@
// calendar.ts
// Utility functions for CalendarView component
// Utility functions for CalendarView and booking system frontend
/**
* Returns display text for a calendar event, including start time and title/room.
* @param title - Event title (string, optional)
* @param start - Event start time (Date or ISO string, optional)
* @param room - Room name (string, optional)
* @returns Formatted display string
*/
export function getEventDisplayText(
title: string,
start: Date,
room: string
title: string | null,
start: Date | string | null,
room: string | null
): string {
const startTime = start
? start.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
: "-";
return title && title.trim() !== ""
? `${startTime} - ${title}`
: `${startTime} - ${room}`;
let startTime = "-";
if (start) {
let d: Date | null = null;
if (typeof start === "string") {
const parsed = Date.parse(start);
d = isNaN(parsed) ? null : new Date(parsed);
} else if (start instanceof Date) {
d = start;
}
if (d && !isNaN(d.getTime())) {
startTime = d.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
});
}
}
if (title && title.trim() !== "") {
return `${startTime} - ${title}`;
}
return `${startTime} - ${room ?? "Room"}`;
}
export function getRoomClass(roomId: any): string {
const idx = roomId ? parseInt(roomId, 10) % 8 : 0;
/**
* Returns a CSS class for a room color, based on room ID.
* @param roomId - Room ID (string, number, or null)
* @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;
}
return `room-color-${idx}`;
}
+14 -2
View File
@@ -1,11 +1,23 @@
// room.ts
// Utility functions for RoomList component
import { ConferenceRoom } from "../schemas";
export function getRoomSecondaryText(room: any): string {
export function getRoomSecondaryText(room: ConferenceRoom): string {
return `Location: ${room.location} | Capacity: ${room.capacity} | Equipment: ${room.equipment}`;
}
export function getRoomListItemStyles(roomId: any, selectedRoomId: any): any {
interface RoomListItemStyles {
backgroundColor?: string;
fontWeight?: number;
color: string;
border?: string;
borderRadius?: number;
}
export function getRoomListItemStyles(
roomId: number,
selectedRoomId?: number
): RoomListItemStyles {
if (roomId === selectedRoomId) {
return {
backgroundColor: "#ececec !important",
+68 -27
View File
@@ -1,30 +1,59 @@
// validation.ts
// Independent field validation helpers for BookingForm
// Independent field validation helpers for BookingForm and booking system frontend
export function validateRoomId(room_id: string | undefined): string | null {
import type { Booking } from "../schemas";
/**
* Validates room ID field.
* @param room_id - Room ID string or null
* @returns Error message or null
*/
export function validateRoomId(room_id: string | null): string | null {
if (!room_id) return "Room is required.";
return null;
}
export function validateStart(start: string | undefined): string | null {
/**
* Validates start time field.
* @param start - Start time string or null
* @returns Error message or null
*/
export function validateStart(start: string | null): string | null {
if (!start) return "Start time is required.";
if (start && new Date(start) < new Date())
return "Start time cannot be in the past.";
const d = new Date(start);
if (isNaN(d.getTime())) return "Invalid start time.";
if (d < new Date()) return "Start time cannot be in the past.";
return null;
}
/**
* Validates end time field.
* @param end - End time string or null
* @param start - Start time string or null
* @returns Error message or null
*/
export function validateEnd(
end: string | undefined,
start?: string
end: string | null,
start?: string | null
): string | null {
if (!end) return "End time is required.";
if (start && end && new Date(start) >= new Date(end))
return "End time must be after start time.";
if (end && new Date(end) < new Date())
return "End time cannot be in the past.";
const dEnd = new Date(end);
if (isNaN(dEnd.getTime())) return "Invalid end time.";
if (start) {
const dStart = new Date(start);
if (isNaN(dStart.getTime())) return "Invalid start time.";
if (dStart >= dEnd) return "End time must be after start time.";
}
if (dEnd < new Date()) return "End time cannot be in the past.";
return null;
}
/**
* Validates invitees field against room capacity.
* @param invitees - Array of invitee strings
* @param roomCapacity - Room capacity number
* @returns Error message or null
*/
export function validateInvitees(
invitees: string[],
roomCapacity: number
@@ -35,6 +64,11 @@ export function validateInvitees(
return null;
}
/**
* Validates a single invitee email address.
* @param email - Email string
* @returns Error message or null
*/
export function validateInviteeEmail(email: string): string | null {
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
return `Invalid email: ${email}`;
@@ -42,36 +76,43 @@ export function validateInviteeEmail(email: string): string | null {
return null;
}
/**
* Validates room availability for a given time range against existing bookings.
* @param room_id - Room ID string
* @param start - Start time string
* @param end - End time string
* @param roomBookings - Array of Booking objects
* @param isEdit - Is editing existing booking
* @param editBooking - The booking being edited, or null
* @returns Error object with roomError, startError, endError
*/
export function validateRoomAvailability(
room_id: string,
start: string,
end: string,
roomBookings: any[],
roomBookings: Booking[],
isEdit: boolean,
editBooking: any
editBooking: Booking | null
): { roomError?: string; startError?: string; endError?: string } {
// Filter out the current booking if editing
const filteredBookings =
isEdit && editBooking?.id
? roomBookings.filter((b: any) => b.id !== editBooking.id)
? roomBookings.filter((b: Booking) => b.id !== editBooking.id)
: roomBookings;
let startConflict = false;
let endConflict = false;
filteredBookings.forEach((b: any) => {
if (
new Date(start) < new Date(b.end_time) &&
new Date(end) > new Date(b.start_time)
) {
if (
new Date(start) < new Date(b.end_time) &&
new Date(start) >= new Date(b.start_time)
) {
filteredBookings.forEach((b: Booking) => {
const bStart = new Date(b.start_time);
const bEnd = new Date(b.end_time);
if (isNaN(bStart.getTime()) || isNaN(bEnd.getTime())) return;
const dStart = new Date(start);
const dEnd = new Date(end);
if (isNaN(dStart.getTime()) || isNaN(dEnd.getTime())) return;
if (dStart < bEnd && dEnd > bStart) {
if (dStart < bEnd && dStart >= bStart) {
startConflict = true;
}
if (
new Date(end) > new Date(b.start_time) &&
new Date(end) <= new Date(b.end_time)
) {
if (dEnd > bStart && dEnd <= bEnd) {
endConflict = true;
}
}
+66 -19
View File
@@ -17,7 +17,7 @@ import {
FormControl,
Box,
} from "@mui/material";
import { EventInput } from "@fullcalendar/core";
import { getRooms } from "../apis/rooms";
import { getUsers } from "../apis/users";
import { getRoomBookings } from "../apis/bookings";
@@ -31,7 +31,6 @@ const BookingPage: React.FC = () => {
const [formOpen, setFormOpen] = useState(false);
// State for room details modal (page-level)
const [roomModalOpen, setRoomModalOpen] = useState(false);
// ...existing code...
// Log mount
useEffect(() => {
logger.info("[BookingPage] Mounted");
@@ -164,19 +163,50 @@ const BookingPage: React.FC = () => {
}, [rooms]);
// Combine all bookings into calendar events, sorted chronologically (room id tiebreaker), with color and formatted title
const events: EventInput[] = useMemo(() => {
// Expose allBookings for event lookup
const allBookings: any[] = React.useMemo(() => {
if (!bookingsByRoom || rooms.length === 0) return [];
// Flatten all bookings with room info
const allBookings = Object.entries(bookingsByRoom).flatMap(
return Object.entries(bookingsByRoom).flatMap(
([roomId, bookings]) => bookings as any[]
);
}, [bookingsByRoom, rooms]);
const events: any[] = 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) => {
const room = rooms.find((r: any) => r.id === Number(roomId));
// Use backend's snake_case property names
// Normalize roomId for lookup
const normalizedRoomId = String(roomId);
const room = rooms.find(
(r: any) => String(r.id) === normalizedRoomId
);
let roomName = room?.name;
if (!roomName || roomName.trim() === "") {
if (
typeof booking.room_name === "string" &&
booking.room_name.trim() !== ""
) {
roomName = booking.room_name;
} else if (
typeof booking.room_id === "string" &&
booking.room_id.trim() !== ""
) {
roomName = booking.room_id;
} else if (typeof booking.room_id === "number") {
roomName = String(booking.room_id);
} else {
roomName = "Room";
}
}
const eventColor = roomColors[normalizedRoomId] || "#1976d2";
const start = new Date(booking.start_time);
const end = new Date(booking.end_time);
const displayTitle =
booking.title && booking.title.trim() !== "" ? booking.title : "";
// Extract invitee names from booking.invitees[].user.name
booking.title && booking.title.trim() !== ""
? booking.title
: roomName;
let inviteeNames: string[] = [];
if (Array.isArray(booking.invitees)) {
inviteeNames = booking.invitees
@@ -188,18 +218,18 @@ const BookingPage: React.FC = () => {
title: displayTitle,
start,
end,
color: roomColors[roomId],
color: eventColor,
backgroundColor: eventColor,
resource: {
room_id: booking.room_id,
room_name: room?.name,
room_name: roomName,
invitees: inviteeNames,
originalBooking: booking,
},
};
})
);
// Sort by start time, then by room id
allBookings.sort((a, b) => {
mappedEvents.sort((a, b) => {
const aStart = a.start.getTime();
const bStart = b.start.getTime();
if (aStart !== bStart) return aStart - bStart;
@@ -207,7 +237,7 @@ const BookingPage: React.FC = () => {
String(b.resource.room_id)
);
});
return allBookings;
return mappedEvents;
}, [bookingsByRoom, rooms, roomColors]);
const [formSlot, setFormSlot] = useState<any>(null);
@@ -351,9 +381,26 @@ const BookingPage: React.FC = () => {
setFormOpen(true);
};
const handleSelectEvent = (event: EventInput) => {
logger.debug("[BookingPage] Event selected for edit", event);
setEditBooking(event);
const handleSelectEvent = (event: any) => {
const originalBooking = event.resource?.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
);
if (foundBooking) {
setEditBooking(foundBooking);
} else {
setEditBooking({
...event,
start_time: event.start,
end_time: event.end,
});
}
}
setFormSlot(null);
setFormOpen(true);
};
@@ -463,8 +510,8 @@ const BookingPage: React.FC = () => {
room={rooms.find((r: any) => r.id === selectedRoomId)}
/>
<CalendarView
events={events}
onEventClick={(event) => handleSelectEvent(event)}
events={events as any}
onEventClick={(event: any) => handleSelectEvent(event)}
onSlotSelect={handleSelectSlot}
selectedRoomId={selectedRoomId}
/>
+1 -1
View File
@@ -43,7 +43,7 @@ const ConfirmationPage: React.FC = () => {
return () => {
es.close();
};
}, [booking?.id]);
}, [booking, booking?.id]);
const [editing, setEditing] = useState(false);
// Fetch all rooms for editing
-1
View File
@@ -55,7 +55,6 @@ const LandingPage: FC = () => {
return () => window.removeEventListener("beforeunload", handler);
}, []);
const navigate = useNavigate();
const today = new Date().toISOString().split("T")[0];
// Fetch available rooms from backend
const {
-33
View File
@@ -12,39 +12,6 @@
opacity: 0.5 !important;
}
/* Room color classes for calendar events */
.room-color-0 {
background: #1976d2 !important;
color: #fff !important;
}
.room-color-1 {
background: #388e3c !important;
color: #fff !important;
}
.room-color-2 {
background: #fbc02d !important;
color: #fff !important;
}
.room-color-3 {
background: #d32f2f !important;
color: #fff !important;
}
.room-color-4 {
background: #7b1fa2 !important;
color: #fff !important;
}
.room-color-5 {
background: #0288d1 !important;
color: #fff !important;
}
.room-color-6 {
background: #c2185b !important;
color: #fff !important;
}
.room-color-7 {
background: #ffa000 !important;
color: #fff !important;
}
/* Truncate and ellipsis for event content and titles */
.fc-event-title-ellipsis,
.fc-event .fc-event-main > div,