src/app/child-dev-project/notes/dashboard-widgets/notes-dashboard/notes-dashboard.component.ts

Description

Dashboard Widget displaying entities that do not have a recently added Note.

If you do not set "sinceDays" of "fromBeginningOfWeek" inputs by default notes since beginning of the current week are considered.

Example

Metadata

Relationships

Used by

No results matching.

Index

Properties
Methods
Inputs

Constructor

constructor()

Inputs

entity
Type : string
Default value : "Child"

Entity for which the recent notes should be counted.

fromBeginningOfWeek
Type : boolean
Default value : true

Whether an additional offset should be automatically added to include notes from the beginning of the week

mode
Type : "with-recent-notes" | "without-recent-notes"
sinceDays
Type : number
Default value : 0

number of days since last note that entities should be considered having a "recent" note.

Methods

Static getRequiredEntities
getRequiredEntities(config: NotesDashboardConfig)
Parameters :
Name Type Optional
config NotesDashboardConfig No
Returns : any

Properties

Readonly entityDefinition
Type : unknown
Default value : computed<EntityConstructor>(() => this.entities.get(this.entity()), )
entries
Type : unknown
Default value : signal<EntityWithRecentNoteInfo[] | undefined>(undefined)

Entities displayed in the template with additional "daysSinceLastNote" field

subtitle
Type : unknown
Default value : computed(() => { const entity = this.entityDefinition(); switch (this.mode()) { case "with-recent-notes": return $localize`:Subtitle|Subtitle informing the user that these are the records with recent reports:${entity.labelPlural} with recent report`; case "without-recent-notes": return $localize`:Subtitle|Subtitle informing the user that these are the records without recent reports:${entity.labelPlural} having no recent reports`; default: return ""; } })
tooltip
Type : unknown
Default value : computed((): string => { switch (this.mode()) { case "with-recent-notes": return $localize`:Tooltip|Spaces in front of the variables are added automatically:includes cases with a note${this.sinceBeginningOfTheWeek()}:sinceBeginningOfWeek:${this.withinTheLastNDays()}:withinTheLastDays:`; case "without-recent-notes": return $localize`:Tooltip|Spaces in front of the variables are added automatically:includes cases without a note${this.sinceBeginningOfTheWeek()}:sinceBeginningOfWeek:${this.withinTheLastNDays()}:withinTheLastDays:`; default: return ""; } })
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  signal,
  untracked,
} from "@angular/core";
import { ChildrenService } from "../../../children/children.service";
import moment from "moment";
import { MatTableModule } from "@angular/material/table";
import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator";
import { EntityRegistry } from "../../../../core/entity/database-entity.decorator";
import { EntityConstructor } from "../../../../core/entity/model/entity";
import { DecimalPipe } from "@angular/common";
import { EntityBlockComponent } from "../../../../core/basic-datatypes/entity/entity-block/entity-block.component";
import { Note } from "../../model/note";
import { DashboardListWidgetComponent } from "../../../../core/dashboard/dashboard-list-widget/dashboard-list-widget.component";

interface NotesDashboardConfig {
  entity?: string;
  sinceDays?: number;
  fromBeginningOfWeek?: boolean;
  mode?: "with-recent-notes" | "without-recent-notes";
}

/**
 * Dashboard Widget displaying entities that do not have a recently added Note.
 *
 * If you do not set "sinceDays" of "fromBeginningOfWeek" inputs
 * by default notes since beginning of the current week are considered.
 */
@DynamicComponent("NotesDashboard")
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-no-recent-notes-dashboard",
  templateUrl: "./notes-dashboard.component.html",
  styleUrls: ["./notes-dashboard.component.scss"],
  imports: [
    MatTableModule,
    EntityBlockComponent,
    DecimalPipe,
    DashboardListWidgetComponent,
  ],
})
export class NotesDashboardComponent {
  private childrenService = inject(ChildrenService);
  private entities = inject(EntityRegistry);

  static getRequiredEntities(config: NotesDashboardConfig) {
    return config?.entity || Note.ENTITY_TYPE;
  }

  /** Entity for which the recent notes should be counted. */
  entity = input("Child");
  readonly entityDefinition = computed<EntityConstructor>(() =>
    this.entities.get(this.entity()),
  );
  /**
   * number of days since last note that entities should be considered having a "recent" note.
   */
  sinceDays = input(0);

  /** Whether an additional offset should be automatically added to include notes from the beginning of the week */
  fromBeginningOfWeek = input(true);

  mode = input<"with-recent-notes" | "without-recent-notes">();

  /**
   * Entities displayed in the template with additional "daysSinceLastNote" field
   */
  entries = signal<EntityWithRecentNoteInfo[] | undefined>(undefined);

  subtitle = computed(() => {
    const entity = this.entityDefinition();
    switch (this.mode()) {
      case "with-recent-notes":
        return $localize`:Subtitle|Subtitle informing the user that these are the records with recent reports:${entity.labelPlural} with recent report`;
      case "without-recent-notes":
        return $localize`:Subtitle|Subtitle informing the user that these are the records without recent reports:${entity.labelPlural} having no recent reports`;
      default:
        return "";
    }
  });

  constructor() {
    effect((onCleanup) => {
      this.entityDefinition();
      this.sinceDays();
      this.fromBeginningOfWeek();
      this.mode();

      let isCurrent = true;
      this.entries.set(undefined);
      untracked(() => {
        void this.loadConcernedEntities(() => isCurrent);
      });

      onCleanup(() => {
        isCurrent = false;
      });
    });
  }

