src/app/features/dashboard-widgets/progress-dashboard-widget/progress-dashboard/progress-dashboard.component.ts

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods
Inputs

Constructor

constructor()

Inputs

dashboardConfigId
Type : string
Default value : ""
explanation
Type : string
Default value : $localize`:dashboard widget explanation: Shows the progress of different parts of project tasks. You can use this to track any kind of targets.`
subtitle
Type : string
Default value : $localize`:dashboard widget subtitle: Progress Overview`

Methods

getOverallProgressPercentage
getOverallProgressPercentage()
Returns : number
Static getRequiredEntities
getRequiredEntities()
Returns : any
Async save
save()
Returns : any
showEditComponent
showEditComponent()
Returns : void

Properties

data
Type : unknown
Default value : signal<ProgressDashboardConfig>(new ProgressDashboardConfig(""))
overallPercentage
Type : unknown
Default value : computed(() => this.getOverallProgressPercentage())
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  input,
  signal,
  untracked,
} from "@angular/core";
import { PercentPipe } from "@angular/common";
import { MatButtonModule } from "@angular/material/button";
import { MatDialog } from "@angular/material/dialog";
import { MatProgressBarModule } from "@angular/material/progress-bar";
import { MatTableModule } from "@angular/material/table";
import { FontAwesomeModule } from "@fortawesome/angular-fontawesome";
import { filter, firstValueFrom } from "rxjs";
import { DynamicComponent } from "../../../../core/config/dynamic-components/dynamic-component.decorator";
import { DashboardListWidgetComponent } from "../../../../core/dashboard/dashboard-list-widget/dashboard-list-widget.component";
import { EntityMapperService } from "../../../../core/entity/entity-mapper/entity-mapper.service";
import { Logging } from "../../../../core/logging/logging.service";
import { waitForChangeTo } from "../../../../core/session/session-states/session-utils";
import { SyncState } from "../../../../core/session/session-states/sync-state.enum";
import { SyncStateSubject } from "../../../../core/session/session-type";
import { EditProgressDashboardComponent } from "../edit-progress-dashboard/edit-progress-dashboard.component";
import { ProgressDashboardConfig } from "./progress-dashboard-config";

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-progress-dashboard",
  templateUrl: "./progress-dashboard.component.html",
  styleUrls: ["./progress-dashboard.component.scss"],
  imports: [
    PercentPipe,
    MatTableModule,
    MatProgressBarModule,
    MatButtonModule,
    FontAwesomeModule,
    DashboardListWidgetComponent,
  ],
})
@DynamicComponent("ProgressDashboard")
export class ProgressDashboardComponent {
  private entityMapper = inject(EntityMapperService);
  private dialog = inject(MatDialog);
  private syncState = inject(SyncStateSubject);

  static getRequiredEntities() {
    return ProgressDashboardConfig.ENTITY_TYPE;
  }

  dashboardConfigId = input("");
  data = signal<ProgressDashboardConfig>(new ProgressDashboardConfig(""));

  subtitle = input<string>(
    $localize`:dashboard widget subtitle: Progress Overview`,
  );
  explanation = input<string>(
    $localize`:dashboard widget explanation: Shows the progress of different parts of project tasks. You can use this to track any kind of targets.`,
  );

  overallPercentage = computed(() => this.getOverallProgressPercentage());

