src/app/features/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts

Description

Set up or select a roll call event for a specific date (either one-time or related to a recurring activity).

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods
Inputs

Inputs

filterConfig
Type : FilterConfig[]
Default value : this.attendanceService.filterConfig()

Configuration for the filter UI shown above the events list.

Methods

createOneTimeEvent
createOneTimeEvent()
Returns : void
filterExistingEvents
filterExistingEvents(filter: DataFilter<Entity>)
Parameters :
Name Type Optional
filter DataFilter<Entity> No
Returns : void
selectEvent
selectEvent(event: EventWithAttendance)
Parameters :
Name Type Optional
event EventWithAttendance No
Returns : void
showLess
showLess()
Returns : void
showMore
showMore()
Returns : void

Properties

activeEvents
Type : unknown
Default value : computed<EventWithAttendance[]>(() => { const result = this.eventsResource.value(); const events = this.showingAll() ? result?.allEvents : result?.events; return events ?? []; })

The active base set: either user-filtered or all, depending on showingAll.

date
Type : unknown
Default value : signal(new Date())
dateField
Type : NgModel
Decorators :
@ViewChild('dateField')
entityList
Type : unknown
Default value : computed(() => this.activeEvents().map((e) => e.entity))

Raw entities from the active event set, used for filter schema look-ups and filter predicate evaluation.

entityType
Type : unknown
Default value : computed( () => this.activeEvents()[0]?.entity.constructor as EntityConstructor | undefined, )

The entity type inferred from the loaded events, used for filter schema look-ups.

Protected Readonly eventsResource
Type : unknown
Default value : resource< { events: EventWithAttendance[]; allEvents: EventWithAttendance[] }, Date >({ params: () => this.date(), loader: ({ params: date }) => this.attendanceService.getAvailableEventsForRollCall(date), })
Readonly FILTER_VISIBLE_THRESHOLD
Type : number
Default value : 4

filters are displayed in the UI only if at least this many events are listed.

This avoids displaying irrelevant filters for an empty or very short list.

filteredEvents
Type : unknown
Default value : linkedSignal<EventWithAttendance[]>(() => this.activeEvents(), )

The events currently shown, after applying any active filter. Resets to the full active set whenever the active set changes.

showingAll
Type : unknown
Default value : linkedSignal(() => { const result = this.eventsResource.value(); return result !== undefined ? result.events.length === 0 : false; })

Whether all events are shown (not just the user's own). Resets to the default (true when own === all) whenever new data loads.

import {
  ChangeDetectionStrategy,
  Component,
  ViewChild,
  computed,
  inject,
  input,
  linkedSignal,
  resource,
  signal,
} from "@angular/core";
import { AttendanceService } from "../../attendance.service";
import { AlertService } from "#src/app/core/alerts/alert.service";
import { AlertDisplay } from "#src/app/core/alerts/alert-display";
import { FormsModule, NgModel } from "@angular/forms";
import { FilterService } from "#src/app/core/filter/filter.service";
import { FilterConfig } from "#src/app/core/entity-list/EntityListConfig";
import { MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatDatepickerModule } from "@angular/material/datepicker";
import { Angulartics2OnModule } from "angulartics2";
import { FilterComponent } from "#src/app/core/filter/filter/filter.component";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { ActivityCardComponent } from "../activity-card/activity-card.component";
import { MatButtonModule } from "@angular/material/button";
import { DataFilter } from "#src/app/core/filter/filters/filters";
import { Router } from "@angular/router";
import { ViewTitleComponent } from "#src/app/core/common-components/view-title/view-title.component";
import { RouteTarget } from "#src/app/route-target";
import { ConfirmationDialogService } from "#src/app/core/common-components/confirmation-dialog/confirmation-dialog.service";
import { OkButton } from "#src/app/core/common-components/confirmation-dialog/confirmation-dialog/confirmation-dialog.component";
import { EventWithAttendance } from "../../model/event-with-attendance";
import { Entity, EntityConstructor } from "#src/app/core/entity/model/entity";

/**
 * Set up or select a roll call event for a specific date
 * (either one-time or related to a recurring activity).
 */
@RouteTarget("AddDayAttendance")
@Component({
  selector: "app-roll-call-setup",
  templateUrl: "./roll-call-setup.component.html",
  styleUrls: ["./roll-call-setup.component.scss"],
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    MatFormFieldModule,
    MatInputModule,
    FormsModule,
    MatDatepickerModule,
    Angulartics2OnModule,
    FilterComponent,
    MatProgressBarModule,
    ActivityCardComponent,
    MatButtonModule,
    ViewTitleComponent,
  ],
})
export class RollCallSetupComponent {
  private readonly attendanceService = inject(AttendanceService);
  private readonly router = inject(Router);
  private readonly alertService = inject(AlertService);
  private readonly filterService = inject(FilterService);
  private readonly confirmationService = inject(ConfirmationDialogService);

