mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-08 08:28:18 -04:00
@@ -65,6 +65,7 @@ export default [
|
||||
jest: "readonly",
|
||||
test: "readonly",
|
||||
global: "readonly",
|
||||
console: "readonly",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()];
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user