  private async loadConcernedEntities(isCurrent: () => boolean) {
    const mode = this.mode();
    if (!mode) {
      this.entries.set([]);
      return;
    }

    let dayRangeBoundary = this.sinceDays();
    if (this.fromBeginningOfWeek()) {
      dayRangeBoundary += moment().diff(moment().startOf("week"), "days");
    }

    const queryRange = Math.round((dayRangeBoundary * 3) / 10) * 10; // query longer range to be able to display exact date of last note for recent

    // recent notes are sorted ascending, without recent notes descending
    const order = mode === "with-recent-notes" ? -1 : 1;
    const filterFn =
      mode === "with-recent-notes"
        ? (stat: [string, number]) => stat[1] <= dayRangeBoundary
        : (stat: [string, number]) => stat[1] >= dayRangeBoundary;

    const recentNotesMap =
      await this.childrenService.getDaysSinceLastNoteOfEachEntity(
        this.entityDefinition().ENTITY_TYPE,
        queryRange,
      );
    const entries = Array.from(recentNotesMap)
      .filter(filterFn)
      .map((stat) => statsToEntityWithRecentNoteInfo(stat, queryRange))
      .sort((a, b) => order * (b.daysSinceLastNote - a.daysSinceLastNote));

    if (isCurrent()) {
      this.entries.set(entries);
    }
  }

  tooltip = computed((): string => {
    switch (this.mode()) {
      case "with-recent-notes":
        return $localize`:Tooltip|Spaces in front of the variables are added automatically:includes cases with a note${this.sinceBeginningOfTheWeek()}:sinceBeginningOfWeek:${this.withinTheLastNDays()}:withinTheLastDays:`;
      case "without-recent-notes":
        return $localize`:Tooltip|Spaces in front of the variables are added automatically:includes cases without a note${this.sinceBeginningOfTheWeek()}:sinceBeginningOfWeek:${this.withinTheLastNDays()}:withinTheLastDays:`;
      default:
        return "";
    }
  });

  private sinceBeginningOfTheWeek = computed(() => {
    if (this.fromBeginningOfWeek()) {
      return (
        " " +
        $localize`:Tooltip-part|'includes cases without a note since the beginning of the week':since the beginning of the week`
      );
    }
    return "";
  });

  private withinTheLastNDays = computed(() => {
    if (this.sinceDays() > 0) {
      return (
        " " +
        $localize`:Tooltip-part|'includes cases without a note within the last x days':without a note within the last ${this.sinceDays()} days`
      );
    }
    return "";
  });
}

/**
 * details on entity stats to be displayed
 */
interface EntityWithRecentNoteInfo {
  entityId: string;
  daysSinceLastNote: number;
  /** true when the daysSinceLastNote is not accurate but was cut off for performance optimization */
  moreThanDaysSince: boolean;
}

/**
 * Map a result entry from getDaysSinceLastNoteOfEachEntity to the EntityWithRecentNoteInfo interface
 * @param stat Array of [entityId, daysSinceLastNote]
 * @param queryRange The query range (the maximum of days that exactly calculated)
 */
function statsToEntityWithRecentNoteInfo(
  stat: [string, number],
  queryRange: number,
): EntityWithRecentNoteInfo {
  if (stat[1] < Number.POSITIVE_INFINITY) {
    return {
      entityId: stat[0],
      daysSinceLastNote: stat[1],
      moreThanDaysSince: false,
    };
  } else {
    return {
      entityId: stat[0],
      daysSinceLastNote: queryRange,
      moreThanDaysSince: true,
    };
  }
}
<app-dashboard-list-widget
  icon="file-alt"
  theme="note"
  [subtitle]="subtitle()"
  [explanation]="tooltip()"
  [entries]="entries()"
>
  <div class="table-wrapper">
    <table mat-table [attr.aria-label]="subtitle()">
      <!-- Table header only for assistive technologies like screen readers -->
      <tr class="visually-hidden">
        <th scope="col">{{ entityDefinition().label }}</th>
        <th scope="col" i18n="Column header for days since the last note">
          Days since last note
        </th>
      </tr>
      <ng-container matColumnDef="entity">
        <td *matCellDef="let entityNoteInfo">
          <app-entity-block
            [entityId]="entityNoteInfo.entityId"
          ></app-entity-block>
        </td>
      </ng-container>

      <ng-container matColumnDef="daysSinceLastNote">
        <td *matCellDef="let entityNoteInfo" class="text-align-end">
          @if (entityNoteInfo.moreThanDaysSince) {
            &gt;&nbsp;
          }
          <span i18n="Amount of days back"
            >{{ entityNoteInfo.daysSinceLastNote | number: "1.0-0" }} days</span
          >
        </td>
      </ng-container>

      <tr
        mat-row
        *matRowDef="let row; columns: ['entity', 'daysSinceLastNote']"
      ></tr>
    </table>

    @if (entries()?.length === 0) {
      <div>
        @if (mode() === "without-recent-notes") {
          <ng-container
            i18n="
              There are no participants that don't have a recent report to be
              shown here
            "
          >
            no records without recent report
          </ng-container>
        }
        @if (mode() === "with-recent-notes") {
          <ng-container
            i18n="
              There are no participants that have a recent report to be shown
              here
            "
          >
            no records with recent report
          </ng-container>
        }
      </div>
    }
  </div>
</app-dashboard-list-widget>

./notes-dashboard.component.scss

@use "../../../../core/dashboard/dashboard-widget-base";
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""