Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

issue-171: Add a separate part in the rooms dropdown to show rooms not in preference #179

Merged
merged 1 commit into from
Jan 27, 2025
Merged
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
4 changes: 2 additions & 2 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiResponse, BookRoomDto, DeleteResponse, EventResponse, GetAvailableRoomsQueryDto, IConferenceRoom, StatusTypes } from '@quickmeet/shared';
import { ApiResponse, BookRoomDto, DeleteResponse, EventResponse, GetAvailableRoomsQueryDto, StatusTypes, IAvailableRooms } from '@quickmeet/shared';
import axios, { AxiosInstance } from 'axios';
import { toast } from 'react-hot-toast';
import { secrets } from '@config/secrets';
Expand Down Expand Up @@ -135,7 +135,7 @@ export default class Api {
const params: GetAvailableRoomsQueryDto = { startTime, duration, timeZone, seats, floor, eventId };
const res = await this.client.get('/api/rooms/available', { params, signal });

return res.data as ApiResponse<IConferenceRoom[]>;
return res.data as ApiResponse<IAvailableRooms>;
} catch (error: any) {
return this.handleError(error);
}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/AttendeeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isEmailValid } from '@/helpers/utility';
import PeopleAltRoundedIcon from '@mui/icons-material/PeopleAltRounded';
import { Autocomplete, Box, Chip, debounce, TextField, Typography } from '@mui/material';
import Avatar from '@mui/material/Avatar';
import type { IPeopleInformation } from '@quickmeet/shared';
import { IPeopleInformation } from '@quickmeet/shared';
import { useState } from 'react';
import toast from 'react-hot-toast';

Expand Down
318 changes: 173 additions & 145 deletions client/src/components/RoomsDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Box, MenuItem, Select, SelectChangeEvent, Skeleton, Typography } from '@mui/material';
import { Box, ListSubheader, MenuItem, Select, SelectChangeEvent, Skeleton, styled, Typography } from '@mui/material';
import { ReactElement } from 'react';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { IConferenceRoom } from '@quickmeet/shared';
import { IAvailableRoomsDropdownOption } from '@/helpers/types';
import { usePreferences } from '@/context/PreferencesContext';

interface DropdownProps {
id: string;
sx?: any;
options?: RoomsDropdownOption[];
options: IAvailableRoomsDropdownOption;
value?: string;
disabled?: boolean;
onChange: (id: string, value: string) => void;
Expand All @@ -16,6 +18,12 @@ interface DropdownProps {
currentRoom?: IConferenceRoom;
}

interface StyledMenuItemProps {
i: number;
option: RoomsDropdownOption;
currentRoom?: IConferenceRoom;
}

export interface RoomsDropdownOption {
text: string;
value: string; // the main value used for api calls
Expand All @@ -24,8 +32,148 @@ export interface RoomsDropdownOption {
isBusy?: boolean;
}

const CustomMenuItem = ({ i, option, currentRoom }: StyledMenuItemProps) => {
return (
<MenuItem value={option.value} key={i}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
{/* Left section */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<Typography
variant="subtitle1"
sx={{
textDecoration: option.isBusy ? 'line-through' : 'inherit',
}}
>
{option.text}
</Typography>
{currentRoom && option.value === currentRoom.email && <CheckCircleIcon color="success" sx={{ ml: 1 }} />}
</Box>

{/* Right section */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
}}
>
<Typography
variant="body2"
sx={[
(theme) => ({
color: theme.palette.grey[200],
}),
]}
>
{option.seats} {option.seats > 1 ? 'persons' : 'person'}
</Typography>
<Typography variant="body2">{option.floor}</Typography>
</Box>
</Box>
</MenuItem>
);
};

const StyledHintTypography = styled(Typography)(({ theme }) => ({
color: theme.palette.grey[500],
fontStyle: 'italic',
fontWeight: 400,
}));

const RenderNoRooms = ({ icon }: { icon?: ReactElement }) => {
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
{icon && icon}

<StyledHintTypography ml={2} variant="subtitle2">
No rooms available
</StyledHintTypography>
</Box>
);
};

const RenderPlaceholder = ({ icon, loading, placeholder }: { icon?: ReactElement; loading?: boolean; placeholder?: string }) => {
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
{icon && icon}
{loading ? (
<Skeleton
animation="wave"
sx={{
width: '100%',
mx: 2,
borderRadius: 0.5,
}}
/>
) : (
<StyledHintTypography ml={2} variant="subtitle2">
{placeholder}
</StyledHintTypography>
)}
</Box>
);
};

