diff --git a/backend/services/event.py b/backend/services/event.py index f0bfa4200..8272e8137 100644 --- a/backend/services/event.py +++ b/backend/services/event.py @@ -77,9 +77,8 @@ def get_paginated_events( range_start = pagination_params.range_start range_end = pagination_params.range_end criteria = and_( - EventEntity.time - >= datetime.strptime(range_start, "%d/%m/%Y, %H:%M:%S"), - EventEntity.time <= datetime.strptime(range_end, "%d/%m/%Y, %H:%M:%S"), + EventEntity.time >= datetime.fromisoformat(range_start), + EventEntity.time <= datetime.fromisoformat(range_end), ) statement = statement.where(criteria) length_statement = length_statement.where(criteria) @@ -106,11 +105,13 @@ def get_paginated_events( limit = pagination_params.page_size if pagination_params.order_by != "": - statement = statement.order_by( - getattr(EventEntity, pagination_params.order_by) - ) if pagination_params.ascending else statement.order_by( + statement = ( + statement.order_by(getattr(EventEntity, pagination_params.order_by)) + if pagination_params.ascending + else statement.order_by( getattr(EventEntity, pagination_params.order_by).desc() ) + ) statement = statement.offset(offset).limit(limit) diff --git a/frontend/src/app/admin/users/list/admin-users-list.component.ts b/frontend/src/app/admin/users/list/admin-users-list.component.ts index 03174cc82..e5eab52ae 100644 --- a/frontend/src/app/admin/users/list/admin-users-list.component.ts +++ b/frontend/src/app/admin/users/list/admin-users-list.component.ts @@ -36,7 +36,9 @@ export class AdminUsersListComponent { canActivate: [permissionGuard('user.list', 'user/')], resolve: { page: () => - inject(UserAdminService).list(AdminUsersListComponent.PaginationParams) + inject(UserAdminService).list( + AdminUsersListComponent.PaginationParams as PaginationParams + ) } }; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index a1cc95580..01d2bf24d 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -40,7 +40,6 @@ import { ErrorDialogComponent } from './navigation/error-dialog/error-dialog.com import { HomeComponent } from './home/home.component'; import { AboutComponent } from './about/about.component'; import { GateComponent } from './gate/gate.component'; -import { ProfileEditorComponent } from './profile/profile-editor/profile-editor.component'; import { SharedModule } from './shared/shared.module'; @NgModule({ diff --git a/frontend/src/app/event/event-details/event-details.component.html b/frontend/src/app/event/event-details/event-details.component.html index 5d7cf7c4f..56094aad2 100644 --- a/frontend/src/app/event/event-details/event-details.component.html +++ b/frontend/src/app/event/event-details/event-details.component.html @@ -1,9 +1,9 @@
- + - + @if ((this.canViewEvent() | async) || this.event?.is_organizer ?? false) { + + }
diff --git a/frontend/src/app/event/event-details/event-details.component.ts b/frontend/src/app/event/event-details/event-details.component.ts index 42c1593c4..899b0e442 100644 --- a/frontend/src/app/event/event-details/event-details.component.ts +++ b/frontend/src/app/event/event-details/event-details.component.ts @@ -3,14 +3,13 @@ * any given event. * * @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney - * @copyright 2023 + * @copyright 2024 * @license MIT */ -import { Component, inject } from '@angular/core'; -import { profileResolver } from 'src/app/profile/profile.resolver'; -import { eventDetailResolver } from '../event.resolver'; -import { Profile } from 'src/app/profile/profile.service'; +import { Component, OnInit } from '@angular/core'; +import { eventResolver } from '../event.resolver'; +import { Profile, ProfileService } from 'src/app/profile/profile.service'; import { ActivatedRoute, ActivatedRouteSnapshot, @@ -31,46 +30,51 @@ let titleResolver: ResolveFn = (route: ActivatedRouteSnapshot) => { templateUrl: './event-details.component.html', styleUrls: ['./event-details.component.css'] }) -export class EventDetailsComponent { +export class EventDetailsComponent implements OnInit { /** Route information to be used in Event Routing Module */ public static Route = { path: ':id', title: 'Event Details', component: EventDetailsComponent, resolve: { - profile: profileResolver, - event: eventDetailResolver + event: eventResolver }, children: [ { path: '', title: titleResolver, component: EventDetailsComponent } ] }; - /** Store Event */ - public event!: Event; - /** Store the currently-logged-in user's profile. */ public profile: Profile; - public adminPermission$: Observable; + /** The event to show */ + public event: Event | undefined; + + /** + * Determines whether or not a user can view the event. + * @returns {Observable} + */ + canViewEvent(): Observable { + return this.permissionService.check( + 'organization.events.view', + `organization/${this.event?.organization!?.id ?? '*'}` + ); + } + + /** Constructs the Event Detail component. */ constructor( private route: ActivatedRoute, - private permission: PermissionService, + private permissionService: PermissionService, + private profileService: ProfileService, private gearService: NagivationAdminGearService ) { - /** Initialize data from resolvers. */ + this.profile = this.profileService.profile()!; + const data = this.route.snapshot.data as { - profile: Profile; event: Event; }; - this.profile = data.profile; - this.event = data.event; - // Admin Permission if has the actual permission or is event organizer - this.adminPermission$ = this.permission.check( - 'organization.events.view', - `organization/${this.event.organization!.id}` - ); + this.event = data.event; } ngOnInit() { @@ -78,7 +82,7 @@ export class EventDetailsComponent { 'events.*', '*', '', - `events/organizations/${this.event.organization?.slug}/events/${this.event.id}/edit` + `events/${this.event?.organization_id}/${this.event?.id}/edit` ); } } diff --git a/frontend/src/app/event/event-editor/event-editor.component.html b/frontend/src/app/event/event-editor/event-editor.component.html index 583cb128f..798198530 100644 --- a/frontend/src/app/event/event-editor/event-editor.component.html +++ b/frontend/src/app/event/event-editor/event-editor.component.html @@ -1,13 +1,11 @@ -
+ - Create Event - Update Event + + {{ this.isNew() ? 'Create' : 'Update' }} Event + @@ -66,10 +64,7 @@ - + @@ -80,8 +75,3 @@
- - - - You do not have permission to view this page - diff --git a/frontend/src/app/event/event-editor/event-editor.component.ts b/frontend/src/app/event/event-editor/event-editor.component.ts index 3bdab28c9..b1d7128c9 100644 --- a/frontend/src/app/event/event-editor/event-editor.component.ts +++ b/frontend/src/app/event/event-editor/event-editor.component.ts @@ -3,7 +3,7 @@ * about events which are publically displayed on the Events page. * * @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney - * @copyright 2023 + * @copyright 2024 * @license MIT */ @@ -12,16 +12,16 @@ import { ActivatedRoute, Route, Router } from '@angular/router'; import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { MatSnackBar } from '@angular/material/snack-bar'; import { EventService } from '../event.service'; -import { profileResolver } from '../../profile/profile.resolver'; -import { Profile, PublicProfile } from '../../profile/profile.service'; -import { Observable, map } from 'rxjs'; -import { eventDetailResolver } from '../event.resolver'; -import { PermissionService } from 'src/app/permission.service'; -import { organizationResolver } from 'src/app/organization/organization.resolver'; -import { Organization } from 'src/app/organization/organization.model'; -import { Event, RegistrationType } from '../event.model'; +import { + Profile, + ProfileService, + PublicProfile +} from '../../profile/profile.service'; +import { eventResolver } from '../event.resolver'; +import { Event } from '../event.model'; import { DatePipe } from '@angular/common'; import { OrganizationService } from 'src/app/organization/organization.service'; +import { eventEditorGuard } from './event-editor.guard'; @Component({ selector: 'app-event-editor', @@ -29,52 +29,43 @@ import { OrganizationService } from 'src/app/organization/organization.service'; styleUrls: ['./event-editor.component.css'] }) export class EventEditorComponent { + /** Route information to be used in Event Routing Module */ public static Route: Route = { - path: 'organizations/:slug/events/:id/edit', + path: ':orgid/:id/edit', component: EventEditorComponent, title: 'Event Editor', + canActivate: [eventEditorGuard], resolve: { - profile: profileResolver, - organization: organizationResolver, - event: eventDetailResolver + event: eventResolver } }; - /** Store the event to be edited or created */ - public event: Event; - public organization_slug: string; - public organization: Organization; - - public profile: Profile | null = null; + /** Store the currently-logged-in user's profile. */ + public profile: Profile; - /** Stores whether the user has admin permission over the current organization. */ - public enabled$: Observable; + /** Stores the event. */ + public event: Event; /** Store organizers */ - public organizers: PublicProfile[] = []; - - /** Add validators to the form */ - name = new FormControl('', [Validators.required]); - time = new FormControl('', [Validators.required]); - location = new FormControl('', [Validators.required]); - description = new FormControl('', [ - Validators.required, - Validators.maxLength(2000) - ]); - public = new FormControl('', [Validators.required]); - registration_limit = new FormControl(0, [ - Validators.required, - Validators.min(0) - ]); - - /** Create a form group */ + public organizers: PublicProfile[]; + + /** Event Editor Form */ public eventForm = this.formBuilder.group({ - name: this.name, - time: this.datePipe.transform(new Date(), 'yyyy-MM-ddTHH:mm'), - location: this.location, - description: this.description, - public: this.public.value! == 'true', - registration_limit: this.registration_limit, + name: new FormControl('', [Validators.required]), + time: new FormControl( + this.datePipe.transform(new Date(), 'yyyy-MM-ddTHH:mm'), + [Validators.required] + ), + location: new FormControl('', [Validators.required]), + description: new FormControl('', [ + Validators.required, + Validators.maxLength(2000) + ]), + public: new FormControl(false, [Validators.required]), + registration_limit: new FormControl(0, [ + Validators.required, + Validators.min(0) + ]), userLookup: '' }); @@ -85,89 +76,54 @@ export class EventEditorComponent { protected organizationService: OrganizationService, protected snackBar: MatSnackBar, private eventService: EventService, - private permission: PermissionService, + private profileService: ProfileService, private datePipe: DatePipe ) { - // Get currently-logged-in user - const data = route.snapshot.data as { - profile: Profile; - organization: Organization; + this.profile = this.profileService.profile()!; + + const data = this.route.snapshot.data as { event: Event; }; - this.profile = data.profile; - - // Initialize event - this.organization = data.organization; this.event = data.event; - this.event.organization_id = this.organization.id; - - // Get ids from the url - let organization_slug = this.route.snapshot.params['slug']; - this.organization_slug = organization_slug; // Set values for form group - this.eventForm.setValue({ - name: this.event.name, - time: this.datePipe.transform(this.event.time, 'yyyy-MM-ddTHH:mm'), - location: this.event.location, - description: this.event.description, - public: this.event.public, - registration_limit: this.event.registration_limit, - userLookup: '' - }); + this.eventForm.patchValue( + Object.assign({}, this.event, { + time: this.datePipe.transform(this.event.time, 'yyyy-MM-ddTHH:mm'), + userLookup: '' + }) + ); // Add validator for registration_limit - this.registration_limit.addValidators( + this.eventForm.controls['registration_limit'].addValidators( Validators.min(this.event.registration_count) ); - this.enabled$ = this.permission - .check( - 'organization.events.update', - `organization/${this.organization!.id}` - ) - .pipe(map((permission) => permission || this.event.is_organizer)); - // Set the organizers // If no organizers already, set current user as organizer - if (this.event.id == null) { - let organizer: PublicProfile = { - id: this.profile.id!, - first_name: this.profile.first_name!, - last_name: this.profile.last_name!, - pronouns: this.profile.pronouns!, - email: this.profile.email!, - github_avatar: this.profile.github_avatar - }; - this.organizers.push(organizer); - } else { - // Set organizers to current organizers - this.organizers = this.event.organizers; - } + this.organizers = this.isNew() + ? [this.profile as PublicProfile] + : this.event.organizers; } - /** Event handler to handle submitting the Create Event Form. + /** Event handler to handle submitting the event form. * @returns {void} */ onSubmit() { if (this.eventForm.valid) { Object.assign(this.event, this.eventForm.value); - - // Set fields not explicitly in form this.event.organizers = this.organizers; - if (this.event.id == null) { - this.eventService.createEvent(this.event).subscribe({ - next: (event) => this.onSuccess(event), - error: (err) => this.onError(err) - }); - } else { - this.eventService.updateEvent(this.event).subscribe({ - next: (event) => this.onSuccess(event), - error: (err) => this.onError(err) - }); - } - this.router.navigate(['/organizations/', this.organization_slug]); + let submittedEvent = this.isNew() + ? this.eventService.createEvent(this.event) + : this.eventService.updateEvent(this.event); + + submittedEvent.subscribe({ + next: (event) => this.onSuccess(event), + error: (err) => this.onError(err) + }); + + this.router.navigate(['/organizations/', this.event.organization?.slug]); } } @@ -183,17 +139,29 @@ export class EventEditorComponent { */ private onSuccess(event: Event): void { this.router.navigate(['/events/', event.id]); - if (this.event.id == null) { - this.snackBar.open('Event Created', '', { duration: 2000 }); - } else { - this.snackBar.open('Event Edited', '', { duration: 2000 }); - } + this.snackBar.open(`Event ${this.action()}`, '', { duration: 2000 }); } /** Opens a confirmation snackbar when there is an error creating an event. * @returns {void} */ private onError(err: any): void { - this.snackBar.open('Error: Event Not Created', '', { duration: 2000 }); + this.snackBar.open(`Error: Event Not ${this.action()}`, '', { + duration: 2000 + }); + } + + /** Shorthand for whether an event is new or not. + * @returns {boolean} + */ + isNew(): boolean { + return this.event.id == null; + } + + /** Shorthand for determining the action being performed on the event. + * @returns {string} + */ + action(): string { + return this.isNew() ? 'Created' : 'Updated'; } } diff --git a/frontend/src/app/event/event-editor/event-editor.guard.ts b/frontend/src/app/event/event-editor/event-editor.guard.ts new file mode 100644 index 000000000..08ed56a6c --- /dev/null +++ b/frontend/src/app/event/event-editor/event-editor.guard.ts @@ -0,0 +1,51 @@ +/** + * The Event Editor Guard ensures that the page can open if the user has + * the correct permissions. + * + * @author Ajay Gandecha + * @copyright 2024 + * @license MIT + */ + +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; +import { PermissionService } from 'src/app/permission.service'; +import { Event } from '../event.model'; +import { combineLatest, map } from 'rxjs'; +import { EventService } from '../event.service'; + +// TODO: Refactor with a new event permission API so that we do not +// duplicate calls to the event API here. + +/** Determines whether the user can access the event editor. + * @param route Active route when the user enters the component. + * @returns {CanActivateFn} + */ +export const eventEditorGuard: CanActivateFn = (route, _) => { + /** Determine if page is viewable by user based on permissions */ + + // Load IDs from the route + let organizationId: string = route.params['orgid']; + let eventId: string = route.params['id']; + + // Create two observables for each check + + // Checks if the user has permissions to update events for + // the organization hosting this event + const permissionCheck$ = inject(PermissionService).check( + 'organization.events.update', + `organization/${organizationId}` + ); + + // Checks if the user is the organizer for the event + const isOrganizerCheck$ = inject(EventService) + .getEvent(+eventId) + .pipe(map((event) => event?.is_organizer ?? false)); + + // Since only one check has to be true for the user to see the page, + // we combine the results of these observables into a single + // observable that returns true if either were true. + return combineLatest([permissionCheck$, isOrganizerCheck$]).pipe( + map(([hasPermission, isOrganizer]) => hasPermission || isOrganizer) + ); +}; diff --git a/frontend/src/app/event/event-list-admin/event-list-admin.component.css b/frontend/src/app/event/event-list-admin/event-list-admin.component.css deleted file mode 100644 index 2284d548a..000000000 --- a/frontend/src/app/event/event-list-admin/event-list-admin.component.css +++ /dev/null @@ -1,35 +0,0 @@ -/** -* admin-organization-list.component.css -* -* The admin organization list page should provide -* a simple, easily readable form for users to view -* all organizations. -* -*/ - -.mat-mdc-row .mat-mdc-cell { - border-bottom: 1px solid transparent; - border-top: 1px solid transparent; - cursor: pointer; -} - -.mat-mdc-row:hover .mat-mdc-cell { - border-color: white; -} - -.header { - display: flex; - align-items: center; - justify-content: space-between; -} - -.row { - display: flex; - align-items: center; - justify-content: space-between; -} - -.button-container button { - justify-content: space-between; - margin: 5px; -} \ No newline at end of file diff --git a/frontend/src/app/event/event-list-admin/event-list-admin.component.html b/frontend/src/app/event/event-list-admin/event-list-admin.component.html deleted file mode 100644 index cc0229657..000000000 --- a/frontend/src/app/event/event-list-admin/event-list-admin.component.html +++ /dev/null @@ -1,25 +0,0 @@ - -
- - - - - - - -
-
Events
-
-
-

{{ element.name }}

-
- -
-
-
-
diff --git a/frontend/src/app/event/event-list-admin/event-list-admin.component.ts b/frontend/src/app/event/event-list-admin/event-list-admin.component.ts deleted file mode 100644 index a61a2a99a..000000000 --- a/frontend/src/app/event/event-list-admin/event-list-admin.component.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { MatSnackBar } from '@angular/material/snack-bar'; -import { Observable, map, of } from 'rxjs'; -import { - Permission, - Profile -} from '/workspace/frontend/src/app/profile/profile.service'; -import { Organization } from 'src/app/organization/organization.model'; -import { Event } from 'src/app/event/event.model'; -import { profileResolver } from 'src/app/profile/profile.resolver'; -import { EventService } from 'src/app/event/event.service'; -import { eventResolver } from '../event.resolver'; -import { OrganizationService } from 'src/app/organization/organization.service'; - -@Component({ - selector: 'app-event-list-admin', - templateUrl: './event-list-admin.component.html', - styleUrls: ['./event-list-admin.component.css'] -}) -export class EventListAdminComponent implements OnInit { - /** Events List */ - protected displayedEvents$: Observable; - - public displayedColumns: string[] = ['name']; - - /** Profile of signed in user */ - protected profile: Profile; - - /** Route information to be used in Organization Routing Module */ - public static Route = { - path: 'admin', - component: EventListAdminComponent, - title: 'Event Administration', - resolve: { - profile: profileResolver, - events: eventResolver - } - }; - - constructor( - private route: ActivatedRoute, - private router: Router, - private snackBar: MatSnackBar, - private organizationAdminService: OrganizationService, - private eventService: EventService - ) { - this.displayedEvents$ = eventService.getEvents(); - - /** Get the profile data of the signed in user */ - const data = this.route.snapshot.data as { - profile: Profile; - }; - this.profile = data.profile; - } - - ngOnInit() { - if (this.profile.permissions[0].resource !== '*') { - let userOrganizationPermissions: string[] = this.profile.permissions - .filter((permission) => permission.resource.includes('organization')) - .map((permission) => permission.resource.substring(13)); - - this.displayedEvents$ = this.displayedEvents$.pipe( - map((events) => - events.filter( - (event) => - event.organization && - userOrganizationPermissions.includes(event.organization.slug) - ) - ) - ); - } - } - - /** Resposible for generating delete and create buttons in HTML code when admin signed in */ - adminPermissions(): boolean { - return this.profile.permissions[0].resource === '*'; - } - - /** Event handler to open Event Editor for the selected event. - * @param event: event to be edited - * @returns void - */ - editEvent(event: Event): void { - this.router.navigate([ - 'events', - 'organizations', - event.organization?.slug, - 'events', - event.id, - 'edit' - ]); - } -} diff --git a/frontend/src/app/event/event-page/event-page.component.html b/frontend/src/app/event/event-page/event-page.component.html index 56a65af2d..d79c6871c 100644 --- a/frontend/src/app/event/event-page/event-page.component.html +++ b/frontend/src/app/event/event-page/event-page.component.html @@ -3,46 +3,30 @@ + (searchBarQueryChange)="onSearchBarQueryChange($event)" />
- - -

- Today {{ endDate | date: 'mediumDate' }} +

+ {{ startDate() | date: 'mediumDate' }} + {{ endDate() | date: 'mediumDate' }}

- -

- {{ startDate | date: 'mediumDate' }} - - {{ endDate | date: 'mediumDate' }} -

-
-
- -
- -
diff --git a/frontend/src/app/event/event-page/event-page.component.ts b/frontend/src/app/event/event-page/event-page.component.ts index f52501267..e6a374f8c 100644 --- a/frontend/src/app/event/event-page/event-page.component.ts +++ b/frontend/src/app/event/event-page/event-page.component.ts @@ -3,268 +3,159 @@ * events hosted by CS Organizations at UNC. * * @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney - * @copyright 2023 + * @copyright 2024 * @license MIT */ import { Component, - HostListener, - OnInit, - inject, - OnDestroy + Signal, + signal, + effect, + WritableSignal, + computed } from '@angular/core'; -import { profileResolver } from 'src/app/profile/profile.resolver'; -import { ActivatedRoute, ActivationEnd, Params, Router } from '@angular/router'; -import { Profile } from 'src/app/profile/profile.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Profile, ProfileService } from 'src/app/profile/profile.service'; import { Event } from '../event.model'; import { DatePipe } from '@angular/common'; -import { EventService } from '../event.service'; -import { NagivationAdminGearService } from 'src/app/navigation/navigation-admin-gear.service'; -import { EventPaginationParams, Paginated } from 'src/app/pagination'; import { - Subject, - Subscription, - debounceTime, - distinctUntilChanged, - filter, - tap -} from 'rxjs'; + DEFAULT_TIME_RANGE_PARAMS, + Paginated, + TimeRangePaginationParams +} from 'src/app/pagination'; +import { EventService } from '../event.service'; +import { GroupEventsPipe } from '../pipes/group-events.pipe'; @Component({ selector: 'app-event-page', templateUrl: './event-page.component.html', styleUrls: ['./event-page.component.css'] }) -export class EventPageComponent implements OnInit, OnDestroy { - public page: Paginated; - public startDate = new Date(); - public endDate = new Date(new Date().setMonth(new Date().getMonth() + 1)); - public today: boolean = true; - - private static EventPaginationParams = { - order_by: 'time', - ascending: 'true', - filter: '', - range_start: new Date().toLocaleString('en-GB'), - range_end: new Date( - new Date().setMonth(new Date().getMonth() + 1) - ).toLocaleString('en-GB') - }; - +export class EventPageComponent { /** Route information to be used in App Routing Module */ public static Route = { path: '', title: 'Events', component: EventPageComponent, - canActivate: [], - resolve: { - profile: profileResolver, - page: () => - inject(EventService).list(EventPageComponent.EventPaginationParams) - } + canActivate: [] }; - /** Store the content of the search bar */ - public searchBarQuery = ''; + /** Stores a reactive event pagination page. */ + public page: WritableSignal< + Paginated | undefined + > = signal(undefined); + private previousParams: TimeRangePaginationParams = DEFAULT_TIME_RANGE_PARAMS; + + /** Stores a reactive mapping of days to events on the active page. */ + protected eventsByDate: Signal<[string, Event[]][]> = computed(() => { + return this.groupEventsPipe.transform(this.page()?.items ?? []); + }); - /** Store a map of days to a list of events for that day */ - public eventsPerDay: [string, Event[]][]; + /** Stores reactive date signals for the bounds of pagination. */ + public startDate: WritableSignal = signal(new Date()); + public endDate: WritableSignal = signal( + new Date(new Date().setMonth(new Date().getMonth() + 1)) + ); + public filterQuery: WritableSignal = signal(''); - /** Store the selected Event */ - public selectedEvent: Event | null = null; + /** Store the content of the search bar */ + public searchBarQuery = ''; /** Store the currently-logged-in user's profile. */ public profile: Profile; - /** Stores the width of the window. */ - public innerWidth: any; - - /** Search bar query string */ - public query: string = ''; - - public searchUpdate = new Subject(); - - private routeSubscription!: Subscription; - /** Constructor for the events page. */ constructor( private route: ActivatedRoute, private router: Router, public datePipe: DatePipe, public eventService: EventService, - private gearService: NagivationAdminGearService + private profileService: ProfileService, + protected groupEventsPipe: GroupEventsPipe ) { - // Initialize data from resolvers - const data = this.route.snapshot.data as { - profile: Profile; - page: Paginated; - }; - this.profile = data.profile; - this.page = data.page; - this.today = - this.startDate.setHours(0, 0, 0, 0) == new Date().setHours(0, 0, 0, 0); - - // Group events by their dates - this.eventsPerDay = eventService.groupEventsByDate(this.page.items); - - // Initialize the initially selected event - if (data.page.items.length > 0) { - this.selectedEvent = this.page.items[0]; - } - - this.searchUpdate - .pipe( - filter((search: string) => search.length > 2 || search.length == 0), - debounceTime(500), - distinctUntilChanged() - ) - .subscribe((query) => { - this.onSearchBarQueryChange(query); - }); + this.profile = this.profileService.profile()!; } - /** Runs when the frontend UI loads */ - ngOnInit() { - if (this.profile !== undefined) { - let userPermissions = this.profile.permissions; - /** Ensure that the signed in user has permissions before looking at the resource */ - if (userPermissions.length !== 0) { - /** Admin user, no need to check further */ - if (userPermissions[0].resource === '*') { - this.gearService.showAdminGearByPermissionCheck( - 'organizations.*', - '*', - '', - 'events/admin' - ); - } else { - /** Find if the signed in user has any organization permissions */ - let organizationPermissions = userPermissions.filter((element) => - element.resource.includes('organization') - ); - /** If they do, show admin gear */ - if (organizationPermissions.length !== 0) { - this.gearService.showAdminGearByPermissionCheck( - 'organizations.*', - organizationPermissions[0].resource, - '', - 'events/admin' - ); - } - } - } - } - // Keep track of the initial width of the browser window - this.innerWidth = window.innerWidth; - - // Watch current route's query params - this.route.queryParams.subscribe((params: Params): void => { - this.startDate = params['start_date'] - ? new Date(Date.parse(params['start_date'])) - : new Date(); - this.endDate = params['end_date'] - ? new Date(Date.parse(params['end_date'])) - : new Date(new Date().setMonth(new Date().getMonth() + 1)); - }); - - const today = new Date(); - if (this.startDate.getTime() < today.setHours(0, 0, 0, 0)) { - this.page.params.ascending = 'false'; - } - - let paginationParams = this.page.params; - paginationParams.range_start = this.startDate.toLocaleString('en-GB'); - paginationParams.range_end = this.endDate.toLocaleString('en-GB'); - this.eventService.list(paginationParams).subscribe((page) => { - this.eventsPerDay = this.eventService.groupEventsByDate(page.items); + /** + * Effect that refreshes the event pagination when the time range changes. This effect + * is also called when the page initially loads. + * + * This effect also reloads the query parameters in the URL so that the URL in the + * browser reflects the newly changed start and end date ranges. + */ + paginationTimeRangeEffect = effect(() => { + // Update the parameters with the new date range + let params = this.previousParams; + params.range_start = this.startDate().toISOString(); + params.range_end = this.endDate().toISOString(); + params.filter = this.filterQuery(); + // Refresh the data + this.eventService.getEvents(params).subscribe((events) => { + this.page.set(events); + this.previousParams = events.params; + this.reloadQueryParams(); }); - - let prevUrl = ''; - this.routeSubscription = this.router.events - .pipe( - filter((e) => e instanceof ActivationEnd), - distinctUntilChanged(() => this.router.url === prevUrl), - tap(() => (prevUrl = this.router.url)) - ) - .subscribe((_) => { - this.page.params.ascending = ( - this.startDate.getTime() > today.setHours(0, 0, 0, 0) - ).toString(); - let paginationParams = this.page.params; - paginationParams.range_start = this.startDate.toLocaleString('en-GB'); - paginationParams.range_end = this.endDate.toLocaleString('en-GB'); - this.eventService.list(paginationParams).subscribe((page) => { - this.eventsPerDay = this.eventService.groupEventsByDate(page.items); - }); - }); + }); + + /** Reloads the page and its query parameters to adjust to the next month. */ + nextPage() { + this.startDate.set( + new Date(this.startDate().setMonth(this.startDate().getMonth() + 1)) + ); + this.endDate.set( + new Date(this.endDate().setMonth(this.endDate().getMonth() + 1)) + ); } - ngOnDestroy() { - this.routeSubscription.unsubscribe(); + /** Reloads the page and its query parameters to adjust to the previous month. */ + previousPage() { + this.startDate.set( + new Date(this.startDate().setMonth(this.startDate().getMonth() - 1)) + ); + this.endDate.set( + new Date(this.endDate().setMonth(this.endDate().getMonth() - 1)) + ); } - /** Handler that runs when the window resizes */ - @HostListener('window:resize', ['$event']) - onResize(_: UIEvent) { - // Update the browser window width - this.innerWidth = window.innerWidth; + /** + * Reloads the page to update the query parameters and reload the data. + * This is required so that the correct query parameters are reflected in the + * browser's URL field. + * @param startDate: The new start date + * @param endDate: The new end date + */ + reloadQueryParams() { + this.router.navigate([], { + relativeTo: this.route, + queryParams: { + start_date: this.startDate().toISOString(), + end_date: this.endDate().toISOString() + }, + queryParamsHandling: 'merge' + }); } + // TODO: Refactor this method to remove manual +/- 100 year range on query filtering. + /** Handler that runs when the search bar query changes. * @param query: Search bar query to filter the items */ onSearchBarQueryChange(query: string) { - this.query = query; - let paginationParams = this.page.params; - paginationParams.ascending = 'true'; - if (query == '') { - paginationParams.range_start = this.startDate.toLocaleString('en-GB'); - paginationParams.range_end = this.endDate.toLocaleString('en-GB'); + if (query === '') { + this.startDate.set(new Date()); + this.endDate.set( + new Date(new Date().setMonth(new Date().getMonth() + 1)) + ); } else { - paginationParams.range_start = new Date( - new Date().setFullYear(new Date().getFullYear() - 100) - ).toLocaleString('en-GB'); - paginationParams.range_end = new Date( - new Date().setFullYear(new Date().getFullYear() + 100) - ).toLocaleString('en-GB'); - paginationParams.filter = this.query; - } - this.eventService.list(paginationParams).subscribe((page) => { - this.eventsPerDay = this.eventService.groupEventsByDate(page.items); - paginationParams.filter = ''; - }); - } - - /** Handler that runs when an event card is clicked. - * This function selects the event to display on the sidebar. - * @param event: Event pressed - */ - onEventCardClicked(event: Event) { - this.selectedEvent = event; - } - - showEvents(isPrevious: boolean) { - //let paginationParams = this.page.params; - this.startDate = isPrevious - ? new Date(this.startDate.setMonth(this.startDate.getMonth() - 1)) - : new Date(this.startDate.setMonth(this.startDate.getMonth() + 1)); - this.endDate = isPrevious - ? new Date(this.endDate.setMonth(this.endDate.getMonth() - 1)) - : new Date(this.endDate.setMonth(this.endDate.getMonth() + 1)); - if (isPrevious === true) { - this.page.params.ascending = 'false'; + this.startDate.set( + new Date(new Date().setMonth(new Date().getFullYear() - 100)) + ); + this.endDate.set( + new Date(new Date().setMonth(new Date().getFullYear() + 100)) + ); } - this.today = - this.startDate.setHours(0, 0, 0, 0) == new Date().setHours(0, 0, 0, 0); - this.router.navigate([], { - relativeTo: this.route, - queryParams: { - start_date: this.startDate.toISOString(), - end_date: this.endDate.toISOString() - }, - queryParamsHandling: 'merge' - }); + this.filterQuery.set(query); } } diff --git a/frontend/src/app/event/event-routing.module.ts b/frontend/src/app/event/event-routing.module.ts index f8dd50367..8cc9ea5cd 100644 --- a/frontend/src/app/event/event-routing.module.ts +++ b/frontend/src/app/event/event-routing.module.ts @@ -12,10 +12,8 @@ import { RouterModule, Routes } from '@angular/router'; import { EventDetailsComponent } from './event-details/event-details.component'; import { EventPageComponent } from './event-page/event-page.component'; import { EventEditorComponent } from './event-editor/event-editor.component'; -import { EventListAdminComponent } from './event-list-admin/event-list-admin.component'; const routes: Routes = [ - EventListAdminComponent.Route, EventPageComponent.Route, EventDetailsComponent.Route, EventEditorComponent.Route diff --git a/frontend/src/app/event/event.model.ts b/frontend/src/app/event/event.model.ts index 9ada9851c..0d0d12efe 100644 --- a/frontend/src/app/event/event.model.ts +++ b/frontend/src/app/event/event.model.ts @@ -3,7 +3,7 @@ * the Event Service and the API. * * @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney - * @copyright 2023 + * @copyright 2024 * @license MIT */ @@ -54,8 +54,10 @@ export interface EventJson { * objects (such as `Date`s) as strings. We need to convert this to * TypeScript objects ourselves. */ -export const parseEventJson = (eventJson: EventJson): Event => { - return Object.assign({}, eventJson, { time: new Date(eventJson.time) }); +export const parseEventJson = (responseModel: EventJson): Event => { + return Object.assign({}, responseModel, { + time: new Date(responseModel.time) + }); }; export enum RegistrationType { diff --git a/frontend/src/app/event/event.module.ts b/frontend/src/app/event/event.module.ts index 91ec7892f..e667ce47a 100644 --- a/frontend/src/app/event/event.module.ts +++ b/frontend/src/app/event/event.module.ts @@ -4,7 +4,7 @@ * application and decouples this feature from other features in the application. * * @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney - * @copyright 2023 + * @copyright 2024 * @license MIT */ @@ -38,7 +38,7 @@ import { EventDetailsComponent } from './event-details/event-details.component'; import { EventPageComponent } from './event-page/event-page.component'; import { EventEditorComponent } from './event-editor/event-editor.component'; import { EventUsersList } from './widgets/event-users-list/event-users-list.widget'; -import { EventListAdminComponent } from './event-list-admin/event-list-admin.component'; +import { GroupEventsPipe } from './pipes/group-events.pipe'; @NgModule({ declarations: [ @@ -46,8 +46,8 @@ import { EventListAdminComponent } from './event-list-admin/event-list-admin.com EventDetailsComponent, EventPageComponent, EventEditorComponent, - EventListAdminComponent, - EventUsersList + EventUsersList, + GroupEventsPipe ], imports: [ CommonModule, @@ -70,6 +70,7 @@ import { EventListAdminComponent } from './event-list-admin/event-list-admin.com RouterModule, SharedModule, EventRoutingModule - ] + ], + providers: [GroupEventsPipe] }) export class EventModule {} diff --git a/frontend/src/app/event/event.resolver.ts b/frontend/src/app/event/event.resolver.ts index 8f5fc211f..9db6d8235 100644 --- a/frontend/src/app/event/event.resolver.ts +++ b/frontend/src/app/event/event.resolver.ts @@ -3,7 +3,7 @@ * of components. * * @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney - * @copyright 2023 + * @copyright 2024 * @license MIT */ @@ -12,17 +12,8 @@ import { ResolveFn } from '@angular/router'; import { Event } from './event.model'; import { EventService } from './event.service'; -/** This resolver injects the list of events into the events component. */ -export const eventResolver: ResolveFn = (route, state) => { - return inject(EventService).getEvents(); -}; - /** This resolver injects an event into the events detail component. */ -export const eventDetailResolver: ResolveFn = ( - route, - state -) => { - console.log(route.paramMap); +export const eventResolver: ResolveFn = (route, state) => { if (route.paramMap.get('id') != 'new') { return inject(EventService).getEvent(+route.paramMap.get('id')!); } else { diff --git a/frontend/src/app/event/event.service.ts b/frontend/src/app/event/event.service.ts index 994d02eaf..4f539c6da 100644 --- a/frontend/src/app/event/event.service.ts +++ b/frontend/src/app/event/event.service.ts @@ -3,252 +3,132 @@ * from the components. * * @author Ajay Gandecha, Jade Keegan, Brianna Ta, Audrey Toney - * @copyright 2023 + * @copyright 2024 * @license MIT */ import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, Subscription, map, tap } from 'rxjs'; +import { + DEFAULT_TIME_RANGE_PARAMS, + Paginated, + PaginationParams, + Paginator, + TimeRangePaginationParams, + TimeRangePaginator +} from '../pagination'; import { Event, EventJson, EventRegistration, parseEventJson } from './event.model'; -import { DatePipe } from '@angular/common'; -import { Profile, ProfileService } from '../profile/profile.service'; -import { - Paginated, - PaginationParams, - TimeRangePaginationParams -} from '../pagination'; -import { RxEvent } from './rx-event'; +import { Observable, map } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { Profile } from '../models.module'; @Injectable({ providedIn: 'root' }) export class EventService { - private profile: Profile | undefined; - private profileSubscription!: Subscription; + /** Encapsulated paginators */ + private eventsPaginator: TimeRangePaginator = + new TimeRangePaginator('/api/events/paginate'); - private events: RxEvent = new RxEvent(); - public events$: Observable = this.events.value$; + /** Constructor */ + constructor(protected http: HttpClient) {} - constructor( - protected http: HttpClient, - protected profileSvc: ProfileService, - public datePipe: DatePipe - ) { - this.profileSubscription = this.profileSvc.profile$.subscribe( - (profile) => (this.profile = profile) - ); - } + // Methods for event data. - /** Returns paginated user entries from the backend database table using the backend HTTP get request. - * @returns {Observable>} + /** + * Retrieves a page of events based on pagination parameters. + * @param params: Pagination parameters. + * @returns {Observable>} */ - getRegisteredUsersForEvent(event_id: number, params: PaginationParams) { - let paramStrings = { - page: params.page.toString(), - page_size: params.page_size.toString(), - order_by: params.order_by, - filter: params.filter - }; - let query = new URLSearchParams(paramStrings); - return this.http.get>( - `/api/events/${event_id}/registrations/users?` + query.toString() - ); + getEvents(params: TimeRangePaginationParams = DEFAULT_TIME_RANGE_PARAMS) { + return this.eventsPaginator.loadPage(params, parseEventJson); } - /** Returns all event entries from the backend database table using the backend HTTP get request. - * @returns {Observable} + /** + * Gets an event based on its id. + * @param id: ID for the event. + * @returns {Observable} */ - getEvents(): Observable { - if (this.profile) { - return this.http - .get('/api/events/range') - .pipe(map((eventJsons) => eventJsons.map(parseEventJson))); - } else { - // if a user isn't logged in, return the normal endpoint without registration statuses - return this.http - .get('/api/events/range/unauthenticated') - .pipe(map((eventJsons) => eventJsons.map(parseEventJson))); - } + getEvent(id: number): Observable { + return this.http + .get('/api/events/' + id) + .pipe(map((eventJson) => parseEventJson(eventJson))); } - /** Returns the event object from the backend database table using the backend HTTP get request. - * @param id: ID of the event to retrieve - * @returns {Observable} - */ - getEvent(id: number): Observable { - if (this.profile) { - return this.http - .get('/api/events/' + id) - .pipe(map((eventJson) => parseEventJson(eventJson))); - } else { - return this.http - .get('/api/events/' + id + '/unauthenticated') - .pipe(map((eventJson) => parseEventJson(eventJson))); - } - } - - /** Returns the event object from the backend database table using the backend HTTP get request. - * @param slug: Slug of the organization to retrieve - * @returns {Observable} - */ - getEventsByOrganization(slug: string): Observable { - if (this.profile) { - return this.http - .get('/api/events/organization/' + slug) - .pipe(map((eventJsons) => eventJsons.map(parseEventJson))); - } else { - return this.http - .get( - '/api/events/organization/' + slug + '/unauthenticated' - ) - .pipe(map((eventJsons) => eventJsons.map(parseEventJson))); - } - } - - /** Returns the new event object from the backend database table using the backend HTTP get request. - * @param event: model of the event to be created + /** + * Returns the new event from the backend database table using the HTTP post request + * and refreshes the current paginated events page. + * @param event Event to add * @returns {Observable} */ createEvent(event: Event): Observable { return this.http.post('/api/events', event); } - /** Returns the updated event object from the backend database table using the backend HTTP put request. - * @param event: Event representing the updated event + /** + * Returns the updated event from the backend database table using the HTTP put request + * and refreshes the current paginated events page. + * @param event Event to update * @returns {Observable} */ updateEvent(event: Event): Observable { return this.http.put('/api/events', event); } - /** Delete the given event object using the backend HTTP delete request. W - * @param event: Event representing the updated event - * @returns void + /** + * Returns the deleted event from the backend database table using the HTTP delete request + * and refreshes the current paginated events page. + * @param event Event to delete + * @returns {Observable} */ deleteEvent(event: Event): Observable { return this.http.delete('/api/events/' + event.id); } - /** Helper function to group a list of events by date, - * filtered based on the input query string. - * @param events: List of the input events - * @param query: Search bar query to filter the items - */ - groupEventsByDate(events: Event[], query: string = ''): [string, Event[]][] { - // Initialize an empty map - let groups: Map = new Map(); + // Methods for event registration data. - // Transform the list of events based on the event filter pipe and query - events.forEach((event) => { - // Find the date to group by - let dateString = - this.datePipe.transform(event.time, 'EEEE, MMMM d, y') ?? ''; - // Add the event - let newEventsList = groups.get(dateString) ?? []; - newEventsList.push(event); - groups.set(dateString, newEventsList); - }); + // TODO: Refactor to remove, load event registrations instead. - // Return the groups - return [...groups.entries()]; - } - - // Event Registration Methods - /** Return an event registration if the user is registered for an event using the backend HTTP get request. - * @param event_id: number representing the Event ID - * @returns Observable + /** + * Loads a paginated list of registered users for a given event. + * @param event: Event to load registrations for. + * @param params: Pagination parameters. + * @returns {Observable>} */ - getEventRegistrationOfUser(event_id: number): Observable { - return this.http.get( - `/api/events/${event_id}/registration` + getRegisteredUsersForEvent( + event: Event, + params: PaginationParams + ): Observable> { + const paginator: Paginator = new Paginator( + `/api/events/${event.id}/registrations/users` ); + return paginator.loadPage(params); } - /** Return all event registrations an event using the backend HTTP get request. - * @param event_id: number representing the Event ID - * @returns Observable + /** + * Registers the current user to an event. + * @param event: Event to register to. + * @returns {Observable} */ - getEventRegistrations(event_id: number): Observable { - return this.http.get( - `/api/events/${event_id}/registrations` - ); - } - - /** Return number of event registrations for an event - * @param event_id: number representing the Event ID - * @returns Observable - */ - getEventRegistrationCount(event_id: number): Observable { - return this.http.get(`/api/events/${event_id}/registration/count`); - } - - /** Create a new registration for an event using the backend HTTP create request. - * @param event_id: number representing the Event ID - * @returns Observable - */ - registerForEvent(event_id: number): Observable { - if (this.profile === undefined) { - throw new Error('Only allowed for logged in users.'); - } - + registerForEvent(event: Event): Observable { return this.http.post( - `/api/events/${event_id}/registration`, + `/api/events/${event.id}/registration`, {} ); } - /** Delete an existing registration for an event using the backend HTTP delete request. - * @param event_registration_id: number representing the Event Registration ID - * @returns void + /** + * Unregisters the current user from an event. + * @param event: Event to unregister from. + * @returns {Observable} */ - unregisterForEvent(event_id: number) { - if (this.profile === undefined) { - throw new Error('Only allowed for logged in users.'); - } - + unregisterForEvent(event: Event): Observable { return this.http.delete( - `/api/events/${event_id}/registration` + `/api/events/${event.id}/registration` ); } - - list(params: TimeRangePaginationParams) { - let paramStrings = { - order_by: params.order_by, - ascending: params.ascending, - filter: params.filter, - range_start: params.range_start, - range_end: params.range_end - }; - let query = new URLSearchParams(paramStrings); - if (this.profile) { - return this.http - .get>( - '/api/events/paginate?' + query.toString() - ) - .pipe( - map((paginated) => ({ - ...paginated, - items: paginated.items.map(parseEventJson) - })) - ); - } else { - // if a user isn't logged in, return the normal endpoint without registration statuses - return this.http - .get>( - '/api/events/paginate/unauthenticated?' + query.toString() - ) - .pipe( - map((paginated) => ({ - ...paginated, - items: paginated.items.map(parseEventJson) - })) - ); - } - } } diff --git a/frontend/src/app/event/pipes/group-events.pipe.ts b/frontend/src/app/event/pipes/group-events.pipe.ts new file mode 100644 index 000000000..4ef3330df --- /dev/null +++ b/frontend/src/app/event/pipes/group-events.pipe.ts @@ -0,0 +1,36 @@ +/** + * This is the pipe used to group events in a page by day. + * @author Ajay Gandecha + * @copyright 2024 + * @license MIT + */ + +import { DatePipe } from '@angular/common'; +import { Pipe, PipeTransform, inject } from '@angular/core'; +import { Event } from '../event.model'; + +@Pipe({ + name: 'groupEvents' +}) +export class GroupEventsPipe implements PipeTransform { + datePipe = inject(DatePipe); + + transform(events: Event[]): [string, Event[]][] { + // Initialize an empty map + let groups: Map = new Map(); + + // Transform the list of events based on the event filter pipe and query + events.forEach((event) => { + // Find the date to group by + let dateString = + this.datePipe.transform(event.time, 'EEEE, MMMM d, y') ?? ''; + // Add the event + let newEventsList = groups.get(dateString) ?? []; + newEventsList.push(event); + groups.set(dateString, newEventsList); + }); + + // Return the groups + return [...groups.entries()]; + } +} diff --git a/frontend/src/app/event/rx-event.ts b/frontend/src/app/event/rx-event.ts deleted file mode 100644 index 45f07eff4..000000000 --- a/frontend/src/app/event/rx-event.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * The RxEvent object is used to ensure proper updating and - * retrieval of the list of all events in the database. - * - * @author Ben Goulet - * @copyright 2024 - * @license MIT - */ - -import { RxObject } from '../rx-object'; -import { Event } from './event.model'; - -export class RxEvent extends RxObject { - pushEvent(event: Event): void { - this.value.push(event); - this.notify(); - } - - updateEvent(event: Event): void { - this.value = this.value.map((o) => { - return o.id !== event.id ? o : event; - }); - this.notify(); - } -} diff --git a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html index 0ad6ee763..37ec7de3a 100644 --- a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html +++ b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.html @@ -11,19 +11,14 @@
- + } - -
@@ -55,50 +50,43 @@ -
- - If you have any questions or concerns about this event, please contact one - of the organizers below: - - + @if (profile && event.organizers.length > 0) { +
+ If you have any questions or concerns about this event, please contact the - organizer: - + organizer{{ event.organizers.length > 1 && 's' }} below: +
-
+ } -
+ @if (profile && event.registration_limit > 0) { +

Seats Remaining: - {{ event.registration_limit - event.registration_count }} / {{ - event.registration_limit }} + {{ event.registration_limit - event.registration_count }} / + {{ event.registration_limit }}

+ @if (event.is_attendee || event.is_organizer) { - - - + } @else { + + }
+ } diff --git a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.ts b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.ts index e8ca2e576..71b39bc0c 100644 --- a/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.ts +++ b/frontend/src/app/event/widgets/event-detail-card/event-detail-card.widget.ts @@ -3,12 +3,12 @@ * detail event card from the whole event page. * * @author Ajay Gandecha, Jade Keegan - * @copyright 2023 + * @copyright 2024 * @license MIT */ import { Component, Input, OnInit } from '@angular/core'; -import { Event, EventRegistration } from '../../event.model'; +import { Event } from '../../event.model'; import { MatSnackBar } from '@angular/material/snack-bar'; import { EventService } from '../../event.service'; import { Observable } from 'rxjs'; @@ -75,48 +75,42 @@ export class EventDetailCard implements OnInit { }); } - /** Registers a user for the given event - * @param event_id: number representing the id of the Event to register the User for - */ - registerForEvent(event_id: number) { + /** Registers a user for the event. */ + registerForEvent() { let confirmRegistration = this.snackBar.open( 'Are you sure you want to register for this event?', 'Register' ); confirmRegistration.onAction().subscribe(() => { - this.eventService.registerForEvent(event_id).subscribe({ - next: (event_registration) => this.onSuccess(event_registration), - error: (err) => this.onError(err) + this.eventService.registerForEvent(this.event).subscribe({ + next: () => this.onSuccess(), + error: () => this.onError() }); }); } - /** Registers a user for the given event - * @param event_id: number representing the id of the Event to register the User for - */ - unregisterForEvent(event_registration_id: number) { + /** Unregisters the user for the event. */ + unregisterForEvent() { let confirmUnregistration = this.snackBar.open( 'Are you sure you want to unregister for this event?', 'Unregister', { duration: 15000 } ); confirmUnregistration.onAction().subscribe(() => { - this.eventService - .unregisterForEvent(event_registration_id) - .subscribe(() => { - this.event.is_attendee = false; - this.event.registration_count -= 1; - this.snackBar.open('Successfully Unregistered!', '', { - duration: 2000 - }); + this.eventService.unregisterForEvent(this.event).subscribe(() => { + this.event.is_attendee = false; + this.event.registration_count -= 1; + this.snackBar.open('Successfully Unregistered!', '', { + duration: 2000 }); + }); }); } /** Opens a confirmation snackbar when an event is successfully created. * @returns {void} */ - private onSuccess(event_registration: EventRegistration): void { + private onSuccess(): void { this.event.is_attendee = true; this.event.registration_count += 1; this.snackBar.open('Thanks for registering!', '', { duration: 2000 }); @@ -125,7 +119,7 @@ export class EventDetailCard implements OnInit { /** Opens a confirmation snackbar when there is an error creating an event. * @returns {void} */ - private onError(err: any): void { + private onError(): void { this.snackBar.open('Error: Event Not Registered For', '', { duration: 2000 }); diff --git a/frontend/src/app/event/widgets/event-users-list/event-users-list.widget.html b/frontend/src/app/event/widgets/event-users-list/event-users-list.widget.html index 792a07e69..cb455abf1 100644 --- a/frontend/src/app/event/widgets/event-users-list/event-users-list.widget.html +++ b/frontend/src/app/event/widgets/event-users-list/event-users-list.widget.html @@ -17,7 +17,8 @@ -
+ @if (page) { +
@@ -45,4 +46,5 @@ [pageIndex]="page.params.page" (page)="handlePageEvent($event)"> + } diff --git a/frontend/src/app/event/widgets/event-users-list/event-users-list.widget.ts b/frontend/src/app/event/widgets/event-users-list/event-users-list.widget.ts index 99bc7c9c6..1f38a586b 100644 --- a/frontend/src/app/event/widgets/event-users-list/event-users-list.widget.ts +++ b/frontend/src/app/event/widgets/event-users-list/event-users-list.widget.ts @@ -14,6 +14,9 @@ import { Profile } from 'src/app/models.module'; import { EventService } from '../../event.service'; import { Event } from '../../event.model'; +// TODO: This component is to be deleted anyway, so it was +// not included in the refactor. + @Component({ selector: 'event-users-list', templateUrl: './event-users-list.widget.html', @@ -37,8 +40,8 @@ export class EventUsersList implements OnInit { ngOnInit() { this.eventService .getRegisteredUsersForEvent( - this.event.id!, - EventUsersList.PaginationParams + this.event, + EventUsersList.PaginationParams as PaginationParams ) .subscribe((page) => (this.page = page)); } @@ -48,7 +51,7 @@ export class EventUsersList implements OnInit { paginationParams.page = e.pageIndex; paginationParams.page_size = e.pageSize; this.eventService - .getRegisteredUsersForEvent(this.event.id!, paginationParams) + .getRegisteredUsersForEvent(this.event, paginationParams) .subscribe((page) => (this.page = page)); } } diff --git a/frontend/src/app/organization/organization-admin/organization-admin.component.ts b/frontend/src/app/organization/organization-admin/organization-admin.component.ts index 7c7d95774..dae08a1f8 100644 --- a/frontend/src/app/organization/organization-admin/organization-admin.component.ts +++ b/frontend/src/app/organization/organization-admin/organization-admin.component.ts @@ -8,12 +8,11 @@ */ import { Component, Signal } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Router } from '@angular/router'; import { Organization } from '../organization.model'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Observable } from 'rxjs'; -import { Profile } from '../../profile/profile.service'; -import { profileResolver } from '../../profile/profile.resolver'; +import { Profile, ProfileService } from '../../profile/profile.service'; import { OrganizationService } from '../organization.service'; import { PermissionService } from '../../permission.service'; @@ -36,25 +35,19 @@ export class OrganizationAdminComponent { public static Route = { path: 'admin', component: OrganizationAdminComponent, - title: 'Organization Administration', - resolve: { profile: profileResolver } + title: 'Organization Administration' }; constructor( - private route: ActivatedRoute, private router: Router, private snackBar: MatSnackBar, + private profileService: ProfileService, private organizationService: OrganizationService, private permissionService: PermissionService ) { + this.profile = this.profileService.profile()!; this.organizations = organizationService.organizations; this.displayedOrganizations = organizationService.adminOrganizations; - - /** Get the profile data of the signed in user */ - const data = this.route.snapshot.data as { - profile: Profile; - }; - this.profile = data.profile; } /** Resposible for generating delete and create buttons in HTML code when admin signed in. diff --git a/frontend/src/app/organization/organization-details/organization-details.component.html b/frontend/src/app/organization/organization-details/organization-details.component.html index 4e9c91d58..b7100b5a5 100644 --- a/frontend/src/app/organization/organization-details/organization-details.component.html +++ b/frontend/src/app/organization/organization-details/organization-details.component.html @@ -6,14 +6,4 @@ - -
- -
} diff --git a/frontend/src/app/organization/organization-details/organization-details.component.ts b/frontend/src/app/organization/organization-details/organization-details.component.ts index 147d3d3f6..4e6f00b15 100644 --- a/frontend/src/app/organization/organization-details/organization-details.component.ts +++ b/frontend/src/app/organization/organization-details/organization-details.component.ts @@ -25,6 +25,7 @@ import { EventService } from '../../event/event.service'; import { Event } from '../../event/event.model'; import { Observable } from 'rxjs'; import { PermissionService } from '../../permission.service'; +import { GroupEventsPipe } from '../../event/pipes/group-events.pipe'; /** Injects the organization's name to adjust the title. */ let titleResolver: ResolveFn = (route: ActivatedRouteSnapshot) => { @@ -74,6 +75,7 @@ export class OrganizationDetailsComponent { protected snackBar: MatSnackBar, private profileService: ProfileService, protected eventService: EventService, + protected groupEventsPipe: GroupEventsPipe, private permission: PermissionService ) { this.profile = this.profileService.profile()!; @@ -84,7 +86,7 @@ export class OrganizationDetailsComponent { }; this.organization = data.organization; - this.eventsPerDay = eventService.groupEventsByDate(data.events ?? []); + this.eventsPerDay = this.groupEventsPipe.transform(data.events ?? []); this.eventCreationPermission$ = this.permission.check( 'organization.*', `organization/${this.organization?.slug ?? '*'}` diff --git a/frontend/src/app/organization/organization-page/organization-page.component.ts b/frontend/src/app/organization/organization-page/organization-page.component.ts index 9d0878d29..71c62c830 100644 --- a/frontend/src/app/organization/organization-page/organization-page.component.ts +++ b/frontend/src/app/organization/organization-page/organization-page.component.ts @@ -9,7 +9,6 @@ */ import { Component, Signal, effect } from '@angular/core'; -import { profileResolver } from '../../profile/profile.resolver'; import { Organization } from '../organization.model'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Profile, ProfileService } from '../../profile/profile.service'; @@ -26,9 +25,7 @@ export class OrganizationPageComponent { public static Route = { path: '', title: 'CS Organizations', - component: OrganizationPageComponent, - canActivate: [], - resolve: { profile: profileResolver } + component: OrganizationPageComponent }; /** Current search bar query on the organization page. */ diff --git a/frontend/src/app/organization/organization.resolver.ts b/frontend/src/app/organization/organization.resolver.ts index b3aa2ca91..92357f702 100644 --- a/frontend/src/app/organization/organization.resolver.ts +++ b/frontend/src/app/organization/organization.resolver.ts @@ -8,11 +8,10 @@ */ import { inject } from '@angular/core'; -import { Resolve, ResolveFn } from '@angular/router'; +import { ResolveFn } from '@angular/router'; import { Organization } from './organization.model'; -import { EventService } from '../event/event.service'; import { Event } from '../event/event.model'; -import { catchError, of } from 'rxjs'; +import { catchError, map, of } from 'rxjs'; import { OrganizationService } from './organization.service'; // TODO: Explore if this can be replaced by a signal. @@ -60,7 +59,7 @@ export const organizationEventsResolver: ResolveFn = ( route, _state ) => { - return inject(EventService).getEventsByOrganization( - route.paramMap.get('slug')! - ); + return inject(OrganizationService) + .getOrganization(route.paramMap.get('slug')!) + .pipe(map((organization) => organization?.events ?? [])); }; diff --git a/frontend/src/app/organization/organization.service.ts b/frontend/src/app/organization/organization.service.ts index 13d4e1db3..ae38d1bb9 100644 --- a/frontend/src/app/organization/organization.service.ts +++ b/frontend/src/app/organization/organization.service.ts @@ -23,6 +23,8 @@ export class OrganizationService { /** Organizations signal */ private organizationsSignal: WritableSignal = signal([]); organizations = this.organizationsSignal.asReadonly(); + + /** Computed organization signals */ adminOrganizations = computed(() => { return this.organizations().filter((organization) => { return this.permissionService.checkSignal( @@ -95,7 +97,7 @@ export class OrganizationService { ); } - /** Returns the deleted organization object from the backend database table using the backend HTTP put request + /** Returns the deleted organization object from the backend database table using the backend HTTP delete request * and updates the organizations signal to exclude the deleted organization. * @param organization: Represents the deleted organization * @returns {Observable} diff --git a/frontend/src/app/pagination.ts b/frontend/src/app/pagination.ts index bde671cd9..a83ef9d82 100644 --- a/frontend/src/app/pagination.ts +++ b/frontend/src/app/pagination.ts @@ -11,7 +11,7 @@ */ import { HttpClient } from '@angular/common/http'; -import { WritableSignal, signal } from '@angular/core'; +import { WritableSignal, inject, signal } from '@angular/core'; import { Observable, map, tap } from 'rxjs'; /** Defines the general model for the pagination parameters expected by the backend. */ @@ -31,6 +31,16 @@ export interface TimeRangePaginationParams extends URLSearchParams { range_end: string; } +export const DEFAULT_TIME_RANGE_PARAMS = { + order_by: 'time', + ascending: 'true', + filter: '', + range_start: new Date().toISOString(), + range_end: new Date( + new Date().setMonth(new Date().getMonth() + 1) + ).toISOString() +} as TimeRangePaginationParams; + /** * Interface that defines a page returned from a paginator. * @@ -70,7 +80,7 @@ abstract class PaginatorAbstraction { */ constructor( protected api: string, - protected http: HttpClient + protected http: HttpClient = inject(HttpClient) ) { this.api = api; } @@ -101,7 +111,7 @@ abstract class PaginatorAbstraction { */ loadPage( paramStrings: Params, - operator?: ((_: APIType) => T) | null + operator?: ((responseModel: APIType) => T) | null ): Observable> { // Stpres the previous pagination parameters used this.previousParams = paramStrings;
Name