src/app/core/admin/admin-overview/admin-overview.component.ts

Description

Admin GUI giving administrative users different options/actions.

Example

Metadata

Relationships

Used by

No results matching.

Depends on

Index

Properties
Methods

Constructor

constructor()

Methods

debugDatabase
debugDatabase()

Send a reference of the PouchDB to the browser's developer console for real-time debugging.

Returns : void
Async downloadConfigClick
downloadConfigClick()
Returns : any
editConfig
editConfig()
Returns : void
Async emptyRecords
emptyRecords()
Returns : any
isExpanded
isExpanded(sectionId: string)
Parameters :
Name Type Optional
sectionId string No
Returns : boolean
Async loadBackup
loadBackup(inputEvent: Event)

Reset the database to the state from the loaded backup file.

Parameters :
Name Type Optional Description
inputEvent Event No

for the input where a file has been selected

Returns : any
Async resetLocalDevice
resetLocalDevice()
Returns : any
Async resetSystem
resetSystem()
Returns : any
Async saveBackup
saveBackup()

Download a full backup of the database as (json) file.

Returns : any
Async saveCsvExport
saveCsvExport()

Download a full export of the database as csv file.

Returns : any
setExpanded
setExpanded(sectionId: string)
Parameters :
Name Type Optional
sectionId string No
Returns : void
Async uploadConfigFile
uploadConfigFile(inputEvent: Event)
Parameters :
Name Type Optional
inputEvent Event No
Returns : any

Properties

Protected adminOverviewService
Type : unknown
Default value : inject(AdminOverviewService)
Public configurationMenuItems
Type : MenuItem[]
Default value : []
expandedSection
Type : unknown
Default value : computed(() => this.sectionStateService.getExpanded())
isSaasEnvironment
Type : boolean
isUploadingConfig
Type : unknown
Default value : signal(false)
Public templates
Type : MenuItem[]
Default value : []
import {
  Component,
  inject,
  computed,
  signal,
  ChangeDetectionStrategy,
} from "@angular/core";
import { AdminSectionStateService } from "./admin-section-state.service";
import { BackupService } from "../backup/backup.service";
import { SystemResetService } from "../system-reset/system-reset.service";
import { LocalDeviceResetService } from "../../database/local-device-reset.service";
import { ConfirmationDialogService } from "../../common-components/confirmation-dialog/confirmation-dialog.service";
import { MatSnackBar } from "@angular/material/snack-bar";
import { ConfigService } from "../../config/config.service";
import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy";
import { readFile } from "../../../utils/utils";
import { DatabaseResolverService } from "../../database/database-resolver.service";
import { MatButtonModule } from "@angular/material/button";
import { RouterLink } from "@angular/router";
import { DownloadService } from "../../export/download-service/download.service";
import { MatListModule } from "@angular/material/list";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatIconModule } from "@angular/material/icon";
import { MatTooltipModule } from "@angular/material/tooltip";
import { RouteTarget } from "../../../route-target";
import { MenuItem } from "../../ui/navigation/menu-item";
import { JsonEditorService } from "#src/app/core/admin/json-editor/json-editor.service";
import { EntityMapperService } from "#src/app/core/entity/entity-mapper/entity-mapper.service";
import { Config } from "#src/app/core/config/config";
import { environment } from "#src/environments/environment";
import moment from "moment";
import { AdminOverviewService } from "./admin-overview.service";
import { WarningNotOptimizedForSmallScreenComponent } from "#src/app/core/common-components/warning-not-optimized-for-small-screen/warning-not-optimized-for-small-screen.component";
import { Logging } from "#src/app/core/logging/logging.service";

/**
 * Admin GUI giving administrative users different options/actions.
 */
