src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.component.ts
import {
Component,
EventEmitter,
OnInit,
Output,
ViewChild,
} from "@angular/core";
import { AttendanceService } from "../../attendance.service";
import { Note } from "../../../notes/model/note";
import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service";
import { RecurringActivity } from "../../model/recurring-activity";
import { FormDialogService } from "../../../../core/form-dialog/form-dialog.service";
import { AlertService } from "../../../../core/alerts/alert.service";
import { AlertDisplay } from "../../../../core/alerts/alert-display";
import { FormsModule, NgModel } from "@angular/forms";
import { FilterService } from "../../../../core/filter/filter.service";
import { FilterConfig } from "../../../../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 "../../../../core/filter/filter/filter.component";
import { NgForOf, NgIf } from "@angular/common";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { ActivityCardComponent } from "../../activity-card/activity-card.component";
import { MatButtonModule } from "@angular/material/button";
import { CurrentUserSubject } from "../../../../core/session/current-user-subject";
import { DataFilter } from "../../../../core/filter/filters/filters";
@Component({
selector: "app-roll-call-setup",
templateUrl: "./roll-call-setup.component.html",
styleUrls: ["./roll-call-setup.component.scss"],
imports: [
MatFormFieldModule,
MatInputModule,
FormsModule,
MatDatepickerModule,
Angulartics2OnModule,
FilterComponent,
NgIf,
MatProgressBarModule,
ActivityCardComponent,
NgForOf,
MatButtonModule,
],
standalone: true,
})
export class RollCallSetupComponent implements OnInit {
date = new Date();
existingEvents: NoteForActivitySetup[] = [];
filteredExistingEvents: NoteForActivitySetup[] = [];
@Output() eventSelected = new EventEmitter<Note>();
allActivities: RecurringActivity[] = [];
visibleActivities: RecurringActivity[] = [];
filterConfig: FilterConfig[] = [{ id: "category" }, { id: "schools" }];
entityType = Note;
showingAll = false;
@ViewChild("dateField") dateField: NgModel;
isLoading = true;
/**
* 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;
constructor(
private entityMapper: EntityMapperService,
private attendanceService: AttendanceService,
private currentUser: CurrentUserSubject,
private formDialog: FormDialogService,
private alertService: AlertService,
private filerService: FilterService,
) {}
async ngOnInit() {
await this.initAvailableEvents();
}
private async initAvailableEvents() {
this.isLoading = true;
this.existingEvents =
await this.attendanceService.getEventsWithUpdatedParticipants(this.date);
await this.loadActivities();
this.sortEvents();
this.filteredExistingEvents = this.existingEvents;
this.isLoading = false;
}
private async loadActivities() {
this.allActivities = await this.entityMapper
.loadType(RecurringActivity)
.then((res) => res.filter((a) => a.isActive));
if (this.showingAll) {
this.visibleActivities = this.allActivities;
} else {
// TODO implement a generic function that finds the property where a entity has relations to another entity type (e.g. `authors` for `Note` when looking for `User`) to allow dynamic checks
this.visibleActivities = this.allActivities.filter((a) =>
a.isAssignedTo(this.currentUser.value?.getId()),
);
if (this.visibleActivities.length === 0) {
this.visibleActivities = this.allActivities.filter(
(a) => a.assignedTo.length === 0,
);
}
if (this.visibleActivities.length === 0) {
this.visibleActivities = this.allActivities;
this.showingAll = true;
}
}
const newEvents = await Promise.all(
this.visibleActivities.map((activity) =>
this.createEventForActivity(activity),
),
);
this.existingEvents = this.existingEvents.concat(
...newEvents.filter((e) => !!e),
);
}
async showMore() {
this.showingAll = !this.showingAll;
await this.initAvailableEvents();
}
async showLess() {
this.showingAll = !this.showingAll;
await this.initAvailableEvents();
}
async setNewDate(date: Date) {
this.date = date;
await this.initAvailableEvents();
}
private async createEventForActivity(
activity: RecurringActivity,
): Promise<NoteForActivitySetup> {
if (this.existingEvents.find((e) => e.relatesTo === activity.getId())) {
return undefined;
}
const event = (await this.attendanceService.createEventForActivity(
activity,
this.date,
)) as NoteForActivitySetup;
if (this.currentUser.value) {
event.authors = [this.currentUser.value.getId()];
}
event.isNewFromActivity = true;
return event;
}
private sortEvents() {
const calculateEventPriority = (event: Note) => {
let score = 0;
const activityAssignedUsers = this.allActivities.find(
(a) => a.getId() === event.relatesTo,
)?.assignedTo;
// use parent activities' assigned users and only fall back to event if necessary
const assignedUsers = activityAssignedUsers ?? event.authors;
if (!RecurringActivity.isActivityEventNote(event)) {
// show one-time events first
score += 1;
}
if (assignedUsers.includes(this.currentUser.value?.getId())) {
score += 2;
}
return score;
};
this.existingEvents.sort(
(a, b) => calculateEventPriority(b) - calculateEventPriority(a),
);
}
createOneTimeEvent() {
const newNote = Note.create(new Date());
if (this.currentUser.value) {
newNote.authors = [this.currentUser.value.getId()];
}
this.formDialog
.openView(newNote, "NoteDetails")
.afterClosed()
.subscribe((createdNote: Note) => {
if (createdNote) {
this.existingEvents.push(createdNote);
}
});
}
filterExistingEvents(filter: DataFilter<Note>) {
const predicate = this.filerService.getFilterPredicate(filter);
this.filteredExistingEvents = this.existingEvents.filter(predicate);
}
selectEvent(event: NoteForActivitySetup) {
if (this.dateField.valid) {
this.eventSelected.emit(event);
} else {
this.alertService.addWarning(
$localize`:Alert when selected date is invalid:Invalid Date`,
AlertDisplay.TEMPORARY,
);
}
}
}
type NoteForActivitySetup = Note & { isNewFromActivity?: boolean };