src/app/features/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
Set up or select a roll call event for a specific date (either one-time or related to a recurring activity).
| changeDetection | ChangeDetectionStrategy.OnPush |
| selector | app-roll-call-setup |
| standalone | true |
| imports |
MatFormFieldModule
MatInputModule
MatDatepickerModule
Angulartics2OnModule
MatProgressBarModule
MatButtonModule
|
| styleUrls | ./roll-call-setup.component.scss |
| templateUrl | ./roll-call-setup.component.html |
No results matching.
MatFormFieldModule
MatInputModule
FormsModule
MatDatepickerModule
Angulartics2OnModule
FilterComponent
MatProgressBarModule
ActivityCardComponent
MatButtonModule
ViewTitleComponent
Properties |
|
Methods |
Inputs |
| filterConfig | |
Type : FilterConfig[]
|
|
Default value : this.attendanceService.filterConfig()
|
|
|
Configuration for the filter UI shown above the events list. |
|
| createOneTimeEvent |
createOneTimeEvent()
|
|
Returns :
void
|
| filterExistingEvents | ||||||
filterExistingEvents(filter: DataFilter<Entity>)
|
||||||
|
Parameters :
Returns :
void
|
| selectEvent | ||||||
selectEvent(event: EventWithAttendance)
|
||||||
|
Parameters :
Returns :
void
|
| showLess |
showLess()
|
|
Returns :
void
|
| showMore |
showMore()
|
|
Returns :
void
|
| date |
Type : unknown
|
Default value : signal(new Date())
|
| dateField |
Type : NgModel
|
Decorators :
@ViewChild('dateField')
|
| 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. |
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;
}