@UntilDestroy()
@RouteTarget("Admin")
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: "app-admin-overview",
  templateUrl: "./admin-overview.component.html",
  styleUrls: ["./admin-overview.component.scss"],
  imports: [
    MatButtonModule,
    RouterLink,
    MatListModule,
    MatExpansionModule,
    MatIconModule,
    MatTooltipModule,
    WarningNotOptimizedForSmallScreenComponent,
  ],
})
export class AdminOverviewComponent {
  private backupService = inject(BackupService);
  private readonly systemResetService = inject(SystemResetService);
  private readonly localDeviceResetService = inject(LocalDeviceResetService);
  private downloadService = inject(DownloadService);
  private dbResolver = inject(DatabaseResolverService);
  private confirmationDialog = inject(ConfirmationDialogService);
  private snackBar = inject(MatSnackBar);
  private configService = inject(ConfigService);
  protected adminOverviewService = inject(AdminOverviewService);
  private jsonEditorService = inject(JsonEditorService);
  private entityMapper = inject(EntityMapperService);
  private readonly sectionStateService = inject(AdminSectionStateService);

  public templates: MenuItem[] = [];
  public configurationMenuItems: MenuItem[] = [];
  expandedSection = computed(() => this.sectionStateService.getExpanded());
  isUploadingConfig = signal(false);

  isSaasEnvironment: boolean;

  constructor() {
    this.templates = this.adminOverviewService.templates;
    this.configurationMenuItems =
      this.adminOverviewService.configurationMenuItems;
    this.isSaasEnvironment = environment.SaaS === true;
  }

  setExpanded(sectionId: string) {
    this.sectionStateService.setExpanded(sectionId);
  }

  isExpanded(sectionId: string): boolean {
    return this.expandedSection() === sectionId;
  }
  /**
   * Send a reference of the PouchDB to the browser's developer console for real-time debugging.
   */
  debugDatabase() {
    console.log(
      'You can assign the following to a global variable in the browser console (right click > "Store as global variable") to run actions on the database with your scripts here:',
    );
    console.log("DatabaseResolverService", this.dbResolver);
    console.log('"app" database', this.dbResolver.getDatabase());
  }

  /**
   * Download a full backup of the database as (json) file.
   */
  async saveBackup() {
    const backup = await this.backupService.getDatabaseExport();
    await this.downloadService.triggerDownload(backup, "json", "backup");
  }

  /**
   * Download a full export of the database as csv file.
   */
  async saveCsvExport() {
    const backup = await this.backupService.getDatabaseExport();
    await this.downloadService.triggerDownload(backup, "csv", "export");
  }

  async downloadConfigClick() {
    const configString = this.configService.exportConfig();
    await this.downloadService.triggerDownload(configString, "json", "config");
  }

  async uploadConfigFile(inputEvent: Event) {
    this.isUploadingConfig.set(true);
    try {
      const loadedFile = await readFile(this.getFileFromInputEvent(inputEvent));
      const parsed = JSON.parse(loadedFile);
      if (Array.isArray(parsed)) {
        const entities = parsed
          .map((doc) => {
            try {
              return this.entityMapper.entityFromRawDoc(doc);
            } catch (e) {
              Logging.warn(
                "Skipping unknown entity type during config upload",
                doc["_id"],
                e,
              );
              return null;
            }
          })
          .filter(Boolean);
        await this.entityMapper.saveAll(entities, true);
      } else {
        await this.configService.saveConfig(parsed);
      }
      this.snackBar.open($localize`Configuration updated`, undefined, {
        duration: 3000,
      });
    } catch (e) {
      Logging.error("Failed to upload configuration", e);
      this.snackBar.open($localize`Upload failed: ${e.message}`, undefined, {
        duration: 5000,
      });
    } finally {
      this.isUploadingConfig.set(false);
    }
  }

  editConfig() {
    const originalData = this.configService.exportConfig(true);
    this.jsonEditorService
      .openJsonEditorDialog(originalData)
      .subscribe(async (updatedData) => {
        if (!updatedData) return;

        const previousConfigBackup = new Config(
          Config.CONFIG_KEY + ":" + moment().format("YYYY-MM-DD_HH-mm-ss"),
          originalData,
        );
        try {
          await this.entityMapper.save(previousConfigBackup);
          await this.configService.saveConfig(updatedData);

          this.showConfirmationWithUndoOption(async () => {
            await this.configService.saveConfig(originalData);
            await this.entityMapper.remove(previousConfigBackup);
          });
        } catch (e) {
          Logging.error("Failed to save configuration changes", e);
          this.snackBar.open(
            $localize`Update failed: ${e.message}`,
            undefined,
            {
              duration: 5000,
            },
          );
        }
      });
  }