  constructor() {
    effect((onCleanup) => {
      const dashboardConfigId = this.dashboardConfigId();
      let isCurrent = true;

      this.data.set(new ProgressDashboardConfig(dashboardConfigId));
      untracked(() => {
        void this.loadConfig(dashboardConfigId, () => isCurrent);
      });

      const subscription = this.entityMapper
        .receiveUpdates(ProgressDashboardConfig)
        .pipe(
          filter((entity) => entity.entity.getId(true) === dashboardConfigId),
        )
        .subscribe((update) => this.updateConfig(update.entity));

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

  private async loadConfig(
    dashboardConfigId: string,
    isCurrent: () => boolean,
  ): Promise<void> {
    try {
      const config = await this.entityMapper.load(
        ProgressDashboardConfig,
        dashboardConfigId,
      );
      if (isCurrent()) {
        this.updateConfig(config);
      }
      return;
    } catch {
      // Retry after the next successful sync in case the config isn't available yet.
    }

    try {
      await firstValueFrom(
        this.syncState.pipe(waitForChangeTo(SyncState.COMPLETED)),
      );
      const config = await this.entityMapper.load(
        ProgressDashboardConfig,
        dashboardConfigId,
      );
      if (isCurrent()) {
        this.updateConfig(config);
      }
    } catch {
      if (isCurrent()) {
        this.createDefaultConfig();
      }
    }
  }

  private updateConfig(updatedConfig: ProgressDashboardConfig) {
    this.data.set(updatedConfig);
  }

  private createDefaultConfig() {
    const current = this.data();
    const next = this.cloneConfig(current);

    Logging.debug(
      `ProgressDashboardConfig (${this.dashboardConfigId()}) not found. Creating ...`,
    );
    next.title = $localize`:The progress, e.g. of a certain activity:Progress of X`;
    this.data.set(next);
    void this.save();
  }

  private cloneConfig(
    config: ProgressDashboardConfig,
  ): ProgressDashboardConfig {
    const clone = new ProgressDashboardConfig(config.getId(true));
    Object.assign(clone, config);
    return clone;
  }

  async save() {
    await this.entityMapper.save(this.data());
  }

  showEditComponent() {
    this.dialog
      .open(EditProgressDashboardComponent, {
        data: this.data(),
      })
      .afterClosed()
      .subscribe(async (next) => {
        if (next) {
          const updatedConfig = this.cloneConfig(this.data());
          Object.assign(updatedConfig, next);
          this.data.set(updatedConfig);
          await this.save();
        }
      });
  }

  // Calculates the weighted overall progress based on total current and target values.
  getOverallProgressPercentage(): number {
    const data = this.data();
    if (!data?.parts || data.parts.length === 0) {
      return 0;
    }

    let totalCurrent = 0;
    let totalTarget = 0;

    data.parts.forEach((entry) => {
      totalCurrent += entry.currentValue;
      totalTarget += entry.targetValue;
    });

    return totalTarget ? totalCurrent / totalTarget : 0;
  }
}
<div class="widget-container">
  <app-dashboard-list-widget
    icon="list"
    theme="child"
    [title]="overallPercentage() | percent: '1.0-0'"
    [subtitle]="subtitle() ?? data().title"
    [explanation]="explanation()"
    [entries]="data().parts"
  >
    <div class="table-wrapper">
      <table
        mat-table
        i18n-aria-label="Label for progress dashboard"
        aria-label="Table showing organization progress"
      >
        <ng-container matColumnDef="label">
          <td *matCellDef="let entry" class="width-1-3 padding-right-small">
            {{ entry.label }}
          </td>
        </ng-container>
        <ng-container matColumnDef="progress-bar">
          <td *matCellDef="let entry" class="full-width">
            <mat-progress-bar
              class="progress-bar"
              mode="determinate"
              [value]="(entry.currentValue / entry.targetValue) * 100"
            ></mat-progress-bar>
          </td>
        </ng-container>
        <ng-container matColumnDef="progress-value">
          <td *matCellDef="let entry" class="progress-cell">
            {{ entry.currentValue }} / {{ entry.targetValue }}
          </td>
        </ng-container>

        <!-- Table header only for assistive technologies like screen readers -->
        <tr class="visually-hidden">
          <th scope="col" i18n="The label of an entry">Label</th>
          <th scope="col" i18n="A visual aid to understand a progress of sorts">
            Progress Bar
          </th>
          <th scope="col" i18n="A progress in numbers">Progress Value</th>
        </tr>
        <tr
          mat-row
          *matRowDef="
            let row;
            columns: ['label', 'progress-bar', 'progress-value']
          "
        ></tr>
      </table>
    </div>
  </app-dashboard-list-widget>

  <button mat-icon-button (click)="showEditComponent()" class="config-panel">
    <fa-icon i18n-aria-label aria-label="configure" icon="wrench"></fa-icon>
  </button>
</div>

./progress-dashboard.component.scss

@use "../../../../core/dashboard/dashboard-widget-base";
@use "variables/sizes";

.progress-bar {
  height: sizes.$small;
}

.progress-cell {
  white-space: nowrap;
  padding-left: sizes.$small;
  text-align: end;
}

.config-panel {
  position: absolute;
  left: 0;
  bottom: 0;
  margin: 4px;
}
.widget-container {
  position: relative;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""