  /**
   * filters are displayed in the UI only if at least this many events are listed.
   *
   * This avoids displaying irrelevant filters for an empty or very short list.
   */
  readonly FILTER_VISIBLE_THRESHOLD = 4;

  /**
   * Configuration for the filter UI shown above the events list.
   */
  readonly filterConfig = input<FilterConfig[]>(
    this.attendanceService.filterConfig(),
  );

  date = signal(new Date());

  protected readonly eventsResource = resource<
    { events: EventWithAttendance[]; allEvents: EventWithAttendance[] },
    Date
  >({
    params: () => this.date(),
    loader: ({ params: date }) =>
      this.attendanceService.getAvailableEventsForRollCall(date),
  });

  /**
   * Whether all events are shown (not just the user's own).
   * Resets to the default (true when own === all) whenever new data loads.
   */
  showingAll = linkedSignal(() => {
    const result = this.eventsResource.value();
    return result !== undefined ? result.events.length === 0 : false;
  });

  /** The active base set: either user-filtered or all, depending on showingAll. */
  activeEvents = computed<EventWithAttendance[]>(() => {
    const result = this.eventsResource.value();
    const events = this.showingAll() ? result?.allEvents : result?.events;
    return events ?? [];
  });

  /**
   * The events currently shown, after applying any active filter.
   * Resets to the full active set whenever the active set changes.
   */
  filteredEvents = linkedSignal<EventWithAttendance[]>(() =>
    this.activeEvents(),
  );

  @ViewChild("dateField") dateField: NgModel;

  /** Raw entities from the active event set, used for filter schema look-ups and filter predicate evaluation. */
  entityList = computed(() => this.activeEvents().map((e) => e.entity));

  /**
   * The entity type inferred from the loaded events, used for filter schema look-ups.
   */
  entityType = computed(
    () =>
      this.activeEvents()[0]?.entity.constructor as
        EntityConstructor | undefined,
  );

  showMore() {
    this.showingAll.set(true);
  }

  showLess() {
    this.showingAll.set(false);
  }

  private formatDateForQuery(date: Date): string {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, "0");
    const day = String(date.getDate()).padStart(2, "0");

    return `${year}-${month}-${day}`;
  }

  createOneTimeEvent() {
    this.confirmationService.getConfirmation(
      $localize`Please create a Recurring Activity`,
      $localize`To record attendance, you need to first create a Recurring Activity (or an individual event record for the selected date).`,
      OkButton,
    );

    // TODO: enable routing to create a one-time event again after we implemented a generically configurable version of this.
    // this.router.navigate(["/attendance/add-day", "new"], {
    //   queryParams: {
    //     date: this.formatDateForQuery(this.date),
    //   },
    // });
  }

  filterExistingEvents(filter: DataFilter<Entity>) {
    const predicate = this.filterService.getFilterPredicate(filter);
    this.filteredEvents.set(
      this.activeEvents().filter((e) => predicate(e.entity)),
    );
  }

  selectEvent(event: EventWithAttendance) {
    if (!this.dateField?.valid) {
      this.alertService.addWarning(
        $localize`:Alert when selected date is invalid:Invalid Date`,
        AlertDisplay.TEMPORARY,
      );
      return;
    }

    const entity = event.entity;
    if (entity.isNew && event.activityId) {
      this.router.navigate(["/attendance/add-day", "new"], {
        queryParams: {
          activity: event.activityId,
          date: this.formatDateForQuery(event.date),
        },
      });
    } else {
      this.router.navigate(["/attendance/add-day", entity.getId()]);
    }
  }
}
<app-view-title
  i18n="
    Record Attendance|Title when recording the attendance at a particular stage
    (e.g. selecting the event, recording it)
  "
