src/app/child-dev-project/attendance/model/activity-attendance.ts
Aggregate information about all events for a RecurringActivity within a given time period.
This object is not saved in the database but instead generated dynamically from stored Events to avoid problems keeping all information in sync in the database.
Properties |
|
Methods |
|
Accessors |
activity |
Type : RecurringActivity
|
The general, recurring activity for which this instance aggregates actual events that took place within a limited time period. |
individualStatusTypeCounts |
Default value : new Map<string, { [key: string]: number }>()
|
Mapping child ids to a map with all status type ids as object keys and their counts as values. |
periodFrom |
Type : Date
|
Starting date of the period this data refers to |
periodTo |
Type : Date
|
End date of the period this data refers to |
Static Readonly THRESHOLD_URGENT |
Type : number
|
Default value : 0.6
|
Static Readonly THRESHOLD_WARNING |
Type : number
|
Default value : 0.8
|
Static Optional _isCustomizedType |
todo: This property is no longer used and will be removed in future versions. |
Type : boolean
|
Inherited from
Entity
|
Defined in
Entity:77
|
True if this type's schema has been customized dynamically from the config. |
_rev |
Type : string
|
Decorators :
@DatabaseField({anonymize: 'retain'})
|
Inherited from
Entity
|
Defined in
Entity:213
|
internal database doc revision, used to detect conflicts by PouchDB/CouchDB |
anonymized |
Type : boolean
|
Decorators :
@DatabaseField({anonymize: 'retain'})
|
Inherited from
Entity
|
Defined in
Entity:232
|
Whether this entity has been anonymized and therefore cannot be re-activated. |
Static color |
Type : string
|
Inherited from
Entity
|
Defined in
Entity:140
|
color used for to highlight this entity type across the app |
created |
Type : UpdateMetadata
|
Decorators :
@DatabaseField({anonymize: 'retain'})
|
Inherited from
Entity
|
Defined in
Entity:218
|
Static DATABASE |
Type : string
|
Default value : "app"
|
Inherited from
Entity
|
Defined in
Entity:63
|
The database where these entities are stored. |
Static Optional enableUserAccounts |
Type : boolean
|
Inherited from
Entity
|
Defined in
Entity:108
|
Whether to enable user account creation for this entity type. When true, the UI will allow management of user accounts associated with this entity. |
Static ENTITY_TYPE |
Type : string
|
Default value : "Entity"
|
Inherited from
Entity
|
Defined in
Entity:58
|
The entity's type.
In classes extending Entity this is usually overridden by the class annotation |
Static hasPII |
Type : boolean
|
Default value : false
|
Inherited from
Entity
|
Defined in
Entity:203
|
whether this entity type can contain "personally identifiable information" (PII) and therefore should follow strict data protection requirements and offer a function to anonymize records. |
Static icon |
Type : IconName
|
Inherited from
Entity
|
Defined in
Entity:135
|
icon id used for this entity |
inactive |
Type : boolean
|
Decorators :
@DatabaseField({anonymize: 'retain'})
|
Inherited from
Entity
|
Defined in
Entity:226
|
Static Optional isInternalEntity |
Type : boolean
|
Inherited from
Entity
|
Defined in
Entity:102
|
if this entity type is an internal entity, i.e. only defined in the code base to store internal system data and not visible to the user for customization. |
Static label |
Type : string
|
Inherited from
Entity
|
Defined in
Entity:95
|
human-readable name/label of the entity in the UI |
Static schema |
Type : EntitySchema
|
Inherited from
Entity
|
Defined in
Entity:71
|
EntitySchema defining property transformations from/to the database.
This is auto-generated from the property annotations see /additional-documentation/how-to-guides/create-a-new-entity-type.html |
Static Optional toBlockDetailsAttributes |
Type : EntityBlockConfig
|
Inherited from
Entity
|
Defined in
Entity:90
|
Defining which attributes will be displayed in a tooltip on hover when the record is displayed as an entity-block. |
Static toStringAttributes |
Type : []
|
Default value : ["entityId"]
|
Inherited from
Entity
|
Defined in
Entity:85
|
Defining which attribute values of an entity should be shown in the The default is the ID of the entity ( |
updated |
Type : UpdateMetadata
|
Decorators :
@DatabaseField({anonymize: 'retain'})
|
Inherited from
Entity
|
Defined in
Entity:223
|
countEventsAbsent | ||||||
countEventsAbsent(childId: string)
|
||||||
Parameters :
Returns :
number
|
countEventsPresent | ||||||
countEventsPresent(childId: string)
|
||||||
Parameters :
Returns :
number
|
countEventsTotal |
countEventsTotal()
|
Returns :
number
|
countEventsWithUnknownStatus | ||||||||
countEventsWithUnknownStatus(forChildId?: string)
|
||||||||
The number of events that have at least one participant with an undefined status. This may occur when the user does not complete the full roll call or skips participants. The count of unknown status can indicate if manual checking and corrections are required.
Parameters :
Returns :
number
|
countTotalAbsent |
countTotalAbsent()
|
Returns :
any
|
countTotalPresent |
countTotalPresent()
|
Returns :
any
|
Static create | ||||||||||||
create(from: Date, events: EventNote[])
|
||||||||||||
Create an instance with the given initial properties.
Parameters :
Returns :
ActivityAttendance
|
getAttendancePercentage | ||||||
getAttendancePercentage(childId: string)
|
||||||
Parameters :
Returns :
number
|
getAttendancePercentageAverage |
getAttendancePercentageAverage()
|
Returns :
number
|
Public getColor | ||||||
getColor(forChildId?: string)
|
||||||
Inherited from
Entity
|
||||||
Defined in
Entity:226
|
||||||
Parameters :
Returns :
string
|
Public getWarningLevel | ||||||
getWarningLevel(forChildId?: string)
|
||||||
Inherited from
Entity
|
||||||
Defined in
Entity:207
|
||||||
Custom warning level for attendance thresholds - optionally for a specific child.
Parameters :
Returns :
WarningLevel
|
recalculateStats |
recalculateStats()
|
Returns :
void
|
assertValid |
assertValid()
|
Inherited from
Entity
|
Defined in
Entity:399
|
Checks if the entity is valid and if the check fails, throws an error explaining the failed check.
Returns :
void
|
Public copy | ||||||||||
copy(newId: string | boolean)
|
||||||||||
Inherited from
Entity
|
||||||||||
Defined in
Entity:382
|
||||||||||
Shallow copy of the entity. The resulting entity will be of the same type as this (taking into account subclassing)
Parameters :
|
Static createPrefixedId | ||||||||||||
createPrefixedId(type: string, id: string)
|
||||||||||||
Inherited from
Entity
|
||||||||||||
Defined in
Entity:188
|
||||||||||||
Create a prefixed id by adding the type prefix if it isn't already part of the given id.
Parameters :
Returns :
string
|
Static extractEntityIdFromId | ||||||||
extractEntityIdFromId(id: string)
|
||||||||
Inherited from
Entity
|
||||||||
Defined in
Entity:172
|
||||||||
Extract entityId without prefix.
Parameters :
Returns :
string
|
Static extractTypeFromId | ||||||||
extractTypeFromId(id: string)
|
||||||||
Inherited from
Entity
|
||||||||
Defined in
Entity:163
|
||||||||
Extract the ENTITY_TYPE from an id.
Parameters :
Returns :
string
|
getConstructor |
getConstructor()
|
Inherited from
Entity
|
Defined in
Entity:293
|
Get the class (Entity or the actual subclass of the instance) to call static methods on the correct class considering inheritance
Returns :
EntityConstructor<>
|
Public getId | ||||||
getId(withoutPrefix)
|
||||||
Inherited from
Entity
|
||||||
Defined in
Entity:312
|
||||||
Returns the id of this entity. Note that an id is final and can't be changed after the object has been instantiated, hence there is no
Parameters :
Returns :
string
the unique id of this entity |
getSchema |
getSchema()
|
Inherited from
Entity
|
Defined in
Entity:300
|
Get the entity schema of this class
Returns :
EntitySchema
|
Public getType |
getType()
|
Inherited from
Entity
|
Defined in
Entity:323
|
Returns the type which is used to categorize this entity in the database. Important: Do not overwrite this method! Types are handled internally.
Returns :
string
the entity's type (which is the class name). |
events | ||||||
getevents()
|
||||||
setevents(value: EventNote[])
|
||||||
Parameters :
Returns :
void
|
participants |
getparticipants()
|
List of (actual, recorded in at least one event) participants.
Returns :
string[]
|
import { AttendanceLogicalStatus } from "./attendance-status";
import { RecurringActivity } from "./recurring-activity";
import { defaultAttendanceStatusTypes } from "../../../core/config/default-config/default-attendance-status-types";
import { EventNote } from "./event-note";
import { getWarningLevelColor, WarningLevel } from "../../warning-level";
import { Entity } from "../../../core/entity/model/entity";
/**
* Aggregate information about all events for a {@link RecurringActivity} within a given time period.
*
* This object is not saved in the database but instead generated dynamically from stored Events
* to avoid problems keeping all information in sync in the database.
*/
export class ActivityAttendance extends Entity {
static readonly THRESHOLD_URGENT = 0.6;
static readonly THRESHOLD_WARNING = 0.8;
/**
* Create an instance with the given initial properties.
*/
static create(from: Date, events: EventNote[] = []) {
const instance = new ActivityAttendance();
instance.periodFrom = from;
instance.events = events;
return instance;
}
/**
* Starting date of the period this data refers to
*/
periodFrom: Date;
/**
* End date of the period this data refers to
*/
periodTo: Date;
/**
* Events within the period relating to the activity
*/
private _events: EventNote[] = [];
set events(value: EventNote[]) {
this._events = value;
this.recalculateStats();
}
get events(): EventNote[] {
return this._events;
}
/**
* The general, recurring activity for which this instance aggregates actual events that took place within a limited time period.
*/
activity: RecurringActivity;
/**
* List of (actual, recorded in at least one event) participants.
*/
get participants(): string[] {
return Array.from(new Set(this.events.flatMap((event) => event.children)));
}
/**
* Mapping child ids to a map with all *logical* status as object keys and their counts as values.
*/
individualLogicalStatusCounts = new Map<
string,
{ [key in AttendanceLogicalStatus]?: number }
>();
/**
* Mapping child ids to a map with all status type ids as object keys and their counts as values.
*/
individualStatusTypeCounts = new Map<string, { [key: string]: number }>();
countEventsTotal(): number {
return this.events.length;
}
countEventsPresent(childId: string): number {
return this.countIndividual(childId, AttendanceLogicalStatus.PRESENT);
}
countEventsAbsent(childId: string): number {
return this.countIndividual(childId, AttendanceLogicalStatus.ABSENT);
}
private countIndividual(
childId: string,
countingType: AttendanceLogicalStatus,
) {
return this.events.filter(
(eventNote) =>
eventNote.getAttendance(childId)?.status.countAs === countingType,
).length;
}
getAttendancePercentage(childId: string): number {
const present = this.countEventsPresent(childId);
const absent = this.countEventsAbsent(childId);
return present / (present + absent);
}
countTotalPresent() {
return this.countWithStatus(AttendanceLogicalStatus.PRESENT);
}
countTotalAbsent() {
return this.countWithStatus(AttendanceLogicalStatus.ABSENT);
}
private countWithStatus(matchingType: AttendanceLogicalStatus) {
return this.events.reduce(
(total, event) => total + event.countWithStatus(matchingType),
0,
);
}
getAttendancePercentageAverage(): number {
return this.countPercentage(AttendanceLogicalStatus.PRESENT, false);
}
private countPercentage(
matchingType: AttendanceLogicalStatus,
rounded: boolean = false,
) {
const calculatedStats = this.events
.map((event) => {
const eventStats = {
matching: 0,
total: event.children.length,
};
for (const childId of event.children) {
const att = event.getAttendance(childId).status;
if (att.countAs === matchingType) {
eventStats.matching++;
} else if (att.countAs === AttendanceLogicalStatus.IGNORE) {
eventStats.total--;
}
}
return eventStats;
})
.reduce(
(accumulatedStats, currentEventStats) => {
accumulatedStats.total += currentEventStats.total;
accumulatedStats.matching += currentEventStats.matching;
return accumulatedStats;
},
{ total: 0, matching: 0 },
);
const result = calculatedStats.matching / calculatedStats.total;
if (rounded) {
return Math.round(result * 10) / 10;
} else {
return result;
}
}
/**
* The number of events that have at least one participant with an undefined status.
* This may occur when the user does not complete the full roll call or skips participants.
* The count of unknown status can indicate if manual checking and corrections are required.
*
* @param forChildId filter the calculation to only include status of the given participant id
*/
countEventsWithUnknownStatus(forChildId?: string): number {
return this.events
.filter((e) => !forChildId || e.children.includes(forChildId))
.reduce(
(count: number, e: EventNote) =>
e.hasUnknownAttendances(forChildId) ? count + 1 : count,
0,
);
}
recalculateStats() {
this.individualStatusTypeCounts = new Map();
this.individualLogicalStatusCounts = new Map();
for (const event of this.events) {
for (const participant of event.children) {
let logicalCount = this.individualLogicalStatusCounts.get(participant);
if (!logicalCount) {
logicalCount = {};
this.individualLogicalStatusCounts.set(participant, logicalCount);
}
let typeCount = this.individualStatusTypeCounts.get(participant);
if (!typeCount) {
typeCount = {};
this.individualStatusTypeCounts.set(participant, typeCount);
}
const att = event.getAttendance(participant);
logicalCount[att.status.countAs] =
(logicalCount[att.status.countAs] ?? 0) + 1;
typeCount[att.status.id] = (typeCount[att.status.id] ?? 0) + 1;
}
}
}
/**
* Custom warning level for attendance thresholds - optionally for a specific child.
*/
public override getWarningLevel(forChildId?: string): WarningLevel {
let attendancePercentage;
if (forChildId) {
attendancePercentage = this.getAttendancePercentage(forChildId);
} else {
attendancePercentage = this.getAttendancePercentageAverage();
}
if (!attendancePercentage) {
return WarningLevel.NONE;
} else if (attendancePercentage < ActivityAttendance.THRESHOLD_URGENT) {
return WarningLevel.URGENT;
} else if (attendancePercentage < ActivityAttendance.THRESHOLD_WARNING) {
return WarningLevel.WARNING;
} else {
return WarningLevel.OK;
}
}
public override getColor(forChildId?: string): string {
return getWarningLevelColor(this.getWarningLevel(forChildId));
}
}
/**
* Generate a event with children for the given AttendanceStatus array.
*
* This is particularly useful to generate simple data for demo or test purposes.
*
* @param participating Object where keys are string childId and values are its attendance status
* @param date (Optional) date of the event; if not given today's date is used
* @param activity (Optional) reference to the connected activity entity
*/
export function generateEventWithAttendance(
participating: (
| [string, AttendanceLogicalStatus]
| [string, AttendanceLogicalStatus, string]
)[],
date = new Date(),
activity?: RecurringActivity,
): EventNote {
const event = EventNote.create(date);
for (const att of participating) {
event.addChild(att[0]);
event.getAttendance(att[0]).status = defaultAttendanceStatusTypes.find(
(t) => t.countAs === att[1],
);
if (att.length === 3) {
event.getAttendance(att[0]).remarks = att[2];
}
}
event.relatesTo = activity?.getId();
return event;
}