From 034c73d57b1f004987a6d71ff9108af609d97d90 Mon Sep 17 00:00:00 2001 From: Sivert Kronen Hatteberg <sivert.hatteberg@usit.uio.no> Date: Wed, 8 Feb 2023 10:46:47 +0100 Subject: [PATCH] Add filter on unit for the guests table --- frontend/public/locales/en/common.json | 1 + frontend/public/locales/nb/common.json | 1 + frontend/public/locales/nn/common.json | 1 + .../src/routes/sponsor/frontpage/index.tsx | 307 ++++++++++++++---- 4 files changed, 255 insertions(+), 55 deletions(-) diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 26411b65..65bb63ea 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -107,6 +107,7 @@ "foundNoGuests": "Found no guests", "sentInvitations": "Sent invitations", "placeholder": "Search for guest", + "chooseUnits": "Choose unit(s)", "sentInvitationsDescription": "Invitations awaiting response from guest.", "noInvitations": "No invitations", "status": "Status", diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index 6780ea9e..908dd470 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -107,6 +107,7 @@ "foundNoGuests": "Fant ingen gjester", "sentInvitations": "Sendte invitasjoner", "placeholder": "Søk etter gjest", + "chooseUnits": "Velg avdeling(er)", "sentInvitationsDescription": "Invitasjoner som venter på at gjesten skal ferdigstille registreringen.", "noInvitations": "Ingen invitasjoner", "status": "Status", diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index c8751c92..2119651f 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -107,6 +107,7 @@ "foundNoGuests": "Fann ingen gjester", "sentInvitations": "Sendte invitasjonar", "placeholder": "Søk etter gjest", + "chooseUnits": "Vel avdeling(ar)", "sentInvitationsDescription": "Invitasjonar som venter på at gjesten skal ferdigstille registreringa.", "noInvitations": "Ingen invitasjonar", "status": "Status", diff --git a/frontend/src/routes/sponsor/frontpage/index.tsx b/frontend/src/routes/sponsor/frontpage/index.tsx index 33833ef8..fe091143 100644 --- a/frontend/src/routes/sponsor/frontpage/index.tsx +++ b/frontend/src/routes/sponsor/frontpage/index.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from 'react' + import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward' import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive' import { @@ -5,8 +7,14 @@ import { AccordionDetails, AccordionSummary, Button, + Chip, + FormControl, InputAdornment, + InputLabel, + MenuItem, Paper, + Select, + SelectChangeEvent, Table, TableBody, TableCell, @@ -18,7 +26,6 @@ import { Typography, } from '@mui/material' import { Box, styled } from '@mui/system' -import { useState } from 'react' import SearchIcon from '@mui/icons-material/Search' import Loading from 'components/loading' @@ -48,6 +55,7 @@ interface GuestTableProps { guests: Guest[] emptyText: string marginWidth: number + unitFilters: string[] } interface FrontPageProps { @@ -98,7 +106,7 @@ const StyledTableHead = styled(TableHead)(({ theme }) => ({ borderRadius: '0', })) -const calculateStatus = (person: Guest, role: Role): [string, number] => { +function calculateStatus(person: Guest, role: Role): [string, number] { const today = new Date() today.setHours(0, 0, 0, 0) let status = '' @@ -125,7 +133,7 @@ const calculateStatus = (person: Guest, role: Role): [string, number] => { return [status, days] } -const Status = ({ person, role }: StatusProps) => { +function Status({ person, role }: StatusProps) { const { t } = useTranslation('common') const [status, days] = calculateStatus(person, role) @@ -265,7 +273,10 @@ function sortByStatus(guests: Guest[], direction: SortDirection): GuestRole[] { .concat(expiringRoleAndPerson) } -function sortGuestsByRoleName(guests: Guest[], direction: SortDirection) { +function sortGuestsByRoleName( + guests: Guest[], + direction: SortDirection +): GuestRole[] { return createGuestsRoles(guests).sort((a, b) => { const aRoleName = getRoleName(a.role).toLowerCase() const bRoleName = getRoleName(b.role).toLowerCase() @@ -280,7 +291,10 @@ function sortGuestsByRoleName(guests: Guest[], direction: SortDirection) { }) } -function sortGuestsByDepartmentName(guests: Guest[], direction: SortDirection) { +function sortGuestsByDepartmentName( + guests: Guest[], + direction: SortDirection +): GuestRole[] { return createGuestsRoles(guests).sort((a, b) => { const aRoleOUName = getRoleOuName(a.role).toLowerCase() const bRoleOUName = getRoleOuName(b.role).toLowerCase() @@ -298,26 +312,43 @@ function sortGuestsByDepartmentName(guests: Guest[], direction: SortDirection) { function sortGuestsAndRoles( guests: Guest[], sortField: SortField, - direction: SortDirection -): { guest: Guest; role: Role }[] { + direction: SortDirection, + unitFilters?: string[] +): GuestRole[] { + let guestRoles: GuestRole[] + switch (sortField) { case 'department': - return sortGuestsByDepartmentName(guests, direction) + guestRoles = sortGuestsByDepartmentName(guests, direction) + break case 'endDate': - return sortGuestRolesByEndDate(createGuestsRoles(guests), direction) + guestRoles = sortGuestRolesByEndDate(createGuestsRoles(guests), direction) + break case 'name': - return sortGuestsByName(guests, direction) + guestRoles = sortGuestsByName(guests, direction) + break case 'role': - return sortGuestsByRoleName(guests, direction) + guestRoles = sortGuestsByRoleName(guests, direction) + break case 'status': - return sortByStatus(guests, direction) + guestRoles = sortByStatus(guests, direction) + break default: // Fallback to original sort - return createGuestsRoles(guests) + guestRoles = createGuestsRoles(guests) + break + } + + if (unitFilters && unitFilters.length > 0) { + return guestRoles.filter(({ role }) => + unitFilters.includes(role.ou_id.toString()) + ) } + + return guestRoles } -const PersonLine = ({ person, role }: PersonLineProps) => { +function PersonLine({ person, role }: PersonLineProps) { const [t] = useTranslation(['common']) return ( @@ -343,7 +374,52 @@ const PersonLine = ({ person, role }: PersonLineProps) => { ) } -const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => { +function PersonTableBody({ + guests, + orderBy, + direction, + unitFilters, +}: { + guests: Guest[] + orderBy: SortField + direction: SortDirection + unitFilters: string[] | undefined +}) { + const { t } = useTranslation('common') + + const filteredSortedGuests = sortGuestsAndRoles( + guests, + orderBy, + direction, + unitFilters + ) + if (filteredSortedGuests.length === 0) { + return ( + <StyledTableRow> + <TableCell>{t('foundNoGuests')}</TableCell> + </StyledTableRow> + ) + } + + return ( + <> + {filteredSortedGuests.map((personRole) => ( + <PersonLine + key={`${personRole.guest.first} ${personRole.guest.last} ${personRole.role.id}`} + role={personRole.role} + person={personRole.guest} + /> + ))} + </> + ) +} + +function GuestTable({ + guests, + emptyText, + marginWidth, + unitFilters, +}: GuestTableProps) { const { t } = useTranslation('common') const [direction, setDirection] = useState<SortDirection>('asc') @@ -425,13 +501,12 @@ const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => { </StyledTableHead> <TableBody> {guests.length > 0 ? ( - sortGuestsAndRoles(guests, orderBy, direction).map((personRole) => ( - <PersonLine - key={`${personRole.guest.first} ${personRole.guest.last} ${personRole.role.id}`} - role={personRole.role} - person={personRole.guest} - /> - )) + <PersonTableBody + guests={guests} + orderBy={orderBy} + direction={direction} + unitFilters={unitFilters} + /> ) : ( <StyledTableRow> <TableCell>{emptyText}</TableCell> @@ -443,7 +518,7 @@ const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => { ) } -const InvitedGuests = ({ persons }: GuestProps) => { +function InvitedGuests({ persons }: GuestProps) { const [activeExpanded, setActiveExpanded] = useState(false) // Show guests that have not responded to the invite yet @@ -477,6 +552,7 @@ const InvitedGuests = ({ persons }: GuestProps) => { <GuestTable guests={guests} emptyText={t('common:noInvitations')} + unitFilters={[]} marginWidth={650} /> </AccordionDetails> @@ -484,32 +560,138 @@ const InvitedGuests = ({ persons }: GuestProps) => { ) } -const ActiveGuests = ({ persons }: GuestProps) => { - const [activeExpanded, setActiveExpanded] = useState(false) - const [searchHasInput, setSearchHasInput] = useState(false) +const ITEM_HEIGHT = 48 +const ITEM_PADDING_TOP = 8 +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 400, + }, + }, +} + +interface UnitFilterSelectProps { + guests: Guest[] + onChange: (units: string[]) => void +} + +function UnitFilterSelect({ guests, onChange }: UnitFilterSelectProps) { + // + const [selectedUnits, setSelectedUnits] = useState<string[]>([]) + const { t } = useTranslation(['common']) + + useEffect(() => { + onChange(selectedUnits) + }, [selectedUnits, onChange]) + + const handleChange = (event: SelectChangeEvent<typeof selectedUnits>) => { + const { + target: { value }, + } = event + setSelectedUnits( + // On autofill we get a stringified value. + typeof value === 'string' ? value.split(',') : value + ) + } + + const units: { [key: string]: string } = {} + guests.forEach((guest) => + guest.roles.forEach((role) => { + if (!(role.ou_id in units)) { + units[role.ou_id.toString()] = getRoleOuName(role) + } + }) + ) + + if (units) { + return ( + <div> + <FormControl sx={{ m: 1, width: 450 }}> + <InputLabel id="ou-select-label">{t('chooseUnits')}</InputLabel> + <Select + labelId="ou-select-label" + id="ou-select" + value={selectedUnits} + onChange={handleChange} + multiple + label={t('chooseUnits')} + renderValue={(selected) => ( + <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}> + {Object.entries(selected).map(([, id]) => ( + <Chip + key={id} + label={units[id]} + onDelete={() => + setSelectedUnits(selectedUnits.filter((u) => id !== u)) + } + onMouseDown={(event) => { + event.stopPropagation() + }} + /> + ))} + </Box> + )} + MenuProps={MenuProps} + > + {Object.entries(units).map(([id, name]) => ( + <MenuItem key={id} value={id}> + {name} + </MenuItem> + ))} + </Select> + </FormControl> + </div> + ) + } + return null +} + +function ActiveGuests({ persons }: GuestProps) { + const [activeExpanded, setActiveExpanded] = useState<boolean>(false) const [searchGuests, setSearchGuests] = useState<Guest[]>([]) + const [selectedUnits, setSelectedUnits] = useState<string[]>([]) + const [searchInput, setSearchInput] = useState<string>('') + const [searching, setSearching] = useState<boolean>(false) + + const [t] = useTranslation(['common']) // Show all verified guests let guests = persons.length > 0 ? persons : [] if (guests.length > 0) { guests = guests.filter((person) => person.verified) } - const [t] = useTranslation(['common']) - const getSponsorGuests = (event: React.ChangeEvent<HTMLInputElement>) => { - if (event.target.value) { - setSearchHasInput(true) - const guestSearch: Guest[] = guests.filter((guest) => - `${guest.first.toLowerCase()} ${guest.last.toLowerCase()}`.includes( - event.target.value.toLowerCase() + // Wait 1s after last change to start the search. + useEffect(() => { + if (searchInput === '') { + if (searchGuests) { + setSearchGuests([]) + } + return () => {} + } + setSearching(true) + const delaySearch = setTimeout(() => { + setSearchGuests( + guests.filter((guest) => + `${guest.first.toLowerCase()} ${guest.last.toLowerCase()}`.includes( + searchInput + ) ) ) - setSearchGuests(guestSearch) + setSearching(false) + }, 1000) + return () => clearTimeout(delaySearch) + }, [searchInput]) + + const getSponsorGuests = (event: React.ChangeEvent<HTMLInputElement>) => { + if (event.target.value) { + setSearchInput(event.target.value.toLowerCase()) } else { - setSearchHasInput(false) - setSearchGuests([]) + setSearchInput('') } } + return ( <StyledAccordion expanded={activeExpanded} @@ -536,30 +718,44 @@ const ActiveGuests = ({ persons }: GuestProps) => { marginBottom: '1rem', }} > - <TextField - variant="standard" - InputProps={{ - endAdornment: ( - <InputAdornment position="end"> - <SearchIcon /> - </InputAdornment> - ), + <Box + sx={{ + display: 'flex', + justifyContent: 'space-between', + alignItems: 'flex-start', }} - placeholder={t('placeholder')} - onChange={getSponsorGuests} - /> - {!searchHasInput ? ( - <GuestTable + > + <FormControl sx={{ m: 1, width: 300 }}> + <TextField + InputProps={{ + endAdornment: ( + <InputAdornment position="end"> + <SearchIcon /> + </InputAdornment> + ), + }} + placeholder={t('placeholder')} + onChange={getSponsorGuests} + /> + </FormControl> + <UnitFilterSelect guests={guests} - emptyText={t('common:noActiveGuests')} - marginWidth={1000} + onChange={(units: string[]) => setSelectedUnits(units)} /> - ) : ( + </Box> + {!searching ? ( <GuestTable - guests={searchGuests} - emptyText={t('common:foundNoGuests')} + guests={searchInput ? searchGuests : guests} + emptyText={ + searchInput + ? t('common:foundNoGuests') + : t('common:noActiveGuests') + } marginWidth={1000} + unitFilters={selectedUnits} /> + ) : ( + <Loading /> )} </Box> </AccordionDetails> @@ -567,7 +763,7 @@ const ActiveGuests = ({ persons }: GuestProps) => { ) } -const WaitingGuests = ({ persons }: GuestProps) => { +function WaitingGuests({ persons }: GuestProps) { const [waitingExpanded, setWaitingExpanded] = useState(false) // Show guests that have completed the registration but are not verified yet @@ -610,6 +806,7 @@ const WaitingGuests = ({ persons }: GuestProps) => { guests={guests} emptyText={t('common:noWaitingGuests')} marginWidth={650} + unitFilters={[]} /> </AccordionDetails> </StyledAccordion> -- GitLab