Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Checkbox, FormControlLabel, FormGroup, Tooltip } from "@mui/material";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import RadioButtonUncheckedIcon from "@mui/icons-material/RadioButtonUnchecked";
import dayjs from "dayjs";
import { useContext, useMemo } from "react";
import { FormContextLevel, RoomSetting } from "../../../../types";
Expand Down Expand Up @@ -29,8 +31,10 @@ export const SelectRooms = ({
setBookingCalendarInfo,
} = useContext(BookingContext);
const { isBookingTimeInBlackout } = useBookingDateRestrictions();
const { resources } = useTenantSchema();
const { resources, calendarConfig } = useTenantSchema();
const selectedIds = selected.map((room) => room.roomId);
const allowMultipleResourceSelect =
calendarConfig?.multipleResourceSelect ?? false;

// Sort rooms by room number for consistent display order
const sortedRooms = useMemo(
Expand Down Expand Up @@ -66,6 +70,12 @@ export const SelectRooms = ({

// walk-ins can only book 1 room unless it's 2 ballroom bays (221-224)
const isDisabled = (roomId: number) => {
if (!allowMultipleResourceSelect) {
if (selectedIds.length === 0) return false;
if (selectedIds.includes(roomId)) return false;
return true;
}

// Don't disable rooms for blackout periods - let the calendar handle time restrictions
// Only apply walk-in restrictions
if (formContext !== FormContextLevel.WALK_IN || selectedIds.length === 0)
Expand Down Expand Up @@ -116,6 +126,14 @@ export const SelectRooms = ({
const newVal: boolean = e.target.checked;
setSelected((prev: RoomSetting[]) => {
if (newVal) {
if (
!allowMultipleResourceSelect &&
prev.length > 0 &&
!prev.some((r) => r.roomId === room.roomId)
) {
return prev;
}

const newSelection = [...prev, room].sort(
(a, b) => a.roomId - b.roomId,
);
Expand All @@ -141,6 +159,12 @@ export const SelectRooms = ({
<Checkbox
checked={selectedIds.includes(room.roomId)}
onChange={(e) => handleCheckChange(e, room)}
icon={
allowMultipleResourceSelect ? undefined : <RadioButtonUncheckedIcon />
}
checkedIcon={
allowMultipleResourceSelect ? undefined : <CheckCircleIcon />
}
inputProps={{
"aria-label": `${room.roomId} ${room.name}`,
}}
Expand All @@ -153,7 +177,7 @@ export const SelectRooms = ({
sx={{
opacity: disabled ? 0.6 : 1,
"& .MuiFormControlLabel-label": {
textDecoration: disabled ? "line-through" : "none",
color: disabled ? "text.disabled" : "text.primary",
},
}}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export type SchemaContextType = {
calendarConfig?: {
startHour?: Record<string, string>; // e.g., { studentVIP: "06:00:00", student: "09:00:00", ... }
slotUnit?: Record<string, number>; // e.g., { student: 15, admin: 15, ... }
multipleResourceSelect?: boolean;
timeSensitiveRequestWarning?: TimeSensitiveRequestWarning;
};
// CC email addresses for notifications, per environment
Expand Down Expand Up @@ -302,6 +303,7 @@ export const defaultScheme: Omit<SchemaContextType, "tenant"> = {
adminVIP: 15,
adminWalkIn: 15,
},
multipleResourceSelect: false,
timeSensitiveRequestWarning: defaultTimeSensitiveRequestWarning,
},
schoolMapping: {},
Expand Down
17 changes: 17 additions & 0 deletions booking-app/components/src/client/routes/super/schemaEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,23 @@ function CalendarConfigSection({
</Box>
</Box>
)}
<Box mb={2}>
<FormControlLabel
control={
<Switch
checked={config.multipleResourceSelect ?? false}
onChange={(e) =>
onChange(
"calendarConfig.multipleResourceSelect",
e.target.checked,
)
}
size="small"
/>
}
label="Allow selecting multiple resources"
/>
</Box>
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>
Time Sensitive Request Warning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const baseMediaCommonsSchema: SchemaContextType = {
facultyVIP: 15,
adminVIP: 15,
},
multipleResourceSelect: false,
timeSensitiveRequestWarning: {
hours: 48,
isActive: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { FormContextLevel, RoomSetting } from "../../components/src/types";
import {
SchemaContext,
generateDefaultSchema,
} from "../../components/src/client/routes/components/SchemaProvider";
import { BookingContext } from "../../components/src/client/routes/booking/bookingProvider";
import { SelectRooms } from "../../components/src/client/routes/booking/components/SelectRooms";

vi.mock(
"../../components/src/client/routes/booking/hooks/useBookingDateRestrictions",
() => ({
useBookingDateRestrictions: () => ({
isBookingTimeInBlackout: () => ({ inBlackout: false, affectedPeriods: [] }),
}),
}),
);

const rooms: RoomSetting[] = [
{
roomId: 202,
name: "Lecture Hall",
capacity: "30",
calendarId: "cal-202",
isEquipment: false,
} as RoomSetting,
{
roomId: 203,
name: "Seminar Room",
capacity: "20",
calendarId: "cal-203",
isEquipment: false,
} as RoomSetting,
];

function TestHarness({ multipleResourceSelect }: { multipleResourceSelect: boolean }) {
const [selected, setSelected] = useState<RoomSetting[]>([]);
const schema = {
...generateDefaultSchema("mc"),
resources: rooms.map((r) => ({
roomId: r.roomId,
name: r.name,
capacity: Number(r.capacity),
calendarId: r.calendarId,
isEquipment: false,
isWalkIn: false,
isWalkInCanBookTwo: false,
services: [],
})),
calendarConfig: {
...generateDefaultSchema("mc").calendarConfig,
multipleResourceSelect,
},
};

return (
<SchemaContext.Provider value={schema as any}>
<BookingContext.Provider
value={{
hasShownMocapModal: false,
setHasShownMocapModal: () => {},
bookingCalendarInfo: undefined,
setBookingCalendarInfo: () => {},
} as any}
>
<SelectRooms
allRooms={rooms}
formContext={FormContextLevel.FULL_FORM}
selected={selected}
setSelected={setSelected}
/>
</BookingContext.Provider>
</SchemaContext.Provider>
);
}

describe("SelectRooms multipleResourceSelect", () => {
it("disables selecting a second resource when multipleResourceSelect is false", () => {
render(<TestHarness multipleResourceSelect={false} />);

const room202 = screen.getByRole("checkbox", {
name: "202 Lecture Hall",
}) as HTMLInputElement;
const room203 = screen.getByRole("checkbox", {
name: "203 Seminar Room",
}) as HTMLInputElement;

fireEvent.click(room202);

expect(room202.checked).toBe(true);
const room203AfterFirstSelection = screen.getByRole("checkbox", {
name: "203 Seminar Room",
}) as HTMLInputElement;
expect(room203AfterFirstSelection.disabled).toBe(true);

fireEvent.click(room203AfterFirstSelection);
expect(room203AfterFirstSelection.checked).toBe(false);
});

it("allows selecting multiple resources when multipleResourceSelect is true", () => {
render(<TestHarness multipleResourceSelect={true} />);

const room202 = screen.getByRole("checkbox", {
name: "202 Lecture Hall",
}) as HTMLInputElement;
const room203 = screen.getByRole("checkbox", {
name: "203 Seminar Room",
}) as HTMLInputElement;

fireEvent.click(room202);
fireEvent.click(room203);

expect(room202.checked).toBe(true);
expect(room203.disabled).toBe(false);
expect(room203.checked).toBe(true);
});
});
Loading