  /**
   * Show a snack bar with undo option and handle undo callback with progress dialog.
   */
  private showConfirmationWithUndoOption(undoAction: () => Promise<void>) {
    const snackBarRef = this.snackBar.open(
      $localize`Configuration updated`,
      $localize`Undo`,
      { duration: 8000 },
    );

    snackBarRef.onAction().subscribe(async () => {
      const progressRef = this.confirmationDialog.showProgressDialog(
        signal($localize`Reverting configuration changes ...`),
      );
      try {
        await undoAction();
      } catch (e) {
        Logging.error("Failed to revert configuration changes", e);
        this.snackBar.open($localize`Revert failed: ${e.message}`, undefined, {
          duration: 5000,
        });
      } finally {
        progressRef.close();
      }
    });
  }

  /**
   * Reset the database to the state from the loaded backup file.
   * @param inputEvent for the input where a file has been selected
   */
  async loadBackup(inputEvent: Event) {
    const restorePoint = await this.backupService.getDatabaseExport();
    const dataToBeRestored = JSON.parse(
      await readFile(this.getFileFromInputEvent(inputEvent)),
    );

    const confirmed = await this.confirmationDialog.getConfirmation(
      `Overwrite complete database?`,
      `Are you sure you want to restore this backup? This will
      delete all ${restorePoint.length} existing records,
      restoring ${dataToBeRestored.length} records from the loaded file.`,
    );

    if (!confirmed) {
      return;
    }

    await this.backupService.clearDatabase();
    await this.backupService.restoreData(dataToBeRestored, true);

    const snackBarRef = this.snackBar.open(`Backup restored`, "Undo", {
      duration: 8000,
    });
    snackBarRef
      .onAction()
      .pipe(untilDestroyed(this))
      .subscribe(async () => {
        await this.backupService.clearDatabase();
        await this.backupService.restoreData(restorePoint, true);
      });
  }

  private getFileFromInputEvent(inputEvent: Event): Blob {
    const target = inputEvent.target as HTMLInputElement;
    return target.files[0];
  }

  async emptyRecords() {
    await this.systemResetService.emptyRecords();
  }

  async resetSystem() {
    await this.systemResetService.resetSystem();
  }

  async resetLocalDevice() {
    await this.localDeviceResetService.resetLocalDevice();
  }
}
<h1 i18n>System Settings</h1>

<app-warning-not-optimized-for-small-screen />

