diff --git a/frontend/src/routes/sponsor/frontpage/index.tsx b/frontend/src/routes/sponsor/frontpage/index.tsx index c509c658304ed5681817b236aa96d877456be24c..33833ef8a5914bbd79b9a4a41e7afe18de3396a7 100644 --- a/frontend/src/routes/sponsor/frontpage/index.tsx +++ b/frontend/src/routes/sponsor/frontpage/index.tsx @@ -13,6 +13,7 @@ import { TableContainer, TableHead, TableRow, + TableSortLabel, TextField, Typography, } from '@mui/material' @@ -54,6 +55,14 @@ interface FrontPageProps { loading: boolean } +type SortDirection = 'desc' | 'asc' +type SortField = 'name' | 'role' | 'status' | 'endDate' | 'department' + +interface GuestRole { + guest: Guest + role: Role +} + const StyledTableRow = styled(TableRow)({ borderTop: '0', borderLeft: '0', @@ -172,22 +181,65 @@ const Status = ({ person, role }: StatusProps) => { } } -const sortByDate = ( - guestRole: { guest: Guest; role: Role }[] -): { guest: Guest; role: Role }[] => - guestRole.sort( - (a, b) => a.role.end_date.getTime() - b.role.end_date.getTime() +function createGuestsRoles(guests: Guest[]): GuestRole[] { + const guestRoles: GuestRole[] = [] + guests.forEach((guest) => + guest.roles.forEach((role) => { + guestRoles.push({ guest, role }) + }) + ) + return guestRoles +} + +function sortByName( + guestA: Guest, + guestB: Guest, + direction: SortDirection +): number { + const firstA = guestA.first.toLowerCase() + const firstB = guestB.first.toLowerCase() + if (firstA < firstB) { + return direction === 'asc' ? -1 : 1 + } + if (firstA > firstB) { + return direction === 'asc' ? 1 : -1 + } + return 0 +} + +function sortGuestsByName(guests: Guest[], direction: SortDirection) { + return createGuestsRoles(guests).sort((a, b) => + sortByName(a.guest, b.guest, direction) ) +} + +function sortGuestRolesByEndDate( + guestRole: GuestRole[], + direction: SortDirection +): GuestRole[] { + return guestRole.sort((a, b) => { + const aEndDate = a.role.end_date.getTime() + const bEndDate = b.role.end_date.getTime() + + if (aEndDate < bEndDate) { + return direction === 'asc' ? -1 : 1 + } + if (aEndDate > bEndDate) { + return direction === 'asc' ? 1 : -1 + } + // Secondary sort on guest name, always asc + return sortByName(a.guest, b.guest, 'asc') + }) +} -const sortByStatus = (guests: Guest[]): { guest: Guest; role: Role }[] => { +function sortByStatus(guests: Guest[], direction: SortDirection): GuestRole[] { let activeRoleAndPerson: { guest: Guest; role: Role }[] = [] let expiringRoleAndPerson: { guest: Guest; role: Role }[] = [] let expiredRoleAndPerson: { guest: Guest; role: Role }[] = [] - let status guests.forEach((guest) => guest.roles.forEach((role) => { - ;[status] = calculateStatus(guest, role) + const [status] = calculateStatus(guest, role) if (status === 'active') { activeRoleAndPerson.push({ guest, role }) } else if (status === 'expiring') { @@ -198,13 +250,71 @@ const sortByStatus = (guests: Guest[]): { guest: Guest; role: Role }[] => { }) ) - activeRoleAndPerson = sortByDate(activeRoleAndPerson) - expiringRoleAndPerson = sortByDate(expiringRoleAndPerson) - expiredRoleAndPerson = sortByDate(expiredRoleAndPerson) + // Always use 'asc' for secondary sorting + activeRoleAndPerson = sortGuestRolesByEndDate(activeRoleAndPerson, 'asc') + expiringRoleAndPerson = sortGuestRolesByEndDate(expiringRoleAndPerson, 'asc') + expiredRoleAndPerson = sortGuestRolesByEndDate(expiredRoleAndPerson, 'asc') - return expiringRoleAndPerson + if (direction === 'asc') { + return expiringRoleAndPerson + .concat(activeRoleAndPerson) + .concat(expiredRoleAndPerson) + } + return expiredRoleAndPerson .concat(activeRoleAndPerson) - .concat(expiredRoleAndPerson) + .concat(expiringRoleAndPerson) +} + +function sortGuestsByRoleName(guests: Guest[], direction: SortDirection) { + return createGuestsRoles(guests).sort((a, b) => { + const aRoleName = getRoleName(a.role).toLowerCase() + const bRoleName = getRoleName(b.role).toLowerCase() + if (aRoleName < bRoleName) { + return direction === 'asc' ? -1 : 1 + } + if (aRoleName > bRoleName) { + return direction === 'asc' ? 1 : -1 + } + // Secondary sort on guest name, always asc + return sortByName(a.guest, b.guest, 'asc') + }) +} + +function sortGuestsByDepartmentName(guests: Guest[], direction: SortDirection) { + return createGuestsRoles(guests).sort((a, b) => { + const aRoleOUName = getRoleOuName(a.role).toLowerCase() + const bRoleOUName = getRoleOuName(b.role).toLowerCase() + if (aRoleOUName < bRoleOUName) { + return direction === 'asc' ? -1 : 1 + } + if (aRoleOUName > bRoleOUName) { + return direction === 'asc' ? 1 : -1 + } + // Secondary sort on guest name, always asc + return sortByName(a.guest, b.guest, 'asc') + }) +} + +function sortGuestsAndRoles( + guests: Guest[], + sortField: SortField, + direction: SortDirection +): { guest: Guest; role: Role }[] { + switch (sortField) { + case 'department': + return sortGuestsByDepartmentName(guests, direction) + case 'endDate': + return sortGuestRolesByEndDate(createGuestsRoles(guests), direction) + case 'name': + return sortGuestsByName(guests, direction) + case 'role': + return sortGuestsByRoleName(guests, direction) + case 'status': + return sortByStatus(guests, direction) + default: + // Fallback to original sort + return createGuestsRoles(guests) + } } const PersonLine = ({ person, role }: PersonLineProps) => { @@ -236,6 +346,17 @@ const PersonLine = ({ person, role }: PersonLineProps) => { const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => { const { t } = useTranslation('common') + const [direction, setDirection] = useState<SortDirection>('asc') + const [orderBy, setOrderBy] = useState<SortField>('status') + + const handleRequestSort = + (sortBy: SortField) => (event: React.MouseEvent<unknown>) => { + event.preventDefault() + const isAsc = orderBy === sortBy && direction === 'asc' + setDirection(isAsc ? 'desc' : 'asc') + setOrderBy(sortBy) + } + return ( <TableContainer component={Paper} @@ -248,17 +369,63 @@ const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => { <Table sx={{ minWidth: marginWidth }} aria-label="simple table"> <StyledTableHead> <TableRow> - <StyledTableHeadCell>{t('common:name')}</StyledTableHeadCell> - <StyledTableHeadCell>{t('common:role')}</StyledTableHeadCell> - <StyledTableHeadCell>{t('common:status')}</StyledTableHeadCell> - <StyledTableHeadCell>{t('common:endDate')}</StyledTableHeadCell> - <StyledTableHeadCell>{t('common:department')}</StyledTableHeadCell> + <StyledTableHeadCell> + <TableSortLabel + key="name" + active={orderBy === 'name'} + direction={orderBy === 'name' ? direction : 'asc'} + onClick={handleRequestSort('name')} + > + {t('common:name')} + </TableSortLabel> + </StyledTableHeadCell> + <StyledTableHeadCell> + <TableSortLabel + key="role" + active={orderBy === 'role'} + direction={orderBy === 'role' ? direction : 'asc'} + onClick={handleRequestSort('role')} + > + {t('common:role')} + </TableSortLabel> + </StyledTableHeadCell> + + <StyledTableHeadCell> + <TableSortLabel + key="status" + active={orderBy === 'status'} + direction={orderBy === 'status' ? direction : 'asc'} + onClick={handleRequestSort('status')} + > + {t('common:status')} + </TableSortLabel> + </StyledTableHeadCell> + <StyledTableHeadCell> + <TableSortLabel + key="endDate" + active={orderBy === 'endDate'} + direction={orderBy === 'endDate' ? direction : 'asc'} + onClick={handleRequestSort('endDate')} + > + {t('common:endDate')} + </TableSortLabel> + </StyledTableHeadCell> + <StyledTableHeadCell> + <TableSortLabel + key="department" + active={orderBy === 'department'} + direction={orderBy === 'department' ? direction : 'asc'} + onClick={handleRequestSort('department')} + > + {t('common:department')} + </TableSortLabel> + </StyledTableHeadCell> <StyledTableHeadCell /> </TableRow> </StyledTableHead> <TableBody> {guests.length > 0 ? ( - sortByStatus(guests).map((personRole) => ( + sortGuestsAndRoles(guests, orderBy, direction).map((personRole) => ( <PersonLine key={`${personRole.guest.first} ${personRole.guest.last} ${personRole.role.id}`} role={personRole.role} @@ -267,7 +434,7 @@ const GuestTable = ({ guests, emptyText, marginWidth }: GuestTableProps) => { )) ) : ( <StyledTableRow> - <TableCell> {emptyText}</TableCell> + <TableCell>{emptyText}</TableCell> </StyledTableRow> )} </TableBody>