diff --git a/src/app/events/components/events-list-entry/events-list-entry.component.html b/src/app/events/components/events-list-entry/events-list-entry.component.html new file mode 100644 index 000000000..f9533b774 --- /dev/null +++ b/src/app/events/components/events-list-entry/events-list-entry.component.html @@ -0,0 +1,31 @@ +
+ {{ event.Designation }} +
+
+ {{ event.dateFrom | date: "dd.MM.yyyy" }}–{{ + event.dateTo | date: "dd.MM.yyyy" + }} +
+
+ {{ event.studentCount }} + {{ + (event.studentCount === 1 ? "events.registration" : "events.registrations") + | translate + }} +
+
+ + arrow_right_alt + {{ event.evaluationText }} + + + arrow_right_alt + {{ event.evaluationText }} + +
diff --git a/src/app/events/components/events-list-entry/events-list-entry.component.scss b/src/app/events/components/events-list-entry/events-list-entry.component.scss new file mode 100644 index 000000000..32ebcad07 --- /dev/null +++ b/src/app/events/components/events-list-entry/events-list-entry.component.scss @@ -0,0 +1,66 @@ +@import "../../../../bootstrap-variables"; +@import "node_modules/bootstrap/scss/mixins"; + +:host { + display: grid; + padding: $spacer; + border-bottom: 1px solid $border-color; + grid-template-areas: "designation date registrations rating"; + grid-template-columns: 4fr 2fr 2fr 3fr; +} + +.designation { + grid-area: designation; + padding-right: $spacer; +} + +.date { + grid-area: date; + padding-right: $spacer; +} + +.registrations { + grid-area: registrations; + padding-right: $spacer; +} + +.rating { + grid-area: rating; + + a { + text-decoration: none; + } + + span { + text-decoration: underline; + } + + span:hover { + text-decoration-color: $accent; + } +} + +.registrations-label { + display: none; +} + +@include media-breakpoint-down(sm) { + .registrations-label { + display: inline; + } + + .designation, + .date, + .registrations { + padding-right: 0; + } + + :host { + grid-template-areas: + "designation" + "date" + "registrations" + "rating"; + grid-template-columns: 1fr; + } +} diff --git a/src/app/events/components/events-list-entry/events-list-entry.component.spec.ts b/src/app/events/components/events-list-entry/events-list-entry.component.spec.ts new file mode 100644 index 000000000..a24aec043 --- /dev/null +++ b/src/app/events/components/events-list-entry/events-list-entry.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { buildEvent } from "src/spec-builders"; +import { buildTestModuleMetadata } from "src/spec-helpers"; +import { EventsListEntryComponent } from "./events-list-entry.component"; + +describe("EventsListEntryComponent", () => { + let component: EventsListEntryComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule( + buildTestModuleMetadata({ + imports: [EventsListEntryComponent], + }), + ).compileComponents(); + + fixture = TestBed.createComponent(EventsListEntryComponent); + component = fixture.componentInstance; + element = fixture.debugElement.nativeElement; + + component.event = buildEvent(1); + component.event.evaluationText = "Lorem ipsum"; + }); + + it("renders entry without ratings column", () => { + component.withRatings = false; + + fixture.detectChanges(); + expect(element.querySelector(".rating")).toBeNull(); + expect(element.textContent).not.toContain("Lorem ipsum"); + }); + + it("renders entry with ratings column", () => { + component.withRatings = true; + + fixture.detectChanges(); + expect(element.querySelector(".rating")).toBeTruthy(); + expect(element.textContent).toContain("Lorem ipsum"); + }); +}); diff --git a/src/app/events/components/events-list-entry/events-list-entry.component.ts b/src/app/events/components/events-list-entry/events-list-entry.component.ts new file mode 100644 index 000000000..149815663 --- /dev/null +++ b/src/app/events/components/events-list-entry/events-list-entry.component.ts @@ -0,0 +1,17 @@ +import { DatePipe, NgIf } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { RouterLink } from "@angular/router"; +import { TranslateModule } from "@ngx-translate/core"; +import { Event } from "../../services/events-state.service"; + +@Component({ + selector: "bkd-events-list-entry", + standalone: true, + imports: [NgIf, RouterLink, DatePipe, TranslateModule], + templateUrl: "./events-list-entry.component.html", + styleUrl: "./events-list-entry.component.scss", +}) +export class EventsListEntryComponent { + @Input() event: Event; + @Input() withRatings: boolean = true; +} diff --git a/src/app/events/components/events-list/events-list.component.html b/src/app/events/components/events-list/events-list.component.html index cf04b8b30..506c0973c 100644 --- a/src/app/events/components/events-list/events-list.component.html +++ b/src/app/events/components/events-list/events-list.component.html @@ -8,7 +8,7 @@ [placeholder]="'events.search-by' | translate" [label]="'events.search' | translate" [disabled]="!data.events" - (valueChange)="state.search$.next($event)" + (valueChange)="state.setSearch($event)" > @@ -23,45 +23,11 @@ {{ "events.rating" | translate }} -
- -
- {{ event.dateFrom | date: "dd.MM.yyyy" }}–{{ - event.dateTo | date: "dd.MM.yyyy" - }} -
-
- {{ event.studentCount }} - {{ - (event.studentCount === 1 - ? "events.registration" - : "events.registrations" - ) | translate - }} -
- -
+
diff --git a/src/app/events/components/events-list/events-list.component.scss b/src/app/events/components/events-list/events-list.component.scss index fca58e9fe..b1443dc2f 100644 --- a/src/app/events/components/events-list/events-list.component.scss +++ b/src/app/events/components/events-list/events-list.component.scss @@ -13,70 +13,8 @@ grid-template-columns: 4fr 2fr 2fr 3fr; } -.event-entry { - display: grid; - padding: $spacer; - border-bottom: 1px solid $border-color; - grid-template-areas: "designation date registrations rating"; - grid-template-columns: 4fr 2fr 2fr 3fr; -} - -.designation { - grid-area: designation; - padding-right: $spacer; -} - -.date { - grid-area: date; - padding-right: $spacer; -} - -.registrations { - grid-area: registrations; - padding-right: $spacer; -} - -.rating { - grid-area: rating; - - a { - text-decoration: none; - } - - span { - text-decoration: underline; - } - - span:hover { - text-decoration-color: $accent; - } -} - -.registrations-label { - display: none; -} - @include media-breakpoint-down(sm) { .event-header { display: none; } - - .registrations-label { - display: inline; - } - - .designation, - .date, - .registrations { - padding-right: 0; - } - - .event-entry { - grid-template-areas: - "designation" - "date" - "registrations" - "rating"; - grid-template-columns: 1fr; - } } diff --git a/src/app/events/components/events-list/events-list.component.spec.ts b/src/app/events/components/events-list/events-list.component.spec.ts index ed1799ea5..319c42287 100644 --- a/src/app/events/components/events-list/events-list.component.spec.ts +++ b/src/app/events/components/events-list/events-list.component.spec.ts @@ -20,6 +20,9 @@ describe("EventsListComponent", () => { events$: of([buildEvent(1)]), search$: of(""), roles$, + setRoles(roles: Option>) { + roles$.next(roles); + }, getEvents: () => of([buildEvent(1)]), } as unknown as EventsStateService; @@ -48,18 +51,14 @@ describe("EventsListComponent", () => { fixture.detectChanges(); }); - it("should create", () => { - expect(component).toBeTruthy(); - }); - - it("should not show the ratings column", () => { + it("renders entry without ratings column", () => { component.withRatings = false; fixture.detectChanges(); expect(element.textContent).not.toContain("events.rating"); }); - it("should show the ratings column", () => { + it("renders entry with ratings column", () => { component.withRatings = true; fixture.detectChanges(); diff --git a/src/app/events/components/events-list/events-list.component.ts b/src/app/events/components/events-list/events-list.component.ts index 8f786743e..ad1c06c29 100644 --- a/src/app/events/components/events-list/events-list.component.ts +++ b/src/app/events/components/events-list/events-list.component.ts @@ -1,12 +1,12 @@ -import { AsyncPipe, DatePipe, NgFor, NgIf } from "@angular/common"; +import { AsyncPipe, NgFor, NgIf } from "@angular/common"; import { Component, Input } from "@angular/core"; -import { RouterLink } from "@angular/router"; import { TranslateModule } from "@ngx-translate/core"; import { ResettableInputComponent } from "../../../shared/components/resettable-input/resettable-input.component"; import { SpinnerComponent } from "../../../shared/components/spinner/spinner.component"; import { LetDirective } from "../../../shared/directives/let.directive"; import { StorageService } from "../../../shared/services/storage.service"; import { EventsStateService } from "../../services/events-state.service"; +import { EventsListEntryComponent } from "../events-list-entry/events-list-entry.component"; @Component({ selector: "bkd-events-list", @@ -18,11 +18,10 @@ import { EventsStateService } from "../../services/events-state.service"; ResettableInputComponent, NgIf, NgFor, - RouterLink, SpinnerComponent, AsyncPipe, - DatePipe, TranslateModule, + EventsListEntryComponent, ], }) export class EventsListComponent { @@ -31,6 +30,6 @@ export class EventsListComponent { public state: EventsStateService, private storage: StorageService, ) { - this.state.roles$.next(this.storage.getPayload()?.roles); + this.state.setRoles(this.storage.getPayload()?.roles ?? null); } } diff --git a/src/app/events/components/tests-header/tests-header.component.ts b/src/app/events/components/tests-header/tests-header.component.ts index bb86629ba..07ff9efcd 100644 --- a/src/app/events/components/tests-header/tests-header.component.ts +++ b/src/app/events/components/tests-header/tests-header.component.ts @@ -10,11 +10,11 @@ import { startWith, switchMap, } from "rxjs"; -import { EventsRestService } from "src/app/shared/services/events-rest.service"; import { BacklinkComponent } from "../../../shared/components/backlink/backlink.component"; import { ReportsLinkComponent } from "../../../shared/components/reports-link/reports-link.component"; import { Course } from "../../../shared/models/course.model"; import { ReportsService } from "../../../shared/services/reports.service"; +import { getCourseDesignation } from "../../utils/events"; @Component({ selector: "bkd-tests-header", @@ -44,10 +44,7 @@ export class TestsHeaderComponent implements OnChanges { startWith([]), ); - constructor( - private reportsService: ReportsService, - private eventsRestService: EventsRestService, - ) {} + constructor(private reportsService: ReportsService) {} ngOnChanges(changes: SimpleChanges): void { if (changes["course"]) { @@ -56,6 +53,6 @@ export class TestsHeaderComponent implements OnChanges { } getDesignation() { - return this.eventsRestService.getDesignation(this.course); + return getCourseDesignation(this.course); } } diff --git a/src/app/events/services/events-state.service.spec.ts b/src/app/events/services/events-state.service.spec.ts index a9cb66e1a..4e8b6dc2a 100644 --- a/src/app/events/services/events-state.service.spec.ts +++ b/src/app/events/services/events-state.service.spec.ts @@ -170,7 +170,7 @@ describe("EventsStateService", () => { describe("with ClassTeacherRole", () => { beforeEach(() => { - service.roles$.next("ClassTeacherRole;TeacherRole"); + service.setRoles("ClassTeacherRole;TeacherRole"); }); it("loads events", () => { @@ -192,7 +192,7 @@ describe("EventsStateService", () => { describe("without ClassTeacherRole", () => { beforeEach(() => { - service.roles$.next("TeacherRole"); + service.setRoles("TeacherRole"); }); it("loads events", () => { diff --git a/src/app/events/services/events-state.service.ts b/src/app/events/services/events-state.service.ts index 9f655e997..ee6dca8ac 100644 --- a/src/app/events/services/events-state.service.ts +++ b/src/app/events/services/events-state.service.ts @@ -13,14 +13,17 @@ import { SETTINGS, Settings } from "src/app/settings"; import { Course } from "src/app/shared/models/course.model"; import { StudyClass } from "src/app/shared/models/study-class.model"; import { CoursesRestService } from "src/app/shared/services/courses-rest.service"; -import { EventsRestService } from "src/app/shared/services/events-rest.service"; import { LoadingService } from "src/app/shared/services/loading-service"; -import { StorageService } from "src/app/shared/services/storage.service"; import { StudyClassesRestService } from "src/app/shared/services/study-classes-rest.service"; import { spread } from "src/app/shared/utils/function"; import { hasRole } from "src/app/shared/utils/roles"; import { searchEntries } from "src/app/shared/utils/search"; -import { EventStateWithLabel, getEventState, isRated } from "../utils/events"; +import { + EventStateWithLabel, + getCourseDesignation, + getEventState, + isRated, +} from "../utils/events"; export enum EventState { Rating = "rating", @@ -41,11 +44,13 @@ export interface Event { evaluationText?: string; evaluationLink?: Option; } + @Injectable({ providedIn: "root" }) export class EventsStateService { loading$ = this.loadingService.loading$; - search$ = new BehaviorSubject(""); - roles$ = new BehaviorSubject>(undefined); + private searchSubject$ = new BehaviorSubject(""); + search$ = this.searchSubject$.asObservable(); + private roles$ = new BehaviorSubject>(null); private formativeAssessments$ = this.studyClassRestService.getActiveFormativeAssessments(); @@ -59,12 +64,18 @@ export class EventsStateService { private coursesRestService: CoursesRestService, private studyClassRestService: StudyClassesRestService, private loadingService: LoadingService, - private storage: StorageService, private translate: TranslateService, - private eventsRestService: EventsRestService, @Inject(SETTINGS) private settings: Settings, ) {} + setSearch(term: string): void { + this.searchSubject$.next(term); + } + + setRoles(roles: Option): void { + this.roles$.next(roles); + } + getEvents(withRatings = false): Observable> { return this.filteredEvents$.pipe( map((events) => @@ -155,7 +166,7 @@ export class EventsStateService { return { id: course.Id, - Designation: this.eventsRestService.getDesignation(course), + Designation: getCourseDesignation(course), detailLink: this.buildLink(course.Id, "eventdetail"), studentCount: course.AttendanceRef.StudentCount || 0, dateFrom: course.DateFrom, diff --git a/src/app/events/utils/events.ts b/src/app/events/utils/events.ts index 4093dfb56..4ce34a99f 100644 --- a/src/app/events/utils/events.ts +++ b/src/app/events/utils/events.ts @@ -86,3 +86,11 @@ export function isRated(course: Course): boolean { !!course.FinalGrades?.length ); } + +export function getCourseDesignation(course: Course): string { + const classes = course.Classes + ? course.Classes.map((c) => c.Number).join(", ") + : null; + + return classes ? course.Designation + ", " + classes : course.Designation; +} diff --git a/src/app/presence-control/services/presence-control-group.service.ts b/src/app/presence-control/services/presence-control-group.service.ts index a85cd6f0e..217cd4deb 100644 --- a/src/app/presence-control/services/presence-control-group.service.ts +++ b/src/app/presence-control/services/presence-control-group.service.ts @@ -11,7 +11,6 @@ import { SubscriptionDetail } from "../../shared/models/subscription-detail.mode import { EventsRestService } from "../../shared/services/events-rest.service"; import { LoadingService } from "../../shared/services/loading-service"; import { SubscriptionDetailsRestService } from "../../shared/services/subscription-details-rest.service"; -import { SubscriptionsRestService } from "../../shared/services/subscriptions-rest.service"; import { spread } from "../../shared/utils/function"; import { GroupOptions } from "../components/presence-control-group-dialog/presence-control-group-dialog.component"; import { LessonEntry } from "../models/lesson-entry.model"; @@ -108,7 +107,6 @@ export class PresenceControlGroupService { constructor( private userSettings: UserSettingsService, private eventService: EventsRestService, - private subscriptionService: SubscriptionsRestService, private subscriptionDetailsService: SubscriptionDetailsRestService, private loadingService: LoadingService, @Inject(SETTINGS) private settings: Settings, diff --git a/src/app/shared/services/events-rest.service.ts b/src/app/shared/services/events-rest.service.ts index 5025ffd39..5c5df59fb 100644 --- a/src/app/shared/services/events-rest.service.ts +++ b/src/app/shared/services/events-rest.service.ts @@ -2,7 +2,6 @@ import { HttpClient } from "@angular/common/http"; import { Inject, Injectable } from "@angular/core"; import { Observable } from "rxjs"; import { switchMap } from "rxjs/operators"; -import { Course } from "src/app/shared/models/course.model"; import { SETTINGS, Settings } from "../../settings"; import { SubscriptionDetail } from "../models/subscription-detail.model"; import { decodeArray } from "../utils/decode"; @@ -23,12 +22,4 @@ export class EventsRestService extends RestService { .get(`${this.baseUrl}/${eventId}/SubscriptionDetails`) .pipe(switchMap(decodeArray(this.codec))); } - - getDesignation(course: Course): string { - const classes = course.Classes - ? course.Classes.map((c) => c.Number).join(", ") - : null; - - return classes ? course.Designation + ", " + classes : course.Designation; - } }