<mat-accordion class="admin-sections margin-top-large" multi>
  <!-- Subscription Section -->
  @if (isSaasEnvironment) {
    <mat-expansion-panel
      [expanded]="isExpanded('subscription')"
      (opened)="setExpanded('subscription')"
    >
      <mat-expansion-panel-header>
        <mat-panel-title i18n>Subscription</mat-panel-title>
        <mat-panel-description i18n
          >Manage your subscription details, privacy settings, and access to
          advanced features.</mat-panel-description
        >
      </mat-expansion-panel-header>
      <mat-nav-list class="section-nav-list">
        <mat-list-item [routerLink]="['/admin/subscription-info']">
          <span matListItemTitle i18n>Subscription Info</span>
        </mat-list-item>
        <mat-list-item [routerLink]="['/admin/advanced-features']">
          <span matListItemTitle i18n>Advanced Features</span>
        </mat-list-item>
        <mat-list-item [routerLink]="['/admin/data-privacy']">
          <span matListItemTitle i18n>Data Privacy</span>
        </mat-list-item>
      </mat-nav-list>
    </mat-expansion-panel>
  }

  <!-- Configuration and Site Wide Settings Section -->
  <mat-expansion-panel
    [expanded]="isExpanded('config')"
    (opened)="setExpanded('config')"
  >
    <mat-expansion-panel-header>
      <mat-panel-title i18n
        >Configuration and Site Wide Settings</mat-panel-title
      >
      <mat-panel-description i18n
        >Set up the essential elements that shape your site's structure and
        system behaviour.</mat-panel-description
      >
    </mat-expansion-panel-header>
    <mat-nav-list class="section-nav-list">
      @for (item of configurationMenuItems; track item.label) {
        @if (item.link) {
          <mat-list-item
            [routerLink]="[item.link]"
            [matTooltip]="item.subtitle ?? ''"
            [matTooltipDisabled]="!item.subtitle"
            matTooltipPosition="before"
          >
            <span matListItemTitle>{{ item.label }}</span>
          </mat-list-item>
        }
      }
    </mat-nav-list>
  </mat-expansion-panel>

  <!-- Templates and Forms Section (items dynamic) -->
  <mat-expansion-panel
    [expanded]="isExpanded('templates')"
    (opened)="setExpanded('templates')"
  >
    <mat-expansion-panel-header>
      <mat-panel-title i18n>Templates and Forms</mat-panel-title>
      <mat-panel-description i18n
        >Customise your templates and forms.</mat-panel-description
      >
    </mat-expansion-panel-header>
    @if (templates.length > 0) {
      <mat-nav-list class="section-nav-list">
        @for (item of templates; track item.label) {
          @if (item.link) {
            <mat-list-item
              [routerLink]="[item.link]"
              [matTooltip]="item.subtitle ?? ''"
              [matTooltipDisabled]="!item.subtitle"
              matTooltipPosition="before"
            >
              <span matListItemTitle>{{ item.label }}</span>
            </mat-list-item>
          }
        }
      </mat-nav-list>
    }
  </mat-expansion-panel>

  <!-- User Management Section -->
  <mat-expansion-panel
    [expanded]="isExpanded('user-management')"
    (opened)="setExpanded('user-management')"
  >
    <mat-expansion-panel-header>
      <mat-panel-title i18n>User Management</mat-panel-title>
      <mat-panel-description i18n
        >Set up and manage user accounts, roles, and
        permissions.</mat-panel-description
      >
    </mat-expansion-panel-header>
    <mat-nav-list class="section-nav-list">
      <mat-list-item
        [routerLink]="['/admin/user-list']"
        matTooltip="Create, edit and manage user accounts."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>User Accounts</span>
      </mat-list-item>
      <mat-list-item
        [routerLink]="['/admin/user-roles']"
        matTooltip="Set roles and manage role-based access."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>User Roles & Permissions</span>
      </mat-list-item>
    </mat-nav-list>
  </mat-expansion-panel>

  <!-- Export and Backups Section (special content) -->
  <mat-expansion-panel
    [expanded]="isExpanded('export')"
    (opened)="setExpanded('export')"
  >
    <mat-expansion-panel-header>
      <mat-panel-title i18n>Export and Backups</mat-panel-title>
      <mat-panel-description i18n
        >Define how your system keeps data protected and
        accessible.</mat-panel-description
      >
    </mat-expansion-panel-header>
    <mat-nav-list class="section-nav-list">
      <mat-list-item
        (click)="saveBackup()"
        button
        matTooltip="Download all data of the database."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>Download Backup (.json)</span>
      </mat-list-item>
      <mat-list-item
        (click)="backupImport.click()"
        button
        matTooltip="Upload a previous backup to restore the database to that state."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>Restore Backup (.json)</span>
      </mat-list-item>
      <mat-list-item
        (click)="saveCsvExport()"
        button
        matTooltip="Download all data of the database in a format that can be opened as a spreadsheet."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>Full Database Export (.csv)</span>
      </mat-list-item>
    </mat-nav-list>
    <input #backupImport type="file" hidden (change)="loadBackup($event)" />
  </mat-expansion-panel>

  <!-- Technical Administration Section (special content) -->
  <mat-expansion-panel
    [expanded]="isExpanded('technical')"
    (opened)="setExpanded('technical')"
  >
    <mat-expansion-panel-header>
      <mat-panel-title i18n>Technical Administration</mat-panel-title>
      <mat-panel-description i18n
        >Manage advanced technical options for maintaining and troubleshooting
        your system.</mat-panel-description
      >
    </mat-expansion-panel-header>
    <mat-nav-list class="section-nav-list">
      <mat-list-item
        [routerLink]="['/admin/config-cleanup']"
        matTooltip="Analyze and clean up active configuration issues, like unused dropdown options."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>Configuration Cleanup</span>
      </mat-list-item>
      <mat-list-item
        (click)="editConfig()"
        button
        matTooltip="Open the raw configuration file to make advanced technical adjustments."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>Edit Application Configuration (JSON)</span>
      </mat-list-item>
      <mat-list-item
        (click)="downloadConfigClick()"
        button
        matTooltip="Download the raw configuration file."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n
          >Download Application Configuration (JSON)</span
        >
      </mat-list-item>
      <mat-list-item
        (click)="!isUploadingConfig() && configImport.click()"
        button
        [class.disabled]="isUploadingConfig()"
        matTooltip="Upload a raw configuration file and overwrite the current system configuration. Accepts a single config object or an array of config documents."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n
          >Upload Application Configuration (JSON)</span
        >
      </mat-list-item>
      <mat-list-item
        [routerLink]="['/admin/ai-agent']"
        matTooltip="Download all configuration documents as a single JSON file to use as context for an AI agent session."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>Configure using AI Agents</span>
      </mat-list-item>
      <mat-list-item
        (click)="debugDatabase()"
        button
        matTooltip="Make the database service available in the browser's developer console for advanced administrative tasks."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>PouchDB Debug: Send to console.log()</span>
      </mat-list-item>
      <mat-list-item
        (click)="emptyRecords()"
        button
        matTooltip="Delete all data while keeping the system configuration and the profiles of users with an account."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle class="color-error" i18n>Empty Records</span>
      </mat-list-item>
      <mat-list-item
        (click)="resetSystem()"
        button
        matTooltip="Delete everything, including all records and the complete system configuration, keeping only your own user profile, to set up this system from scratch."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle class="color-error" i18n>Reset System</span>
      </mat-list-item>
      <mat-list-item
        (click)="resetLocalDevice()"
        button
        matTooltip="Clear all data cached on this device and re-synchronize from the server. Does not affect other users or delete anything on the server."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>Reset Local Device</span>
      </mat-list-item>
      <mat-list-item
        [routerLink]="['/admin/conflicts']"
        matTooltip="View any database records that could not be synced because multiple users edited the record at the same time while offline."
        i18n-matTooltip
        matTooltipPosition="before"
      >
        <span matListItemTitle i18n>Database Conflicts</span>
      </mat-list-item>
    </mat-nav-list>
    <input
      #configImport
      type="file"
      hidden
      (change)="uploadConfigFile($event)"
    />
  </mat-expansion-panel>