const RenderSelectText = ({ icon, loading, selectedOption }: { icon?: ReactElement; loading?: boolean; selectedOption?: RoomsDropdownOption }) => {
return (
<Box
sx={{
alignItems: 'center',
display: 'flex',
}}
>
{icon && icon}
{loading ? (
<Skeleton
animation="wave"
sx={{
width: '100%',
mx: 2,
borderRadius: 0.5,
}}
/>
) : (
<Typography
variant="subtitle1"
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
ml: 2,
}}
>
{selectedOption ? selectedOption.text : ''}
</Typography>
)}
</Box>
);
};

export default function RoomsDropdown({ sx, id, disabled, currentRoom, value, options, onChange, icon, placeholder, loading }: DropdownProps) {
const height = '58px';
const { preferences } = usePreferences();

const handleChange = (event: SelectChangeEvent) => {
onChange(id, event.target.value);
Expand All @@ -40,103 +188,16 @@ export default function RoomsDropdown({ sx, id, disabled, currentRoom, value, op
variant="standard"
disabled={disabled || false}
renderValue={(selected) => {
if (!loading && options?.length === 0) {
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
{icon && icon}

<Typography
variant="subtitle2"
sx={[
(theme) => ({
color: theme.palette.grey[500],
fontStyle: 'italic',
ml: 2,
fontWeight: 400,
}),
]}
>
No rooms available
</Typography>
</Box>
);
if (!loading && options.others.length === 0 && options.preferred.length === 0) {
return <RenderNoRooms icon={icon} />;
}
const selectedOption = options?.find((option) => option.value === selected);

const selectedOption = [...options.preferred, ...options.others].find((option) => option.value === selected);
if (placeholder && selected.length === 0) {
return (
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
{icon && icon}
{loading ? (
<Skeleton
animation="wave"
sx={{
width: '100%',
mx: 2,
borderRadius: 0.5,
}}
/>
) : (
<Typography
variant="subtitle2"
sx={[
(theme) => ({
color: theme.palette.grey[500],
fontStyle: 'italic',
ml: 2,
fontWeight: 400,
}),
]}
>
{placeholder}
</Typography>
)}
</Box>
);
return <RenderPlaceholder icon={icon} loading={loading} placeholder={placeholder} />;
}

return (
<Box
sx={{
alignItems: 'center',
display: 'flex',
}}
>
{icon && icon}
{loading ? (
<Skeleton
animation="wave"
sx={{
width: '100%',
mx: 2,
borderRadius: 0.5,
}}
/>
) : (
<Typography
variant="subtitle1"
sx={{
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
ml: 2,
}}
>
{selectedOption ? selectedOption.text : ''}
</Typography>
)}
</Box>
);
return <RenderSelectText icon={icon} loading={loading} selectedOption={selectedOption} />;
}}
disableUnderline={true}
sx={[
Expand All @@ -154,60 +215,27 @@ export default function RoomsDropdown({ sx, id, disabled, currentRoom, value, op
]}
>
{placeholder && (
<MenuItem disabled value="" sx={{ pt: 0 }}>
<MenuItem disabled value="" sx={{ pt: 1 }}>
<em>{placeholder}</em>
</MenuItem>
)}
{options?.map((option, i) => (
<MenuItem value={option.value} key={i}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}
>
{/* Left section */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
}}
>
<Typography
variant="subtitle1"
sx={{
textDecoration: option.isBusy ? 'line-through' : 'inherit',
}}
>
{option.text}
</Typography>
{currentRoom && option.value === currentRoom.email && <CheckCircleIcon color="success" sx={{ ml: 1 }} />}
</Box>

{/* Right section */}
<Box
sx={{
display: 'flex',
alignItems: 'center',
gap: 2,
}}
>
<Typography
variant="body2"
sx={[
(theme) => ({
color: theme.palette.grey[200],
}),
]}
>
{option.seats} {option.seats > 1 ? 'persons' : 'person'}
</Typography>
<Typography variant="body2">{option.floor}</Typography>
</Box>
</Box>
</MenuItem>

{options.preferred.length === 0 && preferences.floor && (
<StyledHintTypography sx={{ my: 0.5 }}>No rooms available according to your preferences</StyledHintTypography>
)}

{options.preferred.map((option, i) => (
<CustomMenuItem key={i} currentRoom={currentRoom} option={option} i={i} />
))}

{options.others.length > 0 && (
<ListSubheader>
<StyledHintTypography sx={{ my: 0.5 }}>Others</StyledHintTypography>
</ListSubheader>
)}

{options.others.map((option, i) => (
<CustomMenuItem key={i} currentRoom={currentRoom} option={option} i={i} />
))}
</Select>
);
Expand Down
Loading
Loading