From 820b94e0aa3553c7f7d59f848c9574767aac32fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eik=20Hvattum=20R=C3=B8geberg?= Date: Thu, 19 Sep 2024 21:22:54 +0200 Subject: [PATCH] Show waiting list placement for multiple available pools --- app/reducers/events.ts | 9 ++-- .../EventDetail/AttendeeSection.tsx | 14 ++--- .../EventDetail/getWaitingListPosition.ts | 45 ++++++++++++++++ .../events/components/EventDetail/usePools.ts | 2 + .../events/components/RegistrationMeta.tsx | 52 ++++++++++++++----- app/store/models/User.ts | 10 ++-- 6 files changed, 105 insertions(+), 27 deletions(-) create mode 100644 app/routes/events/components/EventDetail/getWaitingListPosition.ts diff --git a/app/reducers/events.ts b/app/reducers/events.ts index c5efb5ffca..e9dab47b8e 100644 --- a/app/reducers/events.ts +++ b/app/reducers/events.ts @@ -32,6 +32,7 @@ import type { AdministrateUser, AdministrateUserWithGrade, PublicUser, + PublicUserWithAbakusGroups, } from 'app/store/models/User'; import type { AnyAction } from 'redux'; import type { Optional, Overwrite } from 'utility-types'; @@ -174,12 +175,14 @@ export const selectEventByIdOrSlug = createSelector( export type PoolRegistrationWithUser = Overwrite< ReadRegistration, - { user: PublicUser } + { user: PublicUserWithAbakusGroups } >; export type PoolWithRegistrations = Overwrite< Optional, { registrations: PoolRegistrationWithUser[] } ->; +> & { + isWaitingList?: boolean; +}; export const selectPoolsForEvent = createSelector( selectEventById, selectPoolEntities, @@ -243,7 +246,7 @@ export const selectMergedPool = createSelector(selectPoolsForEvent, (pools) => { export const selectMergedPoolWithRegistrations = createSelector( selectPoolsForEvent, selectRegistrationEntities, - selectUserEntities, + selectUserEntities, (pools, registrationEntities, userEntities) => { if (pools.length === 0) return []; return [ diff --git a/app/routes/events/components/EventDetail/AttendeeSection.tsx b/app/routes/events/components/EventDetail/AttendeeSection.tsx index 2d09ef3b1b..e2179ea498 100644 --- a/app/routes/events/components/EventDetail/AttendeeSection.tsx +++ b/app/routes/events/components/EventDetail/AttendeeSection.tsx @@ -1,8 +1,10 @@ import { Flex } from '@webkom/lego-bricks'; import moment from 'moment-timezone'; +import { useMemo } from 'react'; import Attendance from 'app/components/UserAttendance/Attendance'; import { useIsLoggedIn } from 'app/reducers/auth'; import { selectRegistrationsFromPools } from 'app/reducers/events'; +import { getWaitingListPosition } from 'app/routes/events/components/EventDetail/getWaitingListPosition'; import RegistrationMeta, { RegistrationMetaSkeleton, } from 'app/routes/events/components/RegistrationMeta'; @@ -39,11 +41,10 @@ export const AttendeeSection = ({ ); const currentMoment = moment(); - const hasSimpleWaitingList = - pools.filter((p) => p.name != 'Venteliste').length <= 1; - const waitingListIndex = - currentRegistration && - currentPool?.registrations.indexOf(currentRegistration); + const waitingListPosition = useMemo( + () => getWaitingListPosition(currentRegistration, currentPool, pools), + [currentRegistration, currentPool, pools], + ); // The UserGrid is expanded when there's less than 5 minutes till activation const minUserGridRows = @@ -78,8 +79,7 @@ export const AttendeeSection = ({ hasEnded={moment(event.endTime).isBefore(currentMoment)} registration={currentRegistration} isPriced={event.isPriced} - registrationIndex={waitingListIndex ?? 0} - hasSimpleWaitingList={hasSimpleWaitingList} + waitingListPosition={waitingListPosition} skeleton={showSkeleton} /> ))} diff --git a/app/routes/events/components/EventDetail/getWaitingListPosition.ts b/app/routes/events/components/EventDetail/getWaitingListPosition.ts new file mode 100644 index 0000000000..37aa3dca3e --- /dev/null +++ b/app/routes/events/components/EventDetail/getWaitingListPosition.ts @@ -0,0 +1,45 @@ +import type { + PoolRegistrationWithUser, + PoolWithRegistrations, +} from 'app/reducers/events'; +import type { PublicUserWithAbakusGroups } from 'app/store/models/User'; + +const isPermittedInPool = ( + user: PublicUserWithAbakusGroups, + pool: PoolWithRegistrations, +) => { + return pool.permissionGroups.some((permissionGroup) => + user.allAbakusGroupIds.some( + (userGroup) => userGroup === permissionGroup.id, + ), + ); +}; + +export const getWaitingListPosition = ( + currentRegistration?: PoolRegistrationWithUser, + waitingList?: PoolWithRegistrations, + pools?: PoolWithRegistrations[], +) => { + if (!currentRegistration || !waitingList || !pools) return undefined; + + const nonWaitingListPools = pools.filter((pool) => !pool.isWaitingList); + + const applicablePools = nonWaitingListPools.filter((pool) => + isPermittedInPool(currentRegistration.user, pool), + ); + + if (applicablePools.length === 0) return undefined; + if (nonWaitingListPools.length === 1) { + return waitingList.registrations.indexOf(currentRegistration) + 1; + } + return applicablePools.map((pool) => { + const applicableWaitingListRegistrations = waitingList.registrations.filter( + (r) => isPermittedInPool(r.user, pool), + ); + return { + poolName: pool.name, + position: + applicableWaitingListRegistrations.indexOf(currentRegistration) + 1, + }; + }); +}; diff --git a/app/routes/events/components/EventDetail/usePools.ts b/app/routes/events/components/EventDetail/usePools.ts index aa55b51d3a..e5b57ad371 100644 --- a/app/routes/events/components/EventDetail/usePools.ts +++ b/app/routes/events/components/EventDetail/usePools.ts @@ -44,6 +44,7 @@ export function usePools( registrations: waitingRegistrations, registrationCount: waitingRegistrations.length, permissionGroups: [], + isWaitingList: true, }, ] : pools; @@ -55,6 +56,7 @@ export function usePools( name: 'Venteliste', registrationCount: event.waitingRegistrationCount, permissionGroups: [], + isWaitingList: true, }, ] : pools; diff --git a/app/routes/events/components/RegistrationMeta.tsx b/app/routes/events/components/RegistrationMeta.tsx index 8edc8c4598..0251882682 100644 --- a/app/routes/events/components/RegistrationMeta.tsx +++ b/app/routes/events/components/RegistrationMeta.tsx @@ -23,11 +23,17 @@ import type { import type { PoolRegistrationWithUser } from 'app/reducers/events'; import type { Presence } from 'app/store/models/Registration'; +type WaitingListPosition = + | number + | { + poolName: string; + position: number; + }[]; + type Props = { registration?: PoolRegistrationWithUser; isPriced: boolean; - registrationIndex: number; - hasSimpleWaitingList: boolean; + waitingListPosition?: WaitingListPosition; useConsent: boolean; hasOpened: boolean; hasEnded: boolean; @@ -246,8 +252,7 @@ const RegistrationMeta = ({ hasEnded, useConsent, isPriced, - registrationIndex, - hasSimpleWaitingList, + waitingListPosition, photoConsents, eventSemester, skeleton, @@ -282,21 +287,40 @@ const RegistrationMeta = ({ /> )} - ) : hasSimpleWaitingList ? ( + ) : ( - Din plass i ventelisten er{' '} - {registrationIndex + 1} - + waitingListPosition === undefined ? ( + <>Du {hasEnded ? 'stod' : 'står'} på venteliste + ) : typeof waitingListPosition === 'number' ? ( + <> + Din plass i ventelisten er{' '} + {waitingListPosition} + + ) : waitingListPosition.length === 1 ? ( + <> + Din plass i ventelisten for{' '} + {waitingListPosition[0].poolName} er{' '} + {waitingListPosition[0].position} + + ) : ( + <> + Dine plasser i ventelistene er{' '} + {waitingListPosition.map( + ({ poolName, position }, index) => ( + <> + {position} for {poolName} + {index < waitingListPosition.length - 2 + ? ', ' + : index < waitingListPosition.length - 1 && ' og '} + + ), + )} + + ) } /> - ) : ( - )} ; -export type PublicUserWithAbakusGroups = Pick & +export type PublicUserWithAbakusGroups = Pick< + User, + 'abakusGroups' | 'allAbakusGroupIds' +> & PublicUser; export type PublicUserWithGroups = Pick< User, - 'abakusGroups' | 'pastMemberships' | 'memberships' + 'pastMemberships' | 'memberships' > & - PublicUser; + PublicUserWithAbakusGroups; export type AdministrateUser = Pick & PublicUser;