</mat-accordion>

./admin-overview.component.scss

@use "../../../../styles/variables/sizes";

mat-panel-title {
  font-size: 1.3em;
}

mat-panel-description {
  white-space: normal;
}

.admin-sections {
  .section-nav-list {
    margin: 0;
    padding: 0;
  }

  // Clean expansion panel styling - seamless look
  ::ng-deep .mat-expansion-panel {
    box-shadow: none;
    border: none;
    border-radius: 0;
    margin-bottom: 0;

    &:not(:last-child) {
      border-bottom: 1px solid rgba(0, 0, 0, 0.12);
    }
  }

  // Stack title and description vertically in expansion panel headers
  ::ng-deep .mat-expansion-panel-header {
    padding: sizes.$regular sizes.$large;
    height: auto !important; // Allow header to grow with content

    .mat-content {
      flex-direction: column;
      align-items: flex-start;
      gap: sizes.$x-small;
      overflow: visible;
      padding-right: sizes.$large;
    }

    &:hover {
      background: rgba(0, 0, 0, 0.04);
    }
  }

  ::ng-deep .mat-expansion-panel-content .mat-expansion-panel-body {
    /* add a slight left border to visually connect the related header */
    padding-left: 1em;
    padding-right: 1em;
    padding-bottom: 0;
    margin-bottom: sizes.$regular;
    border-left: solid 1em #00000014;
    border-bottom-left-radius: var(--mat-expansion-container-shape, 12px);
  }
  ::ng-deep .mat-expansion-panel-header.mat-expanded {
    /* add a slight background to visually connect the related sub-items */
    background-color: #00000014;
    border-top-left-radius: var(--mat-expansion-container-shape, 12px);
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""