src/app/features/dashboard-widgets/birthday-dashboard-widget/birthday-dashboard/birthday-dashboard.component.ts
import {
  ChangeDetectionStrategy,
  Component,
  effect,
  inject,
  input,
  signal,
} from "@angular/core";
import { debounceTime, merge } from "rxjs";
import { DynamicComponent } from "#src/app/core/config/dynamic-components/dynamic-component.decorator";
import { MatTableModule } from "@angular/material/table";
import { EntityMapperService } from "#src/app/core/entity/entity-mapper/entity-mapper.service";
import { DatePipe } from "@angular/common";
import { EntityBlockComponent } from "#src/app/core/basic-datatypes/entity/entity-block/entity-block.component";
import { DashboardListWidgetComponent } from "#src/app/core/dashboard/dashboard-list-widget/dashboard-list-widget.component";
import {
  BirthdayDashboardIndexService,
  EntityPropertyMap,
  EntityWithBirthday,
} from "./birthday-dashboard-index.service";
import { Logging } from "#src/app/core/logging/logging.service";

interface BirthdayDashboardConfig {
  entities: EntityPropertyMap;
  threshold: number;
}

@DynamicComponent("BirthdayDashboard")
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-birthday-dashboard",
  templateUrl: "./birthday-dashboard.component.html",
  styleUrls: ["./birthday-dashboard.component.scss"],
  imports: [
    MatTableModule,
    EntityBlockComponent,
    DatePipe,
    DashboardListWidgetComponent,
  ],
})
export class BirthdayDashboardComponent {
  private readonly birthdayIndex = inject(BirthdayDashboardIndexService);
  private readonly entityMapper = inject(EntityMapperService);
  entries = signal<EntityWithBirthday[] | undefined>(undefined);

  static getRequiredEntities(config: BirthdayDashboardConfig) {
    return config?.entities ? Object.keys(config.entities) : "Child";
  }

  /**
   * An object holding the names of entities and properties where they have a `DateOfBirth` attribute.
   * E.g. (which is also the default)
   * ```json
   * "entities": { "Child": "dateOfBirth" }
   * ```
   */
  entities = input<EntityPropertyMap>({ ["Child"]: "dateOfBirth" });

  /**
   * Birthdays that are less or equal than "threshold" days away are shown.
   * Default 31
   */
  threshold = input(31);

  subtitle = input<string>(
    $localize`:dashboard widget subtitle:Upcoming Birthdays`,
  );
  explanation = input<string>();

  constructor() {
    effect((onCleanup) => {
      const entityConfig = this.entities();
      const threshold = this.threshold();
      let isCurrent = true;

      const indexBuilt = this.birthdayIndex.buildBirthdayIndex(entityConfig);

      const reload = () =>
        indexBuilt
          .then(() =>
            this.birthdayIndex.queryBirthdayIndex(entityConfig, threshold),
          )
          .catch((err) => {
            Logging.error("Failed to load upcoming birthdays", err);
            return [];
          })
          .then((res) => {
            if (isCurrent) {
              this.entries.set(res);
            }
          });

      // initial load - covers the case where matching entities already exist on mount.
      reload();

      // Re-query (cheap/incremental) whenever a relevant entity type changes, so the
      // widget picks up entities added/edited after the initial query (e.g. while the
      // index was still empty during sync/demo-data generation), and stays live for
      // ongoing changes. Debounced because e.g. demo-data generation saves many
      // entities in a burst - without this, that's one redundant query per save.
      const subscription = merge(
        ...Object.keys(entityConfig).map((type) =>
          this.entityMapper.receiveUpdates(type),
        ),
      )
        .pipe(debounceTime(500))
        .subscribe(() => reload());

      onCleanup(() => {
        isCurrent = false;
        subscription.unsubscribe();
      });
    });
  }
}

results matching ""

    No results matching ""