More fixes.

Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
2025-09-30 12:08:16 -04:00
parent 402d7ad9c4
commit 0ee9767859
24 changed files with 213 additions and 94 deletions

View File

@@ -65,6 +65,7 @@ export default [
jest: "readonly",
test: "readonly",
global: "readonly",
console: "readonly",
},
},
rules: {

View File

@@ -37,10 +37,9 @@ describe("BookingForm", () => {
dispatchEvent() {
return true;
}
onmessage: ((this: EventSource, ev: MessageEvent) => unknown) | null =
null;
onerror: ((this: EventSource, ev: Event) => unknown) | null = null;
onopen: ((this: EventSource, ev: Event) => unknown) | null = null;
onmessage: (() => unknown) | null = null;
onerror: (() => unknown) | null = null;
onopen: (() => unknown) | null = null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.EventSource = EventSourceMock as any;

View File

@@ -1,5 +1,5 @@
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import RoomSelect from "../../components/RoomSelect";
describe("RoomSelect", () => {
@@ -9,10 +9,10 @@ describe("RoomSelect", () => {
});
it("calls onChange when a room is selected", () => {
const onChange = jest.fn();
render(<RoomSelect selectedRoomId={"1"} onChange={onChange} />);
const _onChange = jest.fn();
render(<RoomSelect selectedRoomId={"1"} onChange={_onChange} />);
// Simulate selection change if options exist
// fireEvent.change(screen.getByLabelText(/room/i), { target: { value: "2" } });
// expect(onChange).toHaveBeenCalledWith("2");
// expect(_onChange).toHaveBeenCalledWith("2");
});
});

View File

@@ -38,10 +38,9 @@ describe("BookingPage", () => {
dispatchEvent() {
return true;
}
onmessage: ((this: EventSource, ev: MessageEvent) => unknown) | null =
null;
onerror: ((this: EventSource, ev: Event) => unknown) | null = null;
onopen: ((this: EventSource, ev: Event) => unknown) | null = null;
onmessage: (() => unknown) | null = null;
onerror: (() => unknown) | null = null;
onopen: (() => unknown) | null = null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.EventSource = EventSourceMock as any;

View File

@@ -37,10 +37,9 @@ describe("ConfirmationPage", () => {
dispatchEvent() {
return true;
}
onmessage: ((this: EventSource, ev: MessageEvent) => unknown) | null =
null;
onerror: ((this: EventSource, ev: Event) => unknown) | null = null;
onopen: ((this: EventSource, ev: Event) => unknown) | null = null;
onmessage: (() => unknown) | null = null;
onerror: (() => unknown) | null = null;
onopen: (() => unknown) | null = null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.EventSource = EventSourceMock as any;

View File

@@ -39,10 +39,9 @@ describe("LandingPage", () => {
dispatchEvent() {
return true;
}
onmessage: ((this: EventSource, ev: MessageEvent) => unknown) | null =
null;
onerror: ((this: EventSource, ev: Event) => unknown) | null = null;
onopen: ((this: EventSource, ev: Event) => unknown) | null = null;
onmessage: (() => unknown) | null = null;
onerror: (() => unknown) | null = null;
onopen: (() => unknown) | null = null;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
window.EventSource = EventSourceMock as any;

View File

@@ -25,7 +25,9 @@ export function connectRoomsAvailabilityStream({
onError,
}: {
url?: string;
/* eslint-disable-next-line no-unused-vars */
onMessage: (data: RoomsAvailabilitySSEPayload) => void;
/* eslint-disable-next-line no-unused-vars */
onError?: (error: unknown) => void;
}) {
const es = new window.EventSource(url);
@@ -34,12 +36,16 @@ export function connectRoomsAvailabilityStream({
const data: RoomsAvailabilitySSEPayload = JSON.parse(event.data);
onMessage(data);
} catch (e) {
if (onError) onError(e);
if (onError) {
onError(e);
}
}
};
es.onerror = (err) => {
es.close();
if (onError) onError(err);
if (onError) {
onError(err);
}
};
return es;
}

View File

@@ -32,7 +32,9 @@ const BookingConfirmation: React.FC<BookingConfirmationProps> = ({
onEdit,
onBack,
}) => {
if (!booking) return null;
if (!booking) {
return null;
}
const { room, start_time, end_time, title, invitees } = booking;
return (
<Box

View File

@@ -51,7 +51,7 @@ interface BookingFormProps {
onClose: () => void;
editBooking?: Booking;
slotInfo?: { room_id?: string | number; start?: string };
onBookingSuccess?: (booking?: Booking) => void;
onBookingSuccess?: () => void;
}
const BookingForm: React.FC<BookingFormProps> = ({
@@ -97,9 +97,15 @@ const BookingForm: React.FC<BookingFormProps> = ({
// Room selection
const initialRoomId = useMemo(() => {
if (isEdit && editBooking) return getEditBookingRoomId(editBooking);
if (slotInfo?.room_id) return String(slotInfo.room_id);
if (rooms.length > 0) return String((rooms[0] as ConferenceRoom).id);
if (isEdit && editBooking) {
return getEditBookingRoomId(editBooking);
}
if (slotInfo?.room_id) {
return String(slotInfo.room_id);
}
if (rooms.length > 0) {
return String((rooms[0] as ConferenceRoom).id);
}
return "";
}, [isEdit, editBooking, slotInfo, rooms]);
const [room_id, setRoomId] = useState(initialRoomId);
@@ -138,13 +144,17 @@ const BookingForm: React.FC<BookingFormProps> = ({
const currentCandidate = candidate;
const currentCandidateEnd = candidateEnd;
const overlaps = roomBookings.some(function (b) {
if (isEdit && b.id === editBooking?.id) return false;
if (isEdit && b.id === editBooking?.id) {
return false;
}
return (
currentCandidate < new Date(b.end_time) &&
currentCandidateEnd > new Date(b.start_time)
);
});
if (!overlaps && candidate > now) return candidate;
if (!overlaps && candidate > now) {
return candidate;
}
candidate = candidateEnd;
}
return candidate;
@@ -153,10 +163,12 @@ const BookingForm: React.FC<BookingFormProps> = ({
);
const initialStart = useMemo(() => {
if (isEdit && editBooking?.start_time)
if (isEdit && editBooking?.start_time) {
return new Date(editBooking.start_time);
if (slotInfo?.start)
}
if (slotInfo?.start) {
return findNextAvailableStart(room_id, new Date(slotInfo.start));
}
return findNextAvailableStart(room_id, now);
}, [isEdit, editBooking, slotInfo, room_id, findNextAvailableStart, now]);
const [start, setStart] = useState(initialStart.toISOString());
@@ -164,7 +176,9 @@ const BookingForm: React.FC<BookingFormProps> = ({
// End time
const [customInterval, setCustomInterval] = useState<number | null>(null);
const initialEnd = useMemo(() => {
if (isEdit && editBooking?.end_time) return new Date(editBooking.end_time);
if (isEdit && editBooking?.end_time) {
return new Date(editBooking.end_time);
}
return new Date(
initialStart.getTime() + ENV.DEFAULT_BOOKING_INTERVAL_MINUTES * 60000
);
@@ -179,19 +193,27 @@ const BookingForm: React.FC<BookingFormProps> = ({
const newErrors: { [key: string]: string } = {};
// Room
const roomIdError = validateRoomId(room_id);
if (roomIdError) newErrors.room_id = roomIdError;
if (roomIdError) {
newErrors.room_id = roomIdError;
}
// Start
const startError = validateStart(start);
if (startError) newErrors.start = startError;
if (startError) {
newErrors.start = startError;
}
// End
const endError = validateEnd(end, start);
if (endError) newErrors.end = endError;
if (endError) {
newErrors.end = endError;
}
// Overlap
const roomBookings = bookings.filter(
(b: Booking) => String(b.room_id) === String(room_id)
);
const overlaps = roomBookings.some((b: Booking) => {
if (isEdit && b.id === editBooking?.id) return false;
if (isEdit && b.id === editBooking?.id) {
return false;
}
return (
new Date(start) < new Date(b.end_time) &&
new Date(end) > new Date(b.start_time)
@@ -203,7 +225,9 @@ const BookingForm: React.FC<BookingFormProps> = ({
}
// Invitees
const inviteesError = validateInvitees(invitees, roomCapacity);
if (inviteesError) newErrors.invitees = inviteesError;
if (inviteesError) {
newErrors.invitees = inviteesError;
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}
@@ -224,7 +248,9 @@ const BookingForm: React.FC<BookingFormProps> = ({
setEnd(value);
const interval =
(new Date(value).getTime() - new Date(start).getTime()) / 60000;
if (interval > 0) setCustomInterval(interval);
if (interval > 0) {
setCustomInterval(interval);
}
validateFields();
}
function handleInviteeChange(event: SelectChangeEvent<typeof invitees>) {
@@ -245,7 +271,9 @@ const BookingForm: React.FC<BookingFormProps> = ({
onClose();
return;
}
if (!validateFields()) return;
if (!validateFields()) {
return;
}
try {
let bookingResult;
if (!isEdit) {
@@ -269,9 +297,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
logger.info("[BookingForm] updateBooking payload:", updatePayload);
bookingResult = await updateBooking(editBooking.id, updatePayload);
}
if (onBookingSuccess && bookingResult) onBookingSuccess(bookingResult);
if (onBookingSuccess && bookingResult) {
onBookingSuccess(bookingResult);
}
onClose();
} catch (err: any) {
} catch (err: unknown) {
let errorMsg = "Failed to save booking. Please try again.";
if (err?.response?.data?.detail) {
errorMsg = err.response.data.detail;
@@ -308,7 +338,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
<RoomSelect
selectedRoomId={room_id}
onChange={(id) =>
handleRoomChange({ target: { value: id } } as any)
handleRoomChange({ target: { value: id } } as unknown)
}
label="Room"
minWidth={180}
@@ -332,9 +362,13 @@ const BookingForm: React.FC<BookingFormProps> = ({
label="Start Time"
type="datetime-local"
value={(() => {
if (!start) return "";
if (!start) {
return "";
}
const d = new Date(start);
if (isNaN(d.getTime())) return "";
if (isNaN(d.getTime())) {
return "";
}
const pad = (n: number) => n.toString().padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
d.getDate()
@@ -356,9 +390,13 @@ const BookingForm: React.FC<BookingFormProps> = ({
label="End Time"
type="datetime-local"
value={(() => {
if (!end) return "";
if (!end) {
return "";
}
const d = new Date(end);
if (isNaN(d.getTime())) return "";
if (isNaN(d.getTime())) {
return "";
}
const pad = (n: number) => n.toString().padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
d.getDate()
@@ -387,7 +425,9 @@ const BookingForm: React.FC<BookingFormProps> = ({
renderValue={(selected) => (
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
{(selected as string[]).map((value, idx) => {
const user = users.find((u: any) => u.email === value);
const user = users.find(
(u: import("../interfaces").User) => u.email === value
);
return (
<Chip
key={value}
@@ -416,12 +456,14 @@ const BookingForm: React.FC<BookingFormProps> = ({
} left`
: `${Math.abs(remainingSlots)} over room capacity`}
</MenuItem>
{users.map((user: any) => {
{users.map((user: import("../interfaces").User) => {
const email = user.email ?? user.name;
const isSelected = invitees.includes(email);
// User unavailable if booked for another event at this time
const unavailable = bookings.some((b: Booking) => {
if (isEdit && b.id === editBooking?.id) return false;
if (isEdit && b.id === editBooking?.id) {
return false;
}
return (
b.invitees?.includes(email) &&
new Date(start) < new Date(b.end_time) &&
@@ -486,9 +528,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
if (editBooking) {
try {
await deleteBooking(editBooking.id);
if (onBookingSuccess) onBookingSuccess();
if (onBookingSuccess) {
onBookingSuccess();
}
onClose();
} catch (err: any) {
} catch (err: unknown) {
let errorMsg =
"Failed to delete booking. Please try again.";
if (err?.response?.data?.detail) {

View File

@@ -32,7 +32,9 @@ const RoomDetailsModal: React.FC<RoomDetailsModalProps> = ({
room,
}) => {
// Lifecycle and debug logging removed
if (!room) return null;
if (!room) {
return null;
}
return (
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
<DialogTitle>Room Details</DialogTitle>

View File

@@ -25,6 +25,7 @@ import { logger } from "../utils/logger";
export interface RoomSelectProps {
selectedRoomId: string | number;
/* eslint-disable-next-line no-unused-vars */
onChange: (roomId: string | number) => void;
label?: string;
minWidth?: number;

View File

@@ -41,8 +41,9 @@ export const ENV = {
* @returns {string} The CSS variable value, or the fallback if not found.
*/
function getCssVar(name: string, fallback: string): string {
if (typeof window === "undefined" || !document.documentElement)
if (typeof window === "undefined" || !document.documentElement) {
return fallback;
}
const value = getComputedStyle(document.documentElement)
.getPropertyValue(name)
.trim();

View File

@@ -21,6 +21,7 @@ import type { Booking } from "../interfaces";
interface BookingContextType {
bookings: Booking[];
/* eslint-disable-next-line no-unused-vars */
fetchMonth: (month: string) => Promise<void>;
}
@@ -36,11 +37,12 @@ export const BookingContext = createContext<BookingContextType | undefined>(
*/
export const BookingProvider = ({ children }: { children: ReactNode }) => {
// Fetch bookings for a given month and merge into state
const fetchMonth = async (month: string) => {
try {
const newBookings = await getMonthBookings(month);
setBookingsByMonth((prev) => ({ ...prev, [month]: newBookings }));
} catch (err) {
} catch {
// Optionally handle error
}
};
@@ -75,8 +77,9 @@ export const BookingProvider = ({ children }: { children: ReactNode }) => {
const month = `${d.getFullYear()}-${String(
d.getMonth() + 1
).padStart(2, "0")}`;
if (!bookingsByMonthFromSSE[month])
if (!bookingsByMonthFromSSE[month]) {
bookingsByMonthFromSSE[month] = [];
}
bookingsByMonthFromSSE[month].push(b);
});
// SSE received bookings
@@ -158,7 +161,7 @@ export const BookingProvider = ({ children }: { children: ReactNode }) => {
});
}
},
onError: (err) => {
onError: () => {
// Optionally handle SSE errors
// SSE error
},

View File

@@ -60,6 +60,8 @@ export const RoomProvider: React.FC<{ children: ReactNode }> = ({
export const useRooms = () => {
const context = useContext(RoomContext);
if (!context) throw new Error("useRooms must be used within a RoomProvider");
if (!context) {
throw new Error("useRooms must be used within a RoomProvider");
}
return context;
};

View File

@@ -40,8 +40,9 @@ export function useAvailableUsers(params: {
const start = new Date(params.start_time).getTime();
const end = new Date(params.end_time).getTime();
const overlappingBookings = bookings.filter((b) => {
if (params.exclude_booking_id && b.id === params.exclude_booking_id)
if (params.exclude_booking_id && b.id === params.exclude_booking_id) {
return false;
}
const bStart = new Date(b.start_time).getTime();
const bEnd = new Date(b.end_time).getTime();
return bStart < end && bEnd > start;
@@ -95,6 +96,8 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({
export const useUsers = () => {
const context = useContext(UserContext);
if (!context) throw new Error("useUsers must be used within a UserProvider");
if (!context) {
throw new Error("useUsers must be used within a UserProvider");
}
return context;
};

View File

@@ -20,7 +20,9 @@ import type { Booking } from "../interfaces";
export function getEditBookingRoomId(
editBooking: Booking | undefined | null
): string {
if (!editBooking) return "";
if (!editBooking) {
return "";
}
// Only support standard Booking shape
return editBooking.room_id ? String(editBooking.room_id) : "";
}
@@ -31,7 +33,9 @@ export function getEditBookingRoomId(
* @returns User-friendly error message
*/
export function formatBookingError(detail: string): string {
if (!detail) return "An unknown error occurred.";
if (!detail) {
return "An unknown error occurred.";
}
const lower = detail.toLowerCase();
if (lower.includes("overlap")) {
return "The selected room is already booked for the chosen time. Please pick a different time or room.";

View File

@@ -53,12 +53,22 @@ export function formatBookingTime(dateStr: string): string {
* @returns Invitee name string
*/
export function getInviteeName(inv: Invitee | string): string {
if (typeof inv === "string") return inv;
if (typeof inv === "string") {
return inv;
}
if (inv && typeof inv === "object") {
if (typeof inv.name === "string") return inv.name;
if (typeof inv.username === "string") return inv.username;
if (typeof inv.displayName === "string") return inv.displayName;
if (inv.user && typeof inv.user.name === "string") return inv.user.name;
if (typeof inv.name === "string") {
return inv.name;
}
if (typeof inv.username === "string") {
return inv.username;
}
if (typeof inv.displayName === "string") {
return inv.displayName;
}
if (inv.user && typeof inv.user.name === "string") {
return inv.user.name;
}
}
return "";
}

View File

@@ -14,7 +14,9 @@ import type { Booking } from "../interfaces";
* @returns Error message if invalid, otherwise null
*/
export function validateRoomId(room_id: string | null): string | null {
if (!room_id) return "Room is required.";
if (!room_id) {
return "Room is required.";
}
return null;
}
@@ -24,10 +26,16 @@ export function validateRoomId(room_id: string | null): string | null {
* @returns Error message if invalid, otherwise null
*/
export function validateStart(start: string | null): string | null {
if (!start) return "Start time is required.";
if (!start) {
return "Start time is required.";
}
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.";
if (isNaN(d.getTime())) {
return "Invalid start time.";
}
if (d < new Date()) {
return "Start time cannot be in the past.";
}
return null;
}
@@ -41,15 +49,25 @@ export function validateEnd(
end: string | null,
start?: string | null
): string | null {
if (!end) return "End time is required.";
if (!end) {
return "End time is required.";
}
const dEnd = new Date(end);
if (isNaN(dEnd.getTime())) return "Invalid end time.";
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 (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.";
}
if (dEnd < new Date()) return "End time cannot be in the past.";
return null;
}
@@ -103,10 +121,14 @@ export function validateRoomAvailability(
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;
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 (isNaN(dStart.getTime()) || isNaN(dEnd.getTime())) {
return;
}
if (dStart < bEnd && dEnd > bStart) {
if (dStart < bEnd && dStart >= bStart) {
startConflict = true;

View File

@@ -34,8 +34,8 @@ export interface SlotInfo {
*/
export interface CalendarViewProps {
events: import("./types").BookingEvent[];
onEventClick?: (event: import("./types").BookingEvent) => void;
onSlotSelect?: (slotInfo: SlotInfo) => void;
onEventClick?: () => void;
onSlotSelect?: () => void;
selectedRoomId?: string | number;
officeStartHour?: number;
officeEndHour?: number;
@@ -133,11 +133,13 @@ export interface RoomDetailsModalProps {
export interface BookingListProps {
bookings: Booking[];
/* eslint-disable-next-line no-unused-vars */
onSelect?: (booking: Booking) => void;
}
export interface RoomListProps {
rooms: ConferenceRoom[];
selectedRoomId?: number;
/* eslint-disable-next-line no-unused-vars */
onSelectRoom?: (roomId: number) => void;
}

View File

@@ -86,7 +86,9 @@ const BookingPage: React.FC = () => {
* Flatten all bookings from all rooms into a single array.
*/
const allBookings: Booking[] = React.useMemo(() => {
if (!bookings) return [];
if (!bookings) {
return [];
}
return bookings;
}, [bookings]);
@@ -94,7 +96,9 @@ const BookingPage: React.FC = () => {
* Map all bookings to calendar event objects for display.
*/
const events = React.useMemo(() => {
if (!bookings || rooms.length === 0) return [];
if (!bookings || rooms.length === 0) {
return [];
}
const mappedEvents = bookings.map((booking: Booking) => {
const room = rooms.find(
(r: ConferenceRoom) => String(r.id) === String(booking.room_id)
@@ -206,14 +210,18 @@ const BookingPage: React.FC = () => {
// Clamp to office hours (local time)
const officeStart = 8;
const officeEnd = 18;
if (selectedDay.getHours() < officeStart)
if (selectedDay.getHours() < officeStart) {
selectedDay.setHours(officeStart, 0, 0, 0);
if (selectedDay.getHours() >= officeEnd)
}
if (selectedDay.getHours() >= officeEnd) {
selectedDay.setHours(officeEnd - 1, 45, 0, 0);
}
// 2. Find all bookings for this room on this day
const roomBookings = events.filter((e: BookingEvent) => {
if (e.room_id !== selectedRoomId) return false;
if (e.room_id !== selectedRoomId) {
return false;
}
const eventStart =
e.start &&
(typeof e.start === "string" ||
@@ -237,7 +245,9 @@ const BookingPage: React.FC = () => {
typeof b.end === "string" ||
typeof b.end === "number" ||
b.end instanceof Date;
if (!validStart || !validEnd) return undefined;
if (!validStart || !validEnd) {
return undefined;
}
const s = new Date(b.start as string | number | Date);
const e = new Date(b.end as string | number | Date);
return [s.getTime(), e.getTime()];

View File

@@ -67,10 +67,14 @@ const ConfirmationPage: React.FC = () => {
*/
React.useEffect(() => {
const bookingId = location.state?.booking?.id || booking?.id;
if (!bookingId) return;
if (!bookingId) {
return;
}
// Always get latest booking from context
const latestBooking = bookings.find((b: Booking) => b.id === bookingId);
if (latestBooking) setBooking(latestBooking);
if (latestBooking) {
setBooking(latestBooking);
}
const es = connectRoomsAvailabilityStream({
onMessage: (data) => {
if (Array.isArray(data.bookings)) {
@@ -84,7 +88,7 @@ const ConfirmationPage: React.FC = () => {
}
}
},
onError: (err) => {
onError: () => {
// Optionally handle SSE errors
},
});
@@ -115,8 +119,12 @@ const ConfirmationPage: React.FC = () => {
navigate("/booking");
return null;
}
if (roomsLoading) return <div>Loading rooms...</div>;
if (roomsError) return <div>Error loading rooms.</div>;
if (roomsLoading) {
return <div>Loading rooms...</div>;
}
if (roomsError) {
return <div>Error loading rooms.</div>;
}
const handleEdit = () => setEditing(true);
const handleBack = () => navigate("/booking");

View File

@@ -44,7 +44,7 @@ const LandingPage: FC = () => {
* Debug: log before page unload.
*/
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
const handler = () => {
// ...removed debug log...
};
window.addEventListener("beforeunload", handler);

View File

@@ -69,7 +69,7 @@ export interface RoomDetailsModalProps {
*/
export interface BookingListProps {
bookings: Booking[];
onSelect?: (booking: Booking) => void;
onSelect?: () => void;
}
/**
@@ -78,5 +78,5 @@ export interface BookingListProps {
export interface RoomListProps {
rooms: ConferenceRoom[];
selectedRoomId?: number;
onSelectRoom?: (roomId: number) => void;
onSelectRoom?: () => void;
}

View File

@@ -22,7 +22,9 @@ export function roundToStrictlyFutureQuarter(dt: Date): Date {
const copy = new Date(dt.getTime());
copy.setSeconds(0, 0);
let add = 15 - (copy.getMinutes() % 15);
if (add === 0) add = 15;
if (add === 0) {
add = 15;
}
copy.setMinutes(copy.getMinutes() + add);
return copy;
}