>
  Record Attendance
</app-view-title>

<div class="top-control">
  <mat-form-field>
    <mat-label
      i18n="
        Event-Record label|Record an event for a particular date that is to be
        inputted
      "
      >Date
    </mat-label>
    <input
      matInput
      #dateField="ngModel"
      [ngModel]="date()"
      (ngModelChange)="date.set($event)"
      required
      [matDatepicker]="datePicker"
    />
    <mat-datepicker-toggle
      matIconSuffix
      [for]="datePicker"
      angulartics2On="click"
      angularticsCategory="Record Attendance"
      angularticsAction="select_date"
    ></mat-datepicker-toggle>
    <mat-datepicker #datePicker></mat-datepicker>
  </mat-form-field>

  @if (activeEvents().length >= FILTER_VISIBLE_THRESHOLD) {
    <app-filter
      class="flex-row flex-wrap gap-small"
      [filterConfig]="filterConfig()"
      [entityType]="entityType()"
      [entities]="entityList()"
      [onlyShowRelevantFilterOptions]="true"
      (filterObjChange)="filterExistingEvents($event)"
    ></app-filter>
  }
</div>

@if (eventsResource.isLoading()) {
  <div class="process-spinner">
    <mat-progress-bar mode="indeterminate"></mat-progress-bar>
  </div>
} @else {
  <div class="cards-list">
    @for (event of filteredEvents(); track event.entity.getId()) {
      <app-activity-card
        class="pointer"
        [event]="event"
        (click)="selectEvent(event)"
      >
      </app-activity-card>
    }
  </div>
}

<div class="padding-top-regular gap-regular flex-row">
  <button
    mat-stroked-button
    (click)="showingAll() ? showLess() : showMore()"
    angulartics2On="click"
    angularticsCategory="Record Attendance"
    angularticsAction="show_more"
    class="padding-right-small"
  >
    @if (showingAll()) {
      <span i18n="Show less entries of a list"> Show less </span>
    } @else {
      <span i18n="Show more entries of a list"> Show more </span>
    }
  </button>
  <button
    i18n="Not listed|Allows to create a new event"
    mat-button
    (click)="createOneTimeEvent()"
    angulartics2On="click"
    angularticsCategory="Record Attendance"
    angularticsAction="create_onetime_event"
  >
    My event is not listed ...
  </button>
</div>

./roll-call-setup.component.scss

@use "mixins/grid-layout";
@use "variables/sizes";
@use "variables/colors";

.top-control {
  position: sticky;
  top: 0;
  /* make this visible while other contents go "under" this top control */
  z-index: 1;
  /* These small hacks remove the global margin and re-apply them as padding.
     This way, the margin stays consistent and scrolling works
     as intended (there is no overflow left or right)  */
  margin-left: -(sizes.$margin-main-view-left);
  padding-left: sizes.$margin-main-view-left;
  margin-right: -(sizes.$margin-main-view-right);
  padding-right: sizes.$margin-main-view-right;
  margin-top: -(sizes.$margin-main-view-top);
  padding-top: sizes.$margin-main-view-top;
  background-color: colors.$background;
}

.cards-list {
  @include grid-layout.adaptive(350px, 414px);
}

.process-spinner {
  display: flex;
  justify-content: center;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""