mirror of
https://github.com/chuot/rdio-scanner.git
synced 2026-08-13 08:33:00 -06:00
feat: update to angular 21
This commit is contained in:
parent
c9cc7f0a3c
commit
53a426bea9
|
|
@ -16,14 +16,15 @@
|
||||||
"root": "",
|
"root": "",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"prefix": "rdio-scanner",
|
"prefix": "rdio-scanner",
|
||||||
"architect": {
|
"targets": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "../server/webapp",
|
"outputPath": "../server/webapp",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
"polyfills": [
|
||||||
"polyfills": "src/polyfills.ts",
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
|
|
@ -36,15 +37,16 @@
|
||||||
],
|
],
|
||||||
"scripts": [],
|
"scripts": [],
|
||||||
"serviceWorker": true,
|
"serviceWorker": true,
|
||||||
"ngswConfigPath": "ngsw-config.json"
|
"ngswConfigPath": "ngsw-config.json",
|
||||||
|
"main": "src/main.ts"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "1500kb",
|
"maximumWarning": "2mb",
|
||||||
"maximumError": "1500kb"
|
"maximumError": "2mb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
|
|
@ -61,9 +63,7 @@
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"buildOptimizer": false,
|
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
"vendorChunk": true,
|
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"namedChunks": true
|
"namedChunks": true
|
||||||
|
|
@ -75,11 +75,11 @@
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "rdio-scanner:build:production"
|
"buildTarget": "rdio-scanner:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"browserTarget": "rdio-scanner:build:development",
|
"proxyConfig": "src/proxy.conf.js",
|
||||||
"proxyConfig": "src/proxy.conf.js"
|
"buildTarget": "rdio-scanner:build:development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
|
|
@ -87,14 +87,16 @@
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "rdio-scanner:build"
|
"buildTarget": "rdio-scanner:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"main": "src/test.ts",
|
"main": "src/test.ts",
|
||||||
"polyfills": "src/polyfills.ts",
|
"polyfills": [
|
||||||
|
"src/polyfills.ts"
|
||||||
|
],
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"karmaConfig": "karma.conf.js",
|
"karmaConfig": "karma.conf.js",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
|
|
@ -114,5 +116,31 @@
|
||||||
},
|
},
|
||||||
"cli": {
|
"cli": {
|
||||||
"analytics": false
|
"analytics": false
|
||||||
|
},
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"type": "component"
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"type": "directive"
|
||||||
|
},
|
||||||
|
"@schematics/angular:service": {
|
||||||
|
"type": "service"
|
||||||
|
},
|
||||||
|
"@schematics/angular:guard": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:interceptor": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:module": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:pipe": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:resolver": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15368
client/package-lock.json
generated
15368
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,32 +1,34 @@
|
||||||
{
|
{
|
||||||
"author": "Chrystian Huot <chrystian.huot@saubeo.solutions>",
|
"author": "Chrystian Huot <chrystian@huot.qc.ca>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^15.0.4",
|
"@angular/animations": "^21.0.6",
|
||||||
"@angular/cdk": "^15.0.3",
|
"@angular/cdk": "^21.0.5",
|
||||||
"@angular/common": "^15.0.4",
|
"@angular/common": "^21.0.6",
|
||||||
"@angular/compiler": "^15.0.4",
|
"@angular/compiler": "^21.0.6",
|
||||||
"@angular/core": "^15.0.4",
|
"@angular/core": "^21.0.6",
|
||||||
"@angular/forms": "^15.0.4",
|
"@angular/forms": "^21.0.6",
|
||||||
"@angular/material": "^15.0.3",
|
"@angular/material": "^21.0.5",
|
||||||
"@angular/platform-browser": "^15.0.4",
|
"@angular/platform-browser": "^21.0.6",
|
||||||
"@angular/platform-browser-dynamic": "^15.0.4",
|
"@angular/platform-browser-dynamic": "^21.0.6",
|
||||||
"@angular/router": "^15.0.4",
|
"@angular/router": "^21.0.6",
|
||||||
"@angular/service-worker": "^15.0.4",
|
"@angular/service-worker": "^21.0.6",
|
||||||
"rxjs": "~7.5.0",
|
"material-icons": "^1.13.14",
|
||||||
"tslib": "^2.3.0",
|
"rxjs": "~7.8.2",
|
||||||
"zone.js": "~0.11.4"
|
"tslib": "^2.8.1",
|
||||||
|
"zone.js": "~0.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^15.0.4",
|
"@angular-devkit/build-angular": "^21.0.4",
|
||||||
"@angular/cli": "^15.0.4",
|
"@angular/build": "^21.0.4",
|
||||||
"@angular/compiler-cli": "^15.0.4",
|
"@angular/cli": "^21.0.4",
|
||||||
"@types/node": "^18.11.2",
|
"@angular/compiler-cli": "^21.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
"@types/node": "^25.0.3",
|
||||||
"@typescript-eslint/parser": "^5.40.1",
|
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
||||||
|
"@typescript-eslint/parser": "^8.50.0",
|
||||||
"ajv-keywords": "^5.1.0",
|
"ajv-keywords": "^5.1.0",
|
||||||
"eslint": "^8.25.0",
|
"eslint": "^9.39.2",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.2",
|
||||||
"typescript": "~4.8.4"
|
"typescript": "~5.9.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0"
|
"node": ">=16.0"
|
||||||
|
|
@ -36,7 +38,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ng build --base-href ./ --configuration production",
|
"build": "ng build --base-href ./ --configuration production",
|
||||||
"start": "ng serve"
|
"serve": "ng serve"
|
||||||
},
|
},
|
||||||
"version": "7.0.0"
|
"version": "7.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,5 +23,6 @@ import { Component } from '@angular/core';
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
styleUrls: ['./app.component.scss'],
|
styleUrls: ['./app.component.scss'],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AppComponent { }
|
export class AppComponent { }
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
<ng-container *ngIf="!authenticated">
|
@if (!authenticated) {
|
||||||
<rdio-scanner-admin-login></rdio-scanner-admin-login>
|
<rdio-scanner-admin-login></rdio-scanner-admin-login>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngIf="authenticated">
|
@if (authenticated) {
|
||||||
<rdio-scanner-admin-todos></rdio-scanner-admin-todos>
|
<rdio-scanner-admin-todos></rdio-scanner-admin-todos>
|
||||||
<mat-accordion displayMode="flat">
|
<mat-accordion displayMode="flat">
|
||||||
<mat-expansion-panel #configPanel (afterCollapse)="configComponent?.closeAll()">
|
<mat-expansion-panel #configPanel (afterCollapse)="configComponent.closeAll()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>settings</mat-icon>
|
<mat-icon>settings</mat-icon>
|
||||||
Config
|
Config
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-config #configComponent></rdio-scanner-admin-config>
|
<rdio-scanner-admin-config #configComponent></rdio-scanner-admin-config>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel (afterExpand)="logsComponent.reload()">
|
<mat-expansion-panel (afterExpand)="logsComponent.reload()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>article</mat-icon>
|
<mat-icon>article</mat-icon>
|
||||||
Logs
|
Logs
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-logs #logsComponent></rdio-scanner-admin-logs>
|
<rdio-scanner-admin-logs #logsComponent></rdio-scanner-admin-logs>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel (afterCollapse)="toolsComponent?.closeAll()">
|
<mat-expansion-panel (afterCollapse)="toolsComponent.closeAll()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>build</mat-icon>
|
<mat-icon>build</mat-icon>
|
||||||
Tools
|
Tools
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-tools #toolsComponent (config)="configComponent.reset($event, { dirty: true }); configPanel.open()"></rdio-scanner-admin-tools>
|
<rdio-scanner-admin-tools #toolsComponent (config)="configComponent.reset($event, { dirty: true }); configPanel.open()"></rdio-scanner-admin-tools>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel hideToggle (click)="logout()">
|
<mat-expansion-panel hideToggle (click)="logout()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>logout</mat-icon>
|
<mat-icon>logout</mat-icon>
|
||||||
Logout
|
Logout
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
</ng-container>
|
}
|
||||||
|
|
@ -25,6 +25,7 @@ import { AdminEvent, RdioScannerAdminService, Group, Tag } from './admin.service
|
||||||
selector: 'rdio-scanner-admin',
|
selector: 'rdio-scanner-admin',
|
||||||
styleUrls: ['./admin.component.scss'],
|
styleUrls: ['./admin.component.scss'],
|
||||||
templateUrl: './admin.component.html',
|
templateUrl: './admin.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminComponent implements OnDestroy {
|
export class RdioScannerAdminComponent implements OnDestroy {
|
||||||
authenticated = true;
|
authenticated = true;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { AppSharedModule } from '../../../shared/shared.module';
|
import { AppSharedModule } from '../../../shared/shared.module';
|
||||||
import { RdioScannerAdminComponent } from './admin.component';
|
import { RdioScannerAdminComponent } from './admin.component';
|
||||||
|
|
@ -45,8 +45,7 @@ import { RdioScannerAdminImportTalkgroupsComponent } from './tools/import-talkgr
|
||||||
import { RdioScannerAdminImportUnitsComponent } from './tools/import-units/import-units.component';
|
import { RdioScannerAdminImportUnitsComponent } from './tools/import-units/import-units.component';
|
||||||
import { RdioScannerAdminPasswordComponent } from './tools/password/password.component';
|
import { RdioScannerAdminPasswordComponent } from './tools/password/password.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({ declarations: [
|
||||||
declarations: [
|
|
||||||
RdioScannerAdminComponent,
|
RdioScannerAdminComponent,
|
||||||
RdioScannerAdminConfigComponent,
|
RdioScannerAdminConfigComponent,
|
||||||
RdioScannerAdminAccessComponent,
|
RdioScannerAdminAccessComponent,
|
||||||
|
|
@ -71,9 +70,5 @@ import { RdioScannerAdminPasswordComponent } from './tools/password/password.com
|
||||||
RdioScannerAdminToolsComponent,
|
RdioScannerAdminToolsComponent,
|
||||||
RdioScannerAdminUnitComponent,
|
RdioScannerAdminUnitComponent,
|
||||||
],
|
],
|
||||||
entryComponents: [RdioScannerAdminSystemsSelectComponent],
|
exports: [RdioScannerAdminComponent], imports: [AppSharedModule], providers: [RdioScannerAdminService, provideHttpClient(withInterceptorsFromDi())] })
|
||||||
exports: [RdioScannerAdminComponent],
|
|
||||||
imports: [AppSharedModule, HttpClientModule],
|
|
||||||
providers: [RdioScannerAdminService],
|
|
||||||
})
|
|
||||||
export class RdioScannerAdminModule { }
|
export class RdioScannerAdminModule { }
|
||||||
|
|
|
||||||
|
|
@ -1,95 +1,111 @@
|
||||||
<div class="row top">
|
<div class="row top">
|
||||||
<p class="mat-body">You can decide to leave your instance unlocked and accessible to everyone, or set passwords with
|
<p class="mat-body">You can decide to leave your instance unlocked and accessible to everyone, or set passwords with
|
||||||
specific systems/talkgroups. When used, the user will be prompted once for their password. It is then stored
|
specific systems/talkgroups. When used, the user will be prompted once for their password. It is then stored
|
||||||
internally on the client side and reused thereafter.</p>
|
internally on the client side and reused thereafter.</p>
|
||||||
<button type="button" mat-button color="accent" (click)="add()">New access</button>
|
<button type="button" mat-button color="accent" (click)="add()">New access</button>
|
||||||
</div>
|
</div>
|
||||||
<p *ngIf="!accesses.length" class="mat-small text-center">No access is defined, your instance is open and accessible to
|
@if (!accesses.length) {
|
||||||
all</p>
|
<p class="mat-small text-center">No access is defined, your instance is open and accessible to
|
||||||
|
all</p>
|
||||||
|
}
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="accesses" (cdkDropListDropped)="drop($event)">
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="accesses" (cdkDropListDropped)="drop($event)">
|
||||||
<mat-expansion-panel *ngFor="let access of accesses; index as i" cdkDrag>
|
@for (access of accesses; track access; let i = $index) {
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel cdkDrag>
|
||||||
<mat-panel-title>
|
<mat-expansion-panel-header>
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
<mat-panel-title>
|
||||||
{{ access.value.ident || 'NewAccess' }}
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
<mat-icon *ngIf="access.invalid" color="warn">error</mat-icon>
|
{{ access.value.ident || 'NewAccess' }}
|
||||||
</mat-panel-title>
|
@if (access.invalid) {
|
||||||
</mat-expansion-panel-header>
|
<mat-icon color="warn">error</mat-icon>
|
||||||
<ng-container [formGroup]="access">
|
}
|
||||||
<div class="row">
|
</mat-panel-title>
|
||||||
<p>
|
</mat-expansion-panel-header>
|
||||||
<span class="mat-body">Code</span><br>
|
<ng-container [formGroup]="access">
|
||||||
<span class="mat-caption">The access code to unlock the client.</span>
|
<div class="row">
|
||||||
</p>
|
<p>
|
||||||
<mat-form-field floatLabel="auto">
|
<span class="mat-body">Code</span><br>
|
||||||
<input [type]="hideCode ? 'password' : 'text'" matInput formControlName="code" placeholder="Code">
|
<span class="mat-caption">The access code to unlock the client.</span>
|
||||||
<button type="button" mat-icon-button matSuffix (click)="hideCode = !hideCode">
|
</p>
|
||||||
<mat-icon>{{ hideCode ? 'visibility_off' : 'visibility' }}</mat-icon>
|
<mat-form-field floatLabel="auto">
|
||||||
</button>
|
<input [type]="hideCode ? 'password' : 'text'" matInput formControlName="code" placeholder="Code">
|
||||||
<mat-error *ngIf="access.get('code')?.hasError('duplicate')">
|
<button type="button" mat-icon-button matSuffix (click)="hideCode = !hideCode">
|
||||||
Code is already defined
|
<mat-icon>{{ hideCode ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
</mat-error>
|
</button>
|
||||||
<mat-error *ngIf="access.get('code')?.hasError('required')">
|
@if (access.get('code')?.hasError('duplicate')) {
|
||||||
Code is required
|
<mat-error>
|
||||||
</mat-error>
|
Code is already defined
|
||||||
</mat-form-field>
|
</mat-error>
|
||||||
</div>
|
}
|
||||||
<div class="row">
|
@if (access.get('code')?.hasError('required')) {
|
||||||
<p>
|
<mat-error>
|
||||||
<span class="mat-body">Ident</span><br>
|
Code is required
|
||||||
<span class="mat-caption">Dummy identifier which serves only to identify this access code is for
|
</mat-error>
|
||||||
whom.</span>
|
}
|
||||||
</p>
|
</mat-form-field>
|
||||||
<mat-form-field floatLabel="auto">
|
</div>
|
||||||
<input type="text" matInput formControlName="ident" placeholder="Ident">
|
<div class="row">
|
||||||
<mat-error *ngIf="access.get('ident')?.hasError('required')">
|
<p>
|
||||||
Ident is required
|
<span class="mat-body">Ident</span><br>
|
||||||
</mat-error>
|
<span class="mat-caption">Dummy identifier which serves only to identify this access code is for
|
||||||
</mat-form-field>
|
whom.</span>
|
||||||
</div>
|
</p>
|
||||||
<div class="row">
|
<mat-form-field floatLabel="auto">
|
||||||
<p>
|
<input type="text" matInput formControlName="ident" placeholder="Ident">
|
||||||
<span class="mat-body">Expiration</span><br>
|
@if (access.get('ident')?.hasError('required')) {
|
||||||
<span class="mat-caption">Expiration date for this access code.</span>
|
<mat-error>
|
||||||
</p>
|
Ident is required
|
||||||
<mat-form-field floatLabel="auto">
|
</mat-error>
|
||||||
<input type="text" matInput formControlName="expiration" placeholder="Expiration date"
|
}
|
||||||
[matDatepicker]="expirationDate" (click)="expirationDate.open()">
|
</mat-form-field>
|
||||||
<mat-datepicker-toggle matSuffix [for]="expirationDate"></mat-datepicker-toggle>
|
</div>
|
||||||
<mat-datepicker #expirationDate></mat-datepicker>
|
<div class="row">
|
||||||
</mat-form-field>
|
<p>
|
||||||
</div>
|
<span class="mat-body">Expiration</span><br>
|
||||||
<div class="row">
|
<span class="mat-caption">Expiration date for this access code.</span>
|
||||||
<p>
|
</p>
|
||||||
<span class="mat-body">Limit</span><br>
|
<mat-form-field floatLabel="auto">
|
||||||
<span class="mat-caption">Simultaneous connection limit. Access will be refused if the connection
|
<input type="text" matInput formControlName="expiration" placeholder="Expiration date"
|
||||||
with this access code exceeds the defined limit.</span>
|
[matDatepicker]="expirationDate" (click)="expirationDate.open()">
|
||||||
</p>
|
<mat-datepicker-toggle matSuffix [for]="expirationDate"></mat-datepicker-toggle>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-datepicker #expirationDate></mat-datepicker>
|
||||||
<input type="number" min="0" step="1" matInput formControlName="limit" placeholder="Limit">
|
</mat-form-field>
|
||||||
</mat-form-field>
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
<div class="row">
|
<p>
|
||||||
<p>
|
<span class="mat-body">Limit</span><br>
|
||||||
<span class="mat-body">Access</span><br>
|
<span class="mat-caption">Simultaneous connection limit. Access will be refused if the connection
|
||||||
<span class="mat-caption">
|
with this access code exceeds the defined limit.</span>
|
||||||
The access code allows access to <u>
|
</p>
|
||||||
<ng-container *ngIf="access.value.systems === '*'">all</ng-container>
|
<mat-form-field floatLabel="auto">
|
||||||
<ng-container *ngIf="access.value.systems !== '*'">some</ng-container>
|
<input type="number" min="0" step="1" matInput formControlName="limit" placeholder="Limit">
|
||||||
</u> systems and talkgroups.
|
</mat-form-field>
|
||||||
</span>
|
</div>
|
||||||
</p>
|
<div class="row">
|
||||||
<div>
|
<p>
|
||||||
<button type="button" mat-button [disabled]="access.disabled" (click)="select(access)">
|
<span class="mat-body">Access</span><br>
|
||||||
Choose systems
|
<span class="mat-caption">
|
||||||
</button>
|
The access code allows access to <u>
|
||||||
</div>
|
@if (access.value.systems === '*') {
|
||||||
</div>
|
all
|
||||||
<div class="row bottom">
|
}
|
||||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
@if (access.value.systems !== '*') {
|
||||||
Delete access
|
some
|
||||||
</button>
|
}
|
||||||
</div>
|
</u> systems and talkgroups.
|
||||||
</ng-container>
|
</span>
|
||||||
</mat-expansion-panel>
|
</p>
|
||||||
|
<div>
|
||||||
|
<button type="button" mat-button [disabled]="access.disabled" (click)="select(access)">
|
||||||
|
Choose systems
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row bottom">
|
||||||
|
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||||
|
Delete access
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
}
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|
@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-access',
|
selector: 'rdio-scanner-admin-access',
|
||||||
templateUrl: './access.component.html',
|
templateUrl: './access.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminAccessComponent {
|
export class RdioScannerAdminAccessComponent {
|
||||||
@Input() form: FormArray | undefined;
|
@Input() form: FormArray | undefined;
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,10 @@
|
||||||
<button type="button" mat-icon-button matSuffix (click)="copy(key)">
|
<button type="button" mat-icon-button matSuffix (click)="copy(key)">
|
||||||
<mat-icon>content_copy</mat-icon>
|
<mat-icon>content_copy</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-error *ngIf="apiKey.get('key')?.hasError('duplicate')">
|
<mat-error *ngIf="apiKey.get('key').hasError('duplicate')">
|
||||||
Key is already defined
|
Key is already defined
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="apiKey.get('key')?.hasError('required')">
|
<mat-error *ngIf="apiKey.get('key').hasError('required')">
|
||||||
Key is required
|
Key is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="never">
|
<mat-form-field floatLabel="never">
|
||||||
<input type="text" matInput formControlName="ident" placeholder="Ident">
|
<input type="text" matInput formControlName="ident" placeholder="Ident">
|
||||||
<mat-error *ngIf="apiKey.get('ident')?.hasError('required')">
|
<mat-error *ngIf="apiKey.get('ident').hasError('required')">
|
||||||
Ident is required
|
Ident is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,96 @@
|
||||||
<div class="row top">
|
<div class="row top">
|
||||||
<p class="mat-body">API keys are used for recorder upload scripts and downstream instances. Each of them must
|
<p class="mat-body">API keys are used for recorder upload scripts and downstream instances. Each of them must
|
||||||
authenticate with an API key to exchange audio files.</p>
|
authenticate with an API key to exchange audio files.</p>
|
||||||
<button type="button" mat-button color="accent" (click)="add()">New API key</button>
|
<button type="button" mat-button color="accent" (click)="add()">New API key</button>
|
||||||
</div>
|
</div>
|
||||||
<p *ngIf="!apikeys?.length" class="mat-small text-center">No defined API keys</p>
|
@if (!apikeys.length) {
|
||||||
|
<p class="mat-small text-center">No defined API keys</p>
|
||||||
|
}
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="apikeys" (cdkDropListDropped)="drop($event)">
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="apikeys" (cdkDropListDropped)="drop($event)">
|
||||||
<mat-expansion-panel *ngFor="let apikey of apikeys; index as i" cdkDrag>
|
@for (apikey of apikeys; track apikey; let i = $index) {
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel cdkDrag>
|
||||||
<mat-panel-title>
|
<mat-expansion-panel-header>
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
<mat-panel-title>
|
||||||
{{ apikey.value.ident || 'NewApikey' }}
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
<mat-icon *ngIf="apikey.invalid" color="warn">error</mat-icon>
|
{{ apikey.value.ident || 'NewApikey' }}
|
||||||
</mat-panel-title>
|
@if (apikey.invalid) {
|
||||||
</mat-expansion-panel-header>
|
<mat-icon color="warn">error</mat-icon>
|
||||||
<ng-container [formGroup]="apikey">
|
}
|
||||||
<div class="row">
|
</mat-panel-title>
|
||||||
<p>
|
</mat-expansion-panel-header>
|
||||||
<span class="mat-body">Disabled</span><br>
|
<ng-container [formGroup]="apikey">
|
||||||
<span class="mat-caption">Disable the API key.</span>
|
<div class="row">
|
||||||
</p>
|
<p>
|
||||||
<div>
|
<span class="mat-body">Disabled</span><br>
|
||||||
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
<span class="mat-caption">Disable the API key.</span>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<div>
|
||||||
<div class="row">
|
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
||||||
<p>
|
</div>
|
||||||
<span class="mat-body">Key</span><br>
|
</div>
|
||||||
<span class="mat-caption">Api key.</span>
|
<div class="row">
|
||||||
</p>
|
<p>
|
||||||
<mat-form-field floatLabel="auto">
|
<span class="mat-body">Key</span><br>
|
||||||
<input #key type="text" matInput formControlName="key" placeholder="Key">
|
<span class="mat-caption">Api key.</span>
|
||||||
<button type="button" mat-icon-button matSuffix (click)="copy(key)">
|
</p>
|
||||||
<mat-icon>content_copy</mat-icon>
|
<mat-form-field floatLabel="auto">
|
||||||
</button>
|
<input #key type="text" matInput formControlName="key" placeholder="Key">
|
||||||
<mat-error *ngIf="apikey.get('key')?.hasError('duplicate')">
|
<button type="button" mat-icon-button matSuffix (click)="copy(key)">
|
||||||
Key is already defined
|
<mat-icon>content_copy</mat-icon>
|
||||||
</mat-error>
|
</button>
|
||||||
<mat-error *ngIf="apikey.get('key')?.hasError('required')">
|
@if (apikey.get('key')!.hasError('duplicate')) {
|
||||||
Key is required
|
<mat-error>
|
||||||
</mat-error>
|
Key is already defined
|
||||||
</mat-form-field>
|
</mat-error>
|
||||||
</div>
|
}
|
||||||
<div class="row">
|
@if (apikey.get('key')!.hasError('required')) {
|
||||||
<p>
|
<mat-error>
|
||||||
<span class="mat-body">Ident</span><br>
|
Key is required
|
||||||
<span class="mat-caption">Dummy identifier which serves only to identify this API key is for
|
</mat-error>
|
||||||
whom.</span>
|
}
|
||||||
</p>
|
</mat-form-field>
|
||||||
<mat-form-field floatLabel="auto">
|
</div>
|
||||||
<input type="text" matInput formControlName="ident" placeholder="Ident">
|
<div class="row">
|
||||||
<mat-error *ngIf="apikey.get('ident')?.hasError('required')">
|
<p>
|
||||||
Ident is required
|
<span class="mat-body">Ident</span><br>
|
||||||
</mat-error>
|
<span class="mat-caption">Dummy identifier which serves only to identify this API key is for
|
||||||
</mat-form-field>
|
whom.</span>
|
||||||
</div>
|
</p>
|
||||||
<div class="row">
|
<mat-form-field floatLabel="auto">
|
||||||
<p>
|
<input type="text" matInput formControlName="ident" placeholder="Ident">
|
||||||
<span class="mat-body">Access</span><br>
|
@if (apikey.get('ident')!.hasError('required')) {
|
||||||
<span class="mat-caption">
|
<mat-error>
|
||||||
This API key allows access to <u>
|
Ident is required
|
||||||
<ng-container *ngIf="apikey.value.systems === '*'">all</ng-container>
|
</mat-error>
|
||||||
<ng-container *ngIf="apikey.value.systems !== '*'">some</ng-container>
|
}
|
||||||
</u> systems and talkgroups.
|
</mat-form-field>
|
||||||
</span>
|
</div>
|
||||||
</p>
|
<div class="row">
|
||||||
<div>
|
<p>
|
||||||
<button type="button" mat-button [disabled]="apikey.disabled" (click)="select(apikey)">
|
<span class="mat-body">Access</span><br>
|
||||||
Choose systems
|
<span class="mat-caption">
|
||||||
</button>
|
This API key allows access to <u>
|
||||||
</div>
|
@if (apikey.value.systems === '*') {
|
||||||
</div>
|
all
|
||||||
<div class="row bottom">
|
}
|
||||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
@if (apikey.value.systems !== '*') {
|
||||||
Delete API key
|
some
|
||||||
</button>
|
}
|
||||||
</div>
|
</u> systems and talkgroups.
|
||||||
</ng-container>
|
</span>
|
||||||
</mat-expansion-panel>
|
</p>
|
||||||
|
<div>
|
||||||
|
<button type="button" mat-button [disabled]="apikey.disabled" (click)="select(apikey)">
|
||||||
|
Choose systems
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row bottom">
|
||||||
|
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||||
|
Delete API key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
}
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|
@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-apikeys',
|
selector: 'rdio-scanner-admin-apikeys',
|
||||||
templateUrl: './apikeys.component.html',
|
templateUrl: './apikeys.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminApikeysComponent {
|
export class RdioScannerAdminApikeysComponent {
|
||||||
@Input() form: FormArray | undefined;
|
@Input() form: FormArray | undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,90 @@
|
||||||
<form *ngIf="form" autocomplete="off" [formGroup]="form" (ngSubmit)="save()">
|
<form *ngIf="form as f" autocomplete="off" [formGroup]="f" (ngSubmit)="save()">
|
||||||
<mat-accordion displayMode="flat">
|
<mat-accordion displayMode="flat">
|
||||||
<mat-expansion-panel (afterCollapse)="accessComponent.closeAll()">
|
<mat-expansion-panel (afterCollapse)="accessComponent.closeAll()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>manage_accounts</mat-icon>
|
<mat-icon>manage_accounts</mat-icon>
|
||||||
Access
|
Access
|
||||||
<mat-icon *ngIf="form?.get('access')?.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="f.get('access')?.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-access #accessComponent [form]="access"></rdio-scanner-admin-access>
|
<rdio-scanner-admin-access #accessComponent [form]="access"></rdio-scanner-admin-access>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel (afterCollapse)="apikeyComponent.closeAll()">
|
<mat-expansion-panel (afterCollapse)="apikeyComponent.closeAll()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>vpn_key</mat-icon>
|
<mat-icon>vpn_key</mat-icon>
|
||||||
ApiKey
|
ApiKey
|
||||||
<mat-icon *ngIf="form?.get('apikey')?.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="f.get('apikey')?.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-apikeys #apikeyComponent [form]="apikeys"></rdio-scanner-admin-apikeys>
|
<rdio-scanner-admin-apikeys #apikeyComponent [form]="apikeys"></rdio-scanner-admin-apikeys>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel *ngIf="!docker" (afterCollapse)="dirwatchComponent.closeAll()">
|
<mat-expansion-panel *ngIf="!docker" (afterCollapse)="dirwatchComponent.closeAll()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>folder</mat-icon>
|
<mat-icon>folder</mat-icon>
|
||||||
Dirwatch
|
Dirwatch
|
||||||
<mat-icon *ngIf="form?.get('dirwatch')?.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="f.get('dirwatch')?.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-dirwatch #dirwatchComponent [form]="dirwatch"></rdio-scanner-admin-dirwatch>
|
<rdio-scanner-admin-dirwatch #dirwatchComponent [form]="dirwatch"></rdio-scanner-admin-dirwatch>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel (afterCollapse)="downstreamsComponent.closeAll()">
|
<mat-expansion-panel (afterCollapse)="downstreamsComponent.closeAll()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>share</mat-icon>
|
<mat-icon>share</mat-icon>
|
||||||
Downstreams
|
Downstreams
|
||||||
<mat-icon *ngIf="form?.get('downstreams')?.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="f.get('downstreams')?.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-downstreams #downstreamsComponent [form]="downstreams"></rdio-scanner-admin-downstreams>
|
<rdio-scanner-admin-downstreams #downstreamsComponent [form]="downstreams"></rdio-scanner-admin-downstreams>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel>
|
<mat-expansion-panel>
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>workspaces</mat-icon>
|
<mat-icon>workspaces</mat-icon>
|
||||||
Groups
|
Groups
|
||||||
<mat-icon *ngIf="form?.get('groups')?.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="f.get('groups')?.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-groups [form]="groups"></rdio-scanner-admin-groups>
|
<rdio-scanner-admin-groups [form]="groups"></rdio-scanner-admin-groups>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel>
|
<mat-expansion-panel>
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>tune</mat-icon>
|
<mat-icon>tune</mat-icon>
|
||||||
Options
|
Options
|
||||||
<mat-icon *ngIf="form?.get('options')?.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="f.get('options')?.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-options [form]="options"></rdio-scanner-admin-options>
|
<rdio-scanner-admin-options [form]="options"></rdio-scanner-admin-options>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel (afterCollapse)="systemsComponent.closeAll()">
|
<mat-expansion-panel (afterCollapse)="systemsComponent.closeAll()">
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>podcasts</mat-icon>
|
<mat-icon>podcasts</mat-icon>
|
||||||
Systems
|
Systems
|
||||||
<mat-icon *ngIf="form?.get('systems')?.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="f.get('systems')?.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-systems #systemsComponent [form]="systems"></rdio-scanner-admin-systems>
|
<rdio-scanner-admin-systems #systemsComponent [form]="systems"></rdio-scanner-admin-systems>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel>
|
<mat-expansion-panel>
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>sell</mat-icon>
|
<mat-icon>sell</mat-icon>
|
||||||
Tags
|
Tags
|
||||||
<mat-icon *ngIf="form?.get('tags')?.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="f.get('tags')?.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-tags [form]="tags"></rdio-scanner-admin-tags>
|
<rdio-scanner-admin-tags [form]="tags"></rdio-scanner-admin-tags>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
<div class="row bottom">
|
<div class="row bottom">
|
||||||
<button type="button" mat-raised-button [disabled]="form.disabled || form.pristine"
|
<button type="button" mat-raised-button [disabled]="f.disabled || f.pristine"
|
||||||
(click)="reset()">Reset</button>
|
(click)="reset()">Reset</button>
|
||||||
<button type="submit" mat-raised-button color="primary"
|
<button type="submit" mat-raised-button color="primary"
|
||||||
[disabled]="form.disabled || form.pristine || !form.valid">Save</button>
|
[disabled]="f.disabled || f.pristine || !f.valid">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -21,6 +21,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
|
||||||
import { FormArray, FormControl, FormGroup } from '@angular/forms';
|
import { FormArray, FormControl, FormGroup } from '@angular/forms';
|
||||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||||
import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service';
|
import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
|
@ -28,6 +30,7 @@ import { AdminEvent, RdioScannerAdminService, Config } from '../admin.service';
|
||||||
selector: 'rdio-scanner-admin-config',
|
selector: 'rdio-scanner-admin-config',
|
||||||
styleUrls: ['./config.component.scss'],
|
styleUrls: ['./config.component.scss'],
|
||||||
templateUrl: './config.component.html',
|
templateUrl: './config.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||||
docker = false;
|
docker = false;
|
||||||
|
|
@ -68,7 +71,7 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
private config: Config | undefined;
|
private config: Config | undefined;
|
||||||
|
|
||||||
private eventSubscription;
|
private readonly destroy$ = new Subject<void>();
|
||||||
|
|
||||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||||
|
|
||||||
|
|
@ -76,10 +79,9 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||||
private adminService: RdioScannerAdminService,
|
private adminService: RdioScannerAdminService,
|
||||||
private ngChangeDetectorRef: ChangeDetectorRef,
|
private ngChangeDetectorRef: ChangeDetectorRef,
|
||||||
) {
|
) {
|
||||||
this.eventSubscription = this.adminService.event.subscribe(async (event: AdminEvent) => {
|
this.adminService.event.pipe(takeUntil(this.destroy$)).subscribe(async (event: AdminEvent) => {
|
||||||
if ('authenticated' in event && event.authenticated === true) {
|
if ('authenticated' in event && event.authenticated === true) {
|
||||||
this.config = await this.adminService.getConfig();
|
this.config = await this.adminService.getConfig();
|
||||||
|
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +100,8 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.eventSubscription.unsubscribe();
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit(): Promise<void> {
|
async ngOnInit(): Promise<void> {
|
||||||
|
|
@ -116,12 +119,17 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||||
reset(config = this.config, options?: { dirty?: boolean }): void {
|
reset(config = this.config, options?: { dirty?: boolean }): void {
|
||||||
this.form = this.adminService.newConfigForm(config);
|
this.form = this.adminService.newConfigForm(config);
|
||||||
|
|
||||||
this.form.statusChanges.subscribe(() => {
|
this.form.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||||
this.ngChangeDetectorRef.markForCheck();
|
this.ngChangeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.groups.valueChanges.subscribe(() => {
|
const systemsArray = this.systems;
|
||||||
this.systems.controls.forEach((system) => {
|
const groupsArray = this.groups;
|
||||||
|
const tagsArray = this.tags;
|
||||||
|
|
||||||
|
groupsArray?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||||
|
if (!systemsArray) { return; }
|
||||||
|
systemsArray.controls.forEach((system) => {
|
||||||
const talkgroups = system.get('talkgroups') as FormArray;
|
const talkgroups = system.get('talkgroups') as FormArray;
|
||||||
|
|
||||||
talkgroups.controls.forEach((talkgroup) => {
|
talkgroups.controls.forEach((talkgroup) => {
|
||||||
|
|
@ -136,8 +144,9 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.tags.valueChanges.subscribe(() => {
|
tagsArray?.valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||||
this.systems.controls.forEach((system) => {
|
if (!systemsArray) { return; }
|
||||||
|
systemsArray.controls.forEach((system) => {
|
||||||
const talkgroups = system.get('talkgroups') as FormArray;
|
const talkgroups = system.get('talkgroups') as FormArray;
|
||||||
|
|
||||||
talkgroups.controls.forEach((talkgroup) => {
|
talkgroups.controls.forEach((talkgroup) => {
|
||||||
|
|
@ -160,8 +169,10 @@ export class RdioScannerAdminConfigComponent implements OnDestroy, OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(): Promise<void> {
|
async save(): Promise<void> {
|
||||||
this.form?.markAsPristine();
|
if (!this.form) { return; }
|
||||||
|
|
||||||
await this.adminService.saveConfig(this.form?.getRawValue());
|
this.form.markAsPristine();
|
||||||
|
|
||||||
|
await this.adminService.saveConfig(this.form.getRawValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,240 +1,282 @@
|
||||||
<div class="row top">
|
<div class="row top">
|
||||||
<p class="mat-body">Define a dirwatch to monitor new audio files from any directory.</p>
|
<p class="mat-body">Define a dirwatch to monitor new audio files from any directory.</p>
|
||||||
<button type="button" mat-button color="accent" (click)="add()">New dir watch</button>
|
<button type="button" mat-button color="accent" (click)="add()">New dir watch</button>
|
||||||
</div>
|
</div>
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="dirwatches"
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="dirwatches"
|
||||||
(cdkDropListDropped)="drop($event)">
|
(cdkDropListDropped)="drop($event)">
|
||||||
<mat-expansion-panel *ngFor="let dirwatch of dirwatches; index as i" cdkDrag>
|
@for (dirwatch of dirwatches; track dirwatch; let i = $index) {
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel cdkDrag>
|
||||||
<mat-panel-title>
|
<mat-expansion-panel-header>
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
<mat-panel-title>
|
||||||
{{ dirwatch.value.directory || 'NewDirwatch' }}
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
<mat-icon *ngIf="dirwatch.invalid" color="warn">error</mat-icon>
|
{{ dirwatch.value.directory || 'NewDirwatch' }}
|
||||||
</mat-panel-title>
|
@if (dirwatch.invalid) {
|
||||||
</mat-expansion-panel-header>
|
<mat-icon color="warn">error</mat-icon>
|
||||||
<ng-container [formGroup]="dirwatch">
|
}
|
||||||
<div class="row">
|
</mat-panel-title>
|
||||||
<p>
|
</mat-expansion-panel-header>
|
||||||
<span class="mat-body">Disabled</span><br>
|
<ng-container [formGroup]="dirwatch">
|
||||||
<span class="mat-caption">Disable the dirwatch.</span>
|
<div class="row">
|
||||||
</p>
|
<p>
|
||||||
<div>
|
<span class="mat-body">Disabled</span><br>
|
||||||
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
<span class="mat-caption">Disable the dirwatch.</span>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<div>
|
||||||
<div class="row">
|
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
||||||
<p>
|
</div>
|
||||||
<span class="mat-body">Delete After</span><br>
|
</div>
|
||||||
<span class="mat-caption">Delete the audio file after being ingested. If activated, all pre-existing
|
<div class="row">
|
||||||
audio files will be ingested and deleted as soon as the server starts. If not activated,
|
<p>
|
||||||
pre-existing audio files are neither ingested nor deleted.</span>
|
<span class="mat-body">Delete After</span><br>
|
||||||
</p>
|
<span class="mat-caption">Delete the audio file after being ingested. If activated, all pre-existing
|
||||||
<div>
|
audio files will be ingested and deleted as soon as the server starts. If not activated,
|
||||||
<mat-slide-toggle color="primary" formControlName="deleteAfter"></mat-slide-toggle>
|
pre-existing audio files are neither ingested nor deleted.</span>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<div>
|
||||||
<div class="row">
|
<mat-slide-toggle color="primary" formControlName="deleteAfter"></mat-slide-toggle>
|
||||||
<p>
|
</div>
|
||||||
<span class="mat-body">Type</span><br>
|
</div>
|
||||||
<span class="mat-caption">
|
<div class="row">
|
||||||
Dirwatch type defines how the metadata are obtained.
|
<p>
|
||||||
<ul>
|
<span class="mat-body">Type</span><br>
|
||||||
<li><b>Default</b> - Extract the metadata from a custom mask.</li>
|
<span class="mat-caption">
|
||||||
<li><b>DSDPlus Fast Lane</b> - Extract the metadata from the file path.</li>
|
Dirwatch type defines how the metadata are obtained.
|
||||||
<li><b>SDR Trunk</b> - Extract the metadata from the MP3 tags defined on the SDR Trunk's
|
<ul>
|
||||||
aliases tab.</li>
|
<li><b>Default</b> - Extract the metadata from a custom mask.</li>
|
||||||
<li><b>Trunk Recorder</b> - Extract the metadata from the json file.</li>
|
<li><b>DSDPlus Fast Lane</b> - Extract the metadata from the file path.</li>
|
||||||
</ul>
|
<li><b>SDR Trunk</b> - Extract the metadata from the MP3 tags defined on the SDR Trunk's
|
||||||
</span>
|
aliases tab.</li>
|
||||||
</p>
|
<li><b>Trunk Recorder</b> - Extract the metadata from the json file.</li>
|
||||||
<mat-form-field floatLabel="auto">
|
</ul>
|
||||||
<mat-select formControlName="type" placeholder="Type">
|
</span>
|
||||||
<mat-option value="default">Default</mat-option>
|
</p>
|
||||||
<mat-option value="dsdplus">DSDPlus Fast Lane</mat-option>
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-option value="sdr-trunk">SDR Trunk</mat-option>
|
<mat-select formControlName="type" placeholder="Type">
|
||||||
<mat-option value="trunk-recorder">Trunk Recorder</mat-option>
|
<mat-option value="default">Default</mat-option>
|
||||||
</mat-select>
|
<mat-option value="dsdplus">DSDPlus Fast Lane</mat-option>
|
||||||
</mat-form-field>
|
<mat-option value="sdr-trunk">SDR Trunk</mat-option>
|
||||||
</div>
|
<mat-option value="trunk-recorder">Trunk Recorder</mat-option>
|
||||||
<div class="row">
|
</mat-select>
|
||||||
<p>
|
</mat-form-field>
|
||||||
<span class="mat-body">Directory</span><br>
|
</div>
|
||||||
<span class="mat-caption">
|
<div class="row">
|
||||||
Path of the
|
<p>
|
||||||
<ng-container [ngSwitch]="dirwatch.get('type')?.value">
|
<span class="mat-body">Directory</span><br>
|
||||||
<ng-container *ngSwitchCase="'dsdplus'">
|
<span class="mat-caption">
|
||||||
<b>Record</b>, <b>1R-Record</b> or <b>VC-Record</b>
|
Path of the
|
||||||
</ng-container>
|
@switch (dirwatch.get('type')?.value) {
|
||||||
<ng-container *ngSwitchCase="'sdr-trunk'">
|
@case ('dsdplus') {
|
||||||
<b>Recordings</b>
|
<b>Record</b>, <b>1R-Record</b> or <b>VC-Record</b>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngSwitchCase="'trunk-recorder'">
|
@case ('sdr-trunk') {
|
||||||
<b>captureDir</b>
|
<b>Recordings</b>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngSwitchDefault>
|
@case ('trunk-recorder') {
|
||||||
local
|
<b>captureDir</b>
|
||||||
</ng-container>
|
}
|
||||||
</ng-container>
|
@default {
|
||||||
directory to monitor for file ingestion. Note that dirwatch is not compatible with networked
|
local
|
||||||
disk.
|
}
|
||||||
</span>
|
}
|
||||||
</p>
|
directory to monitor for file ingestion. Note that dirwatch is not compatible with networked
|
||||||
<mat-form-field floatLabel="auto">
|
disk.
|
||||||
<input type="text" matInput formControlName="directory" placeholder="Directory">
|
</span>
|
||||||
<mat-error *ngIf="dirwatch.get('directory')?.hasError('required')">
|
</p>
|
||||||
Directory is required
|
<mat-form-field floatLabel="auto">
|
||||||
</mat-error>
|
<input type="text" matInput formControlName="directory" placeholder="Directory">
|
||||||
<mat-error *ngIf="dirwatch.get('directory')?.hasError('duplicate')">
|
@if (dirwatch.get('directory')?.hasError('required')) {
|
||||||
Directory is already defined
|
<mat-error>
|
||||||
</mat-error>
|
Directory is required
|
||||||
<mat-error *ngIf="dirwatch.get('directory')?.hasError('network')">
|
</mat-error>
|
||||||
Incompatible network folder
|
}
|
||||||
</mat-error>
|
@if (dirwatch.get('directory')?.hasError('duplicate')) {
|
||||||
</mat-form-field>
|
<mat-error>
|
||||||
</div>
|
Directory is already defined
|
||||||
<div class="row" *ngIf="['default','dsdplus','trunk-recorder'].includes(dirwatch.get('type')?.value)">
|
</mat-error>
|
||||||
<p>
|
}
|
||||||
<span class="mat-body">Extension</span><br>
|
@if (dirwatch.get('directory')?.hasError('network')) {
|
||||||
<span class="mat-caption">The audio call extension to monitor without the period. Ex.: "mp3",
|
<mat-error>
|
||||||
"wav".</span>
|
Incompatible network folder
|
||||||
</p>
|
</mat-error>
|
||||||
<mat-form-field floatLabel="auto">
|
}
|
||||||
<input type="text" matInput formControlName="extension" placeholder="Extension">
|
</mat-form-field>
|
||||||
<mat-error *ngIf="dirwatch.get('extension')?.hasError('invalid')">
|
</div>
|
||||||
Invalid extension
|
@if (['default','dsdplus','trunk-recorder'].includes(dirwatch.get('type')?.value)) {
|
||||||
</mat-error>
|
<div class="row">
|
||||||
</mat-form-field>
|
<p>
|
||||||
</div>
|
<span class="mat-body">Extension</span><br>
|
||||||
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)">
|
<span class="mat-caption">The audio call extension to monitor without the period. Ex.: "mp3",
|
||||||
<p>
|
"wav".</span>
|
||||||
<span class="mat-body">System</span><br>
|
</p>
|
||||||
<span class="mat-caption">System to where the audio files should go.</span>
|
<mat-form-field floatLabel="auto">
|
||||||
</p>
|
<input type="text" matInput formControlName="extension" placeholder="Extension">
|
||||||
<mat-form-field floatLabel="auto">
|
@if (dirwatch.get('extension')?.hasError('invalid')) {
|
||||||
<mat-select #sysId formControlName="systemId" placeholder="System">
|
<mat-error>
|
||||||
<mat-option [value]="null"></mat-option>
|
Invalid extension
|
||||||
<ng-container *ngFor="let system of systems">
|
</mat-error>
|
||||||
<mat-option *ngIf="system.value.id > 0" [value]="system.value.id">
|
}
|
||||||
{{ system.value.label }}
|
</mat-form-field>
|
||||||
</mat-option>
|
</div>
|
||||||
</ng-container>
|
}
|
||||||
</mat-select>
|
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
|
||||||
<mat-error *ngIf="dirwatch.get('systemId')?.hasError('required')">
|
<div class="row">
|
||||||
System is required
|
<p>
|
||||||
</mat-error>
|
<span class="mat-body">System</span><br>
|
||||||
</mat-form-field>
|
<span class="mat-caption">System to where the audio files should go.</span>
|
||||||
</div>
|
</p>
|
||||||
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)">
|
<mat-form-field floatLabel="auto">
|
||||||
<p>
|
<mat-select #sysId formControlName="systemId" placeholder="System">
|
||||||
<span class="mat-body">Site</span><br>
|
<mat-option [value]="null"></mat-option>
|
||||||
<span class="mat-caption">Site to where the audio files should go.</span>
|
@for (system of systems; track system) {
|
||||||
</p>
|
@if (system.value.id > 0) {
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-option [value]="system.value.id">
|
||||||
<mat-select formControlName="siteId" placeholder="Site">
|
{{ system.value.label }}
|
||||||
<mat-option [value]="null"></mat-option>
|
</mat-option>
|
||||||
<ng-container *ngFor="let site of sites[dirwatch.value.systemId] || []">
|
}
|
||||||
<mat-option *ngIf="site.value.id > 0" [value]="site.value.id">
|
}
|
||||||
{{ site.value.label }}
|
</mat-select>
|
||||||
</mat-option>
|
@if (dirwatch.get('systemId')?.hasError('required')) {
|
||||||
</ng-container>
|
<mat-error>
|
||||||
</mat-select>
|
System is required
|
||||||
</mat-form-field>
|
</mat-error>
|
||||||
</div>
|
}
|
||||||
<div class="row" *ngIf="['default','dsdplus'].includes(dirwatch.get('type')?.value)">
|
</mat-form-field>
|
||||||
<p>
|
</div>
|
||||||
<span class="mat-body">Talkgroup</span><br>
|
}
|
||||||
<span class="mat-caption">Talkgroup to where the audio files should go.</span>
|
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
|
||||||
</p>
|
<div class="row">
|
||||||
<mat-form-field floatLabel="auto">
|
<p>
|
||||||
<mat-select formControlName="talkgroupId" placeholder="Talkgroup">
|
<span class="mat-body">Site</span><br>
|
||||||
<mat-option [value]="null"></mat-option>
|
<span class="mat-caption">Site to where the audio files should go.</span>
|
||||||
<ng-container *ngFor="let talkgroup of talkgroups[dirwatch.value.systemId] || []">
|
</p>
|
||||||
<mat-option *ngIf="talkgroup.value.id > 0" [value]="talkgroup.value.id">
|
<mat-form-field floatLabel="auto">
|
||||||
{{ talkgroup.value.label }}
|
<mat-select formControlName="siteId" placeholder="Site">
|
||||||
</mat-option>
|
<mat-option [value]="null"></mat-option>
|
||||||
</ng-container>
|
@for (site of sites[dirwatch.value.systemId] || []; track site) {
|
||||||
</mat-select>
|
@if (site.value.id > 0) {
|
||||||
<mat-error *ngIf="dirwatch.get('talkgroupId')?.hasError('required')">
|
<mat-option [value]="site.value.id">
|
||||||
Talkgroup is required
|
{{ site.value.label }}
|
||||||
</mat-error>
|
</mat-option>
|
||||||
</mat-form-field>
|
}
|
||||||
</div>
|
}
|
||||||
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)">
|
</mat-select>
|
||||||
<p>
|
</mat-form-field>
|
||||||
<span class="mat-body">Mask</span><br>
|
</div>
|
||||||
<span class="mat-caption">Some metadata can be extracted from the file name of the audio file using
|
}
|
||||||
specific META tags. Possible META tags:
|
@if (['default','dsdplus'].includes(dirwatch.get('type')?.value)) {
|
||||||
<ul>
|
<div class="row">
|
||||||
<li><b>#DATE</b> - extract the date like 20201231 (YYYYMMMDD),
|
<p>
|
||||||
2020-12-31 (YYYY-MM-DD) or 2020_12_31 (YYYY_MM_DD).
|
<span class="mat-body">Talkgroup</span><br>
|
||||||
</li>
|
<span class="mat-caption">Talkgroup to where the audio files should go.</span>
|
||||||
<li><b>#GROUP</b> - extract the group label.</li>
|
</p>
|
||||||
<li><b>#HZ</b> - extract the frequency in hertz like 119100000.</li>
|
<mat-form-field floatLabel="auto">
|
||||||
<li><b>#KHZ</b> - extract the frequency in kilohertz like 119100.</li>
|
<mat-select formControlName="talkgroupId" placeholder="Talkgroup">
|
||||||
<li><b>#MHZ</b> - extract the frequency in megahertz like 119.100.</li>
|
<mat-option [value]="null"></mat-option>
|
||||||
<li><b>#SITE</b> - extract the site id like 1.</li>
|
@for (talkgroup of talkgroups[dirwatch.value.systemId] || []; track talkgroup) {
|
||||||
<li><b>#SITELBL</b> - extract the site label.</li>
|
@if (talkgroup.value.id > 0) {
|
||||||
<li><b>#SYS</b> - extract the system id like 11.</li>
|
<mat-option [value]="talkgroup.value.id">
|
||||||
<li><b>#SYSLBL</b> - extract the system label.</li>
|
{{ talkgroup.value.label }}
|
||||||
<li><b>#TAG</b> - extract the tag label.</li>
|
</mat-option>
|
||||||
<li><b>#TG</b> - extract the talkgroup id like 1457 in decimal format.</li>
|
}
|
||||||
<li><b>#TGAFS</b> - extract the talkgroup id like 11-061 in AFS (agency-fleet-subfleet)
|
}
|
||||||
format.</li>
|
</mat-select>
|
||||||
<li><b>#TGHZ</b> - extract the frequency in hertz like 119100000 and set the talkgroup id to
|
@if (dirwatch.get('talkgroupId')?.hasError('required')) {
|
||||||
119100.</li>
|
<mat-error>
|
||||||
<li><b>#TGKHZ</b> - extract the frequency in kilohertz like 119100.000 or 119100 and set the
|
Talkgroup is required
|
||||||
talkgroup id to 119100.</li>
|
</mat-error>
|
||||||
<li><b>#TGLBL</b> - extract the talkgroup label.</li>
|
}
|
||||||
<li><b>#TGMHZ</b> - extract the frequency in megahertz like 119.100000 or 119 and set the
|
</mat-form-field>
|
||||||
talkgroup id to 119100.</li>
|
</div>
|
||||||
<li><b>#TIME</b> - extract the local time like 0853439 (HHMMSS),
|
}
|
||||||
08-34-39 (HH-MM-SS) or 08:34:39 (HH:MM:SS).</li>
|
@if (['default'].includes(dirwatch.get('type')?.value)) {
|
||||||
<li><b>#UNIT</b> - extract the unit id like 4424001.</li>
|
<div class="row">
|
||||||
<li><b>#UNITLBL</b> - extract the unit label. Also requires #UNIT.</li>
|
<p>
|
||||||
<li><b>#ZTIME</b> - extract the zulu time like 0453439 (HHMMSS),
|
<span class="mat-body">Mask</span><br>
|
||||||
08-34-39 (HH-MM-SS) or 04:34:39 (HH:MM:SS).</li>
|
<span class="mat-caption">Some metadata can be extracted from the file name of the audio file using
|
||||||
</ul>
|
specific META tags. Possible META tags:
|
||||||
Example: cymx_#TG_#DATE_#TIME_#HZ
|
<ul>
|
||||||
</span>
|
<li><b>#DATE</b> - extract the date like 20201231 (YYYYMMMDD),
|
||||||
</p>
|
2020-12-31 (YYYY-MM-DD) or 2020_12_31 (YYYY_MM_DD).
|
||||||
<mat-form-field floatLabel="auto">
|
</li>
|
||||||
<input type="text" matInput formControlName="mask" placeholder="Mask">
|
<li><b>#GROUP</b> - extract the group label.</li>
|
||||||
<mat-error *ngIf="dirwatch.get('mask')?.hasError('invalid')">
|
<li><b>#HZ</b> - extract the frequency in hertz like 119100000.</li>
|
||||||
Invalid mask
|
<li><b>#KHZ</b> - extract the frequency in kilohertz like 119100.</li>
|
||||||
</mat-error>
|
<li><b>#MHZ</b> - extract the frequency in megahertz like 119.100.</li>
|
||||||
</mat-form-field>
|
<li><b>#SITE</b> - extract the site id like 1.</li>
|
||||||
</div>
|
<li><b>#SITELBL</b> - extract the site label.</li>
|
||||||
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)">
|
<li><b>#SYS</b> - extract the system id like 11.</li>
|
||||||
<p>
|
<li><b>#SYSLBL</b> - extract the system label.</li>
|
||||||
<span class="mat-body">Frequency</span><br>
|
<li><b>#TAG</b> - extract the tag label.</li>
|
||||||
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
|
<li><b>#TG</b> - extract the talkgroup id like 1457 in decimal format.</li>
|
||||||
</p>
|
<li><b>#TGAFS</b> - extract the talkgroup id like 11-061 in AFS (agency-fleet-subfleet)
|
||||||
<mat-form-field floatLabel="auto">
|
format.</li>
|
||||||
<input type="number" matInput formControlName="frequency" placeholder="Frequency">
|
<li><b>#TGHZ</b> - extract the frequency in hertz like 119100000 and set the talkgroup id to
|
||||||
<mat-error *ngIf="dirwatch.get('frequency')?.errors">
|
119100.</li>
|
||||||
Invalid frequency
|
<li><b>#TGKHZ</b> - extract the frequency in kilohertz like 119100.000 or 119100 and set the
|
||||||
</mat-error>
|
talkgroup id to 119100.</li>
|
||||||
</mat-form-field>
|
<li><b>#TGLBL</b> - extract the talkgroup label.</li>
|
||||||
</div>
|
<li><b>#TGMHZ</b> - extract the frequency in megahertz like 119.100000 or 119 and set the
|
||||||
<div class="row" *ngIf="['default'].includes(dirwatch.get('type')?.value)">
|
talkgroup id to 119100.</li>
|
||||||
<p>
|
<li><b>#TIME</b> - extract the local time like 0853439 (HHMMSS),
|
||||||
<span class="mat-body">Delay</span><br>
|
08-34-39 (HH-MM-SS) or 08:34:39 (HH:MM:SS).</li>
|
||||||
<span class="mat-caption">Depending on the recorder, audio files can be ingested too soon after the
|
<li><b>#UNIT</b> - extract the unit id like 4424001.</li>
|
||||||
recorder has created the file. You can set a delay value in milliseconds for the audio file to
|
<li><b>#UNITLBL</b> - extract the unit label. Also requires #UNIT.</li>
|
||||||
settle before ingesting it.</span>
|
<li><b>#ZTIME</b> - extract the zulu time like 0453439 (HHMMSS),
|
||||||
</p>
|
08-34-39 (HH-MM-SS) or 04:34:39 (HH:MM:SS).</li>
|
||||||
<mat-form-field floatLabel="auto">
|
</ul>
|
||||||
<input type="number" matInput formControlName="delay" min="2000" placeholder="Delay">
|
Example: cymx_#TG_#DATE_#TIME_#HZ
|
||||||
<mat-error *ngIf="dirwatch.get('delay')?.hasError('min')">
|
</span>
|
||||||
Delay value cannot be less than 2000 milliseconds.
|
</p>
|
||||||
</mat-error>
|
<mat-form-field floatLabel="auto">
|
||||||
</mat-form-field>
|
<input type="text" matInput formControlName="mask" placeholder="Mask">
|
||||||
</div>
|
@if (dirwatch.get('mask')?.hasError('invalid')) {
|
||||||
<div class="row bottom">
|
<mat-error>
|
||||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
Invalid mask
|
||||||
Delete dirwatch
|
</mat-error>
|
||||||
</button>
|
}
|
||||||
</div>
|
</mat-form-field>
|
||||||
</ng-container>
|
</div>
|
||||||
</mat-expansion-panel>
|
}
|
||||||
|
@if (['default'].includes(dirwatch.get('type')?.value)) {
|
||||||
|
<div class="row">
|
||||||
|
<p>
|
||||||
|
<span class="mat-body">Frequency</span><br>
|
||||||
|
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
|
||||||
|
</p>
|
||||||
|
<mat-form-field floatLabel="auto">
|
||||||
|
<input type="number" matInput formControlName="frequency" placeholder="Frequency">
|
||||||
|
@if (dirwatch.get('frequency')?.errors) {
|
||||||
|
<mat-error>
|
||||||
|
Invalid frequency
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (['default'].includes(dirwatch.get('type')?.value)) {
|
||||||
|
<div class="row">
|
||||||
|
<p>
|
||||||
|
<span class="mat-body">Delay</span><br>
|
||||||
|
<span class="mat-caption">Depending on the recorder, audio files can be ingested too soon after the
|
||||||
|
recorder has created the file. You can set a delay value in milliseconds for the audio file to
|
||||||
|
settle before ingesting it.</span>
|
||||||
|
</p>
|
||||||
|
<mat-form-field floatLabel="auto">
|
||||||
|
<input type="number" matInput formControlName="delay" min="2000" placeholder="Delay">
|
||||||
|
@if (dirwatch.get('delay')?.hasError('min')) {
|
||||||
|
<mat-error>
|
||||||
|
Delay value cannot be less than 2000 milliseconds.
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="row bottom">
|
||||||
|
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||||
|
Delete dirwatch
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
}
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|
@ -26,6 +26,7 @@ import { RdioScannerAdminService } from '../../admin.service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-dirwatch',
|
selector: 'rdio-scanner-admin-dirwatch',
|
||||||
templateUrl: './dirwatch.component.html',
|
templateUrl: './dirwatch.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminDirwatchComponent implements OnChanges {
|
export class RdioScannerAdminDirwatchComponent implements OnChanges {
|
||||||
@Input() form: FormArray | undefined;
|
@Input() form: FormArray | undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,101 @@
|
||||||
<div class="row top">
|
<div class="row top">
|
||||||
<p class="mat-body">Ingested audio calls can be sent downstream to other instances.</p>
|
<p class="mat-body">Ingested audio calls can be sent downstream to other instances.</p>
|
||||||
<button type="button" mat-button color="accent" (click)="add()">New downstream</button>
|
<button type="button" mat-button color="accent" (click)="add()">New downstream</button>
|
||||||
</div>
|
</div>
|
||||||
<p *ngIf="!downstreams.length" class="mat-small text-center">No defined downstreams</p>
|
@if (!downstreams.length) {
|
||||||
|
<p class="mat-small text-center">No defined downstreams</p>
|
||||||
|
}
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="downstreams" (cdkDropListDropped)="drop($event)">
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="downstreams" (cdkDropListDropped)="drop($event)">
|
||||||
<mat-expansion-panel *ngFor="let downstream of downstreams; index as i" cdkDrag>
|
@for (downstream of downstreams; track downstream; let i = $index) {
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel cdkDrag>
|
||||||
<mat-panel-title>
|
<mat-expansion-panel-header>
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
<mat-panel-title>
|
||||||
{{ downstream.value.url || 'NewDownstream' }}
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
<mat-icon *ngIf="downstream.invalid" color="warn">error</mat-icon>
|
{{ downstream.value.url || 'NewDownstream' }}
|
||||||
</mat-panel-title>
|
@if (downstream.invalid) {
|
||||||
</mat-expansion-panel-header>
|
<mat-icon color="warn">error</mat-icon>
|
||||||
<ng-container [formGroup]="downstream">
|
}
|
||||||
<div class="row">
|
</mat-panel-title>
|
||||||
<p>
|
</mat-expansion-panel-header>
|
||||||
<span class="mat-body">Disabled</span><br>
|
<ng-container [formGroup]="downstream">
|
||||||
<span class="mat-caption">Disable the downstream.</span>
|
<div class="row">
|
||||||
</p>
|
<p>
|
||||||
<div>
|
<span class="mat-body">Disabled</span><br>
|
||||||
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
<span class="mat-caption">Disable the downstream.</span>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<div>
|
||||||
<div class="row">
|
<mat-slide-toggle color="primary" formControlName="disabled"></mat-slide-toggle>
|
||||||
<p>
|
</div>
|
||||||
<span class="mat-body">API Key</span><br>
|
</div>
|
||||||
<span class="mat-caption">Api key of the remote instance.</span>
|
<div class="row">
|
||||||
</p>
|
<p>
|
||||||
<mat-form-field floatLabel="auto">
|
<span class="mat-body">API Key</span><br>
|
||||||
<input #key type="text" matInput formControlName="apikey" placeholder="API key">
|
<span class="mat-caption">Api key of the remote instance.</span>
|
||||||
<mat-error *ngIf="downstream.get('apikey')?.hasError('duplicate')">
|
</p>
|
||||||
API key is already defined
|
<mat-form-field floatLabel="auto">
|
||||||
</mat-error>
|
<input #key type="text" matInput formControlName="apikey" placeholder="API key">
|
||||||
<mat-error *ngIf="downstream.get('apikey')?.hasError('required')">
|
@if (downstream.get('apikey')?.hasError('duplicate')) {
|
||||||
API key is required
|
<mat-error>
|
||||||
</mat-error>
|
API key is already defined
|
||||||
</mat-form-field>
|
</mat-error>
|
||||||
</div>
|
}
|
||||||
<div class="row">
|
@if (downstream.get('apikey')?.hasError('required')) {
|
||||||
<p>
|
<mat-error>
|
||||||
<span class="mat-body">URL</span><br>
|
API key is required
|
||||||
<span class="mat-caption">URL of the remote instance.</span>
|
</mat-error>
|
||||||
</p>
|
}
|
||||||
<mat-form-field floatLabel="auto">
|
</mat-form-field>
|
||||||
<input type="text" matInput formControlName="url" placeholder="URL">
|
</div>
|
||||||
<mat-error *ngIf="downstream.get('url')?.hasError('required')">
|
<div class="row">
|
||||||
URL is required
|
<p>
|
||||||
</mat-error>
|
<span class="mat-body">URL</span><br>
|
||||||
<mat-error *ngIf="downstream.get('url')?.hasError('invalid')">
|
<span class="mat-caption">URL of the remote instance.</span>
|
||||||
URL is invalid
|
</p>
|
||||||
</mat-error>
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-error *ngIf="downstream.get('url')?.hasError('duplicate')">
|
<input type="text" matInput formControlName="url" placeholder="URL">
|
||||||
URL is already defined
|
@if (downstream.get('url')?.hasError('required')) {
|
||||||
</mat-error>
|
<mat-error>
|
||||||
</mat-form-field>
|
URL is required
|
||||||
</div>
|
</mat-error>
|
||||||
<div class="row">
|
}
|
||||||
<p>
|
@if (downstream.get('url')?.hasError('invalid')) {
|
||||||
<span class="mat-body">Access</span><br>
|
<mat-error>
|
||||||
<span class="mat-caption">
|
URL is invalid
|
||||||
This downstream allows access to <u>
|
</mat-error>
|
||||||
<ng-container *ngIf="downstream.value.systems === '*'">all</ng-container>
|
}
|
||||||
<ng-container *ngIf="downstream.value.systems !== '*'">some</ng-container>
|
@if (downstream.get('url')?.hasError('duplicate')) {
|
||||||
</u> systems and talkgroups.
|
<mat-error>
|
||||||
</span>
|
URL is already defined
|
||||||
</p>
|
</mat-error>
|
||||||
<div>
|
}
|
||||||
<button type="button" mat-button [disabled]="downstream.disabled" (click)="select(downstream)">
|
</mat-form-field>
|
||||||
Choose systems
|
</div>
|
||||||
</button>
|
<div class="row">
|
||||||
</div>
|
<p>
|
||||||
</div>
|
<span class="mat-body">Access</span><br>
|
||||||
<div class="row bottom">
|
<span class="mat-caption">
|
||||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
This downstream allows access to <u>
|
||||||
Delete downstream
|
@if (downstream.value.systems === '*') {
|
||||||
</button>
|
all
|
||||||
</div>
|
}
|
||||||
</ng-container>
|
@if (downstream.value.systems !== '*') {
|
||||||
</mat-expansion-panel>
|
some
|
||||||
|
}
|
||||||
|
</u> systems and talkgroups.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<button type="button" mat-button [disabled]="downstream.disabled" (click)="select(downstream)">
|
||||||
|
Choose systems
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row bottom">
|
||||||
|
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||||
|
Delete downstream
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</mat-expansion-panel>
|
||||||
|
}
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|
@ -28,6 +28,7 @@ import { RdioScannerAdminSystemsSelectComponent } from '../systems/select/select
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-downstreams',
|
selector: 'rdio-scanner-admin-downstreams',
|
||||||
templateUrl: './downstreams.component.html',
|
templateUrl: './downstreams.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminDownstreamsComponent {
|
export class RdioScannerAdminDownstreamsComponent {
|
||||||
@Input() form: FormArray | undefined;
|
@Input() form: FormArray | undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,68 @@
|
||||||
<div class="row top">
|
<div class="row top">
|
||||||
<p class="mat-body">All system talkgroups must be associated with a group, which is then used to toggle between
|
<p class="mat-body">All system talkgroups must be associated with a group, which is then used to toggle between
|
||||||
active talkgroups.</p>
|
active talkgroups.</p>
|
||||||
<button type="button" mat-button color="accent" (click)="add()">New Group</button>
|
<button type="button" mat-button color="accent" (click)="add()">New Group</button>
|
||||||
</div>
|
</div>
|
||||||
<p *ngIf="!groups?.length" class="mat-small text-center">No defined Groups</p>
|
<p *ngIf="!groups.length" class="mat-small text-center">No defined Groups</p>
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="groups"
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="groups"
|
||||||
(cdkDropListDropped)="drop($event)">
|
(cdkDropListDropped)="drop($event)">
|
||||||
<mat-expansion-panel *ngFor="let group of groups; index as i" cdkDrag>
|
<mat-expansion-panel *ngFor="let group of groups; let i = index" cdkDrag>
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
{{ group.value.label || 'NewGroup' }}
|
{{ group.value.label || 'NewGroup' }}
|
||||||
<mat-icon *ngIf="group.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="group.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<ng-container [formGroup]="group">
|
<ng-container [formGroup]="group">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Label</span><br>
|
<span class="mat-body">Label</span><br>
|
||||||
<span class="mat-caption">Group label displayed on the main screen, on the search panel and on the
|
<span class="mat-caption">Group label displayed on the main screen, on the search panel and on the
|
||||||
select panel.</span>
|
select panel.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
<mat-error *ngIf="group.get('label')!.hasError('required')">
|
||||||
Label is required
|
Label is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Audio alert</span><br>
|
<span class="mat-body">Audio alert</span><br>
|
||||||
<span class="mat-caption">Prefixes an alert sound to all calls assigned to this group. The alert
|
<span class="mat-caption">Prefixes an alert sound to all calls assigned to this group. The alert
|
||||||
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
|
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
|
||||||
<mat-option value="">No Alert</mat-option>
|
<mat-option value="">No Alert</mat-option>
|
||||||
<mat-option *ngFor="let alert of alerts" [value]="alert">
|
<mat-option *ngFor="let alert of alerts" [value]="alert">
|
||||||
{{ alert | titlecase }}
|
{{ alert | titlecase }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Led Color</span><br>
|
<span class="mat-body">Led Color</span><br>
|
||||||
<span class="mat-caption">The default LED color is green but it can be changed at several levels.
|
<span class="mat-caption">The default LED color is green but it can be changed at several levels.
|
||||||
The color assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
The color assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="led" placeholder="Color">
|
<mat-select formControlName="led" placeholder="Color">
|
||||||
<mat-option value="">Default</mat-option>
|
<mat-option value="">Default</mat-option>
|
||||||
<mat-option *ngFor="let led of leds" [value]="led">
|
<mat-option *ngFor="let led of leds" [value]="led">
|
||||||
{{ led | titlecase }}
|
{{ led | titlecase }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row bottom">
|
<div class="row bottom">
|
||||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||||
Delete Group
|
Delete Group
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|
@ -18,35 +18,60 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { Component, Input, QueryList, ViewChildren } from '@angular/core';
|
import { Component, Input, QueryList, ViewChildren, OnDestroy } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { FormArray, FormGroup } from '@angular/forms';
|
import { FormArray, FormGroup } from '@angular/forms';
|
||||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||||
import { MatSelectChange } from '@angular/material/select';
|
import { MatSelectChange } from '@angular/material/select';
|
||||||
import { RdioScannerAdminService } from '../../admin.service';
|
import { RdioScannerAdminService } from '../../admin.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-groups',
|
selector: 'rdio-scanner-admin-groups',
|
||||||
templateUrl: './groups.component.html',
|
templateUrl: './groups.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminGroupsComponent {
|
export class RdioScannerAdminGroupsComponent implements OnDestroy {
|
||||||
@Input() form: FormArray | undefined;
|
private _form: FormArray | undefined;
|
||||||
|
private formSub: Subscription | undefined;
|
||||||
|
private cachedGroups: FormGroup[] | undefined;
|
||||||
|
private cacheValid = false;
|
||||||
|
readonly leds: string[] = [];
|
||||||
|
readonly alertsArray: string[] = [];
|
||||||
|
|
||||||
leds: string[];
|
@Input()
|
||||||
|
set form(value: FormArray | undefined) {
|
||||||
|
this._form = value;
|
||||||
|
this.cacheValid = false;
|
||||||
|
this.cachedGroups = undefined;
|
||||||
|
this.formSub?.unsubscribe();
|
||||||
|
if (this._form) {
|
||||||
|
this.formSub = this._form.valueChanges.subscribe(() => {
|
||||||
|
this.cacheValid = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get form(): FormArray | undefined {
|
||||||
|
return this._form;
|
||||||
|
}
|
||||||
|
|
||||||
get alerts(): string[] {
|
get alerts(): string[] {
|
||||||
return Object.keys(this.adminService.Alerts || {});
|
return this.alertsArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
get groups(): FormGroup[] {
|
get groups(): FormGroup[] {
|
||||||
return this.form?.controls
|
if (this.cacheValid && this.cachedGroups) return this.cachedGroups;
|
||||||
.sort((a, b) => a.value.order - b.value.order) as FormGroup[];
|
const controls = (this._form?.controls?.slice() || []) as FormGroup[];
|
||||||
|
this.cachedGroups = controls.sort((a, b) => (a.value.order || 0) - (b.value.order || 0));
|
||||||
|
this.cacheValid = true;
|
||||||
|
return this.cachedGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||||
|
|
||||||
constructor(private adminService: RdioScannerAdminService, private matDialog: MatDialog) {
|
constructor(private adminService: RdioScannerAdminService, private matDialog: MatDialog) {
|
||||||
this.leds = this.adminService.getLeds();
|
this.leds = this.adminService.getLeds();
|
||||||
|
this.alertsArray = Object.keys(this.adminService.Alerts || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
add(): void {
|
add(): void {
|
||||||
|
|
@ -57,6 +82,7 @@ export class RdioScannerAdminGroupsComponent {
|
||||||
this.form?.insert(0, group);
|
this.form?.insert(0, group);
|
||||||
|
|
||||||
this.form?.markAsDirty();
|
this.form?.markAsDirty();
|
||||||
|
this.cacheValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAll(): void {
|
closeAll(): void {
|
||||||
|
|
@ -70,6 +96,7 @@ export class RdioScannerAdminGroupsComponent {
|
||||||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||||
|
|
||||||
this.form?.markAsDirty();
|
this.form?.markAsDirty();
|
||||||
|
this.cacheValid = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,5 +108,10 @@ export class RdioScannerAdminGroupsComponent {
|
||||||
this.form?.removeAt(index);
|
this.form?.removeAt(index);
|
||||||
|
|
||||||
this.form?.markAsDirty();
|
this.form?.markAsDirty();
|
||||||
|
this.cacheValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.formSub?.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,166 +1,168 @@
|
||||||
<ng-container *ngIf="form" [formGroup]="form">
|
<ng-container *ngIf="form">
|
||||||
|
<ng-container [formGroup]="form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">12-Hour Time Format</span><br>
|
<span class="mat-body">12-Hour Time Format</span><br>
|
||||||
<span class="mat-caption">Display time stamp in 12-hour format.</span>
|
<span class="mat-caption">Display time stamp in 12-hour format.</span>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<mat-slide-toggle color="primary" formControlName="time12hFormat"></mat-slide-toggle>
|
<mat-slide-toggle color="primary" formControlName="time12hFormat"></mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Audio Conversion</span><br>
|
<span class="mat-body">Audio Conversion</span><br>
|
||||||
<span class="mat-caption">Convert incoming audio files to H.264 m4a with ffmpeg.</span>
|
<span class="mat-caption">Convert incoming audio files to H.264 m4a with ffmpeg.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="audioConversion" placeholder="Audio Conversion">
|
<mat-select formControlName="audioConversion" placeholder="Audio Conversion">
|
||||||
<mat-option [value]="0">Disabled</mat-option>
|
<mat-option [value]="0">Disabled</mat-option>
|
||||||
<mat-option [value]="1">Enabled without normalization</mat-option>
|
<mat-option [value]="1">Enabled without normalization</mat-option>
|
||||||
<mat-option [value]="2">Enabled with normalization</mat-option>
|
<mat-option [value]="2">Enabled with normalization</mat-option>
|
||||||
<mat-option [value]="3">Enabled with loud normalization</mat-option>
|
<mat-option [value]="3">Enabled with loud normalization</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Auto Populate</span><br>
|
<span class="mat-body">Auto Populate</span><br>
|
||||||
<span class="mat-caption">Globaly allows the automatic creation of unconfigured systems and
|
<span class="mat-caption">Globally allows the automatic creation of unconfigured systems and
|
||||||
talkgroups.</span>
|
talkgroups.</span>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<mat-slide-toggle color="primary" formControlName="autoPopulate"></mat-slide-toggle>
|
<mat-slide-toggle color="primary" formControlName="autoPopulate"></mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Branding Label</span><br>
|
<span class="mat-body">Branding Label</span><br>
|
||||||
<span class="mat-caption">A simple line of text displayed above the main screen to help identify the Rdio
|
<span class="mat-caption">A simple line of text displayed above the main screen to help identify the Rdio
|
||||||
Scanner instance.</span>
|
Scanner instance.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="text" matInput formControlName="branding" placeholder="Branding">
|
<input type="text" matInput formControlName="branding" placeholder="Branding">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Dimmer Delay</span><br>
|
<span class="mat-body">Dimmer Delay</span><br>
|
||||||
<span class="mat-caption">Delay in milliseconds before turning off the screen backlight when
|
<span class="mat-caption">Delay in milliseconds before turning off the screen backlight when
|
||||||
inactive.</span>
|
inactive.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input type="number" min="0" step="1" matInput formControlName="dimmerDelay">
|
<input type="number" min="0" step="1" matInput formControlName="dimmerDelay">
|
||||||
<mat-error *ngIf="form?.get('dimmerDelay')?.hasError('required')">
|
<mat-error *ngIf="form.get('dimmerDelay')?.hasError('required')">
|
||||||
Dimmer delay is required
|
Dimmer delay is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="form?.get('dimmerDelay')?.hasError('min')">
|
<mat-error *ngIf="form.get('dimmerDelay')?.hasError('min')">
|
||||||
Dimmer delay is invalid
|
Dimmer delay is invalid
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Disable Duplicate Call Detection</span><br>
|
<span class="mat-body">Disable Duplicate Call Detection</span><br>
|
||||||
<span class="mat-caption">Disable duplicate audio file detection.</span>
|
<span class="mat-caption">Disable duplicate audio file detection.</span>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<mat-slide-toggle color="primary" formControlName="disableDuplicateDetection"></mat-slide-toggle>
|
<mat-slide-toggle color="primary" formControlName="disableDuplicateDetection"></mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Duplicate Call Detection Time Frame</span><br>
|
<span class="mat-body">Duplicate Call Detection Time Frame</span><br>
|
||||||
<span class="mat-caption">New incoming calls will be rejected if a call already exists in the database with
|
<span class="mat-caption">New incoming calls will be rejected if a call already exists in the database with
|
||||||
a start time of +/- this delay in milliseconds.</span>
|
a start time of +/- this delay in milliseconds.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input type="number" min="0" step="1" matInput formControlName="duplicateDetectionTimeFrame">
|
<input type="number" min="0" step="1" matInput formControlName="duplicateDetectionTimeFrame">
|
||||||
<mat-error *ngIf="form?.get('duplicateDetectionTimeFrame')?.hasError('required')">
|
<mat-error *ngIf="form.get('duplicateDetectionTimeFrame')?.hasError('required')">
|
||||||
Duplicate call time frame is required
|
Duplicate call time frame is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="form?.get('duplicateDetectionTimeFrame')?.hasError('min')">
|
<mat-error *ngIf="form.get('duplicateDetectionTimeFrame')?.hasError('min')">
|
||||||
Duplicate call time frame is invalid
|
Duplicate call time frame is invalid
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Email Support</span><br>
|
<span class="mat-body">Email Support</span><br>
|
||||||
<span class="mat-caption">Email address where users can write to to get support.</span>
|
<span class="mat-caption">Email address where users can write to to get support.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="text" matInput formControlName="email" placeholder="Email">
|
<input type="text" matInput formControlName="email" placeholder="Email">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Keypad Beep Style</span><br>
|
<span class="mat-body">Keypad Beep Style</span><br>
|
||||||
<span class="mat-caption">Provide audio feedback when pressing buttons.</span>
|
<span class="mat-caption">Provide audio feedback when pressing buttons.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="keypadBeeps" placeholder="Style">
|
<mat-select formControlName="keypadBeeps" placeholder="Style">
|
||||||
<mat-option value="uniden">Uniden</mat-option>
|
<mat-option value="uniden">Uniden</mat-option>
|
||||||
<mat-option value="whistler">Whistler</mat-option>
|
<mat-option value="whistler">Whistler</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Max Clients</span><br>
|
<span class="mat-body">Max Clients</span><br>
|
||||||
<span class="mat-caption">Max number of simultaneous clients.</span>
|
<span class="mat-caption">Max number of simultaneous clients.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input type="number" min="0" step="1" matInput formControlName="maxClients">
|
<input type="number" min="0" step="1" matInput formControlName="maxClients">
|
||||||
<mat-error *ngIf="form?.get('maxClients')?.hasError('required')">
|
<mat-error *ngIf="form.get('maxClients')?.hasError('required')">
|
||||||
Max clients is required
|
Max clients is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="form?.get('maxClients')?.hasError('min')">
|
<mat-error *ngIf="form.get('maxClients')?.hasError('min')">
|
||||||
Max clients is invalid
|
Max clients is invalid
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Playback Mode Goes Live</span><br>
|
<span class="mat-body">Playback Mode Goes Live</span><br>
|
||||||
<span class="mat-caption">Playback mode changes to live stream mode when the search list is
|
<span class="mat-caption">Playback mode changes to live stream mode when the search list is
|
||||||
exhausted.</span>
|
exhausted.</span>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<mat-slide-toggle color="primary" formControlName="playbackGoesLive"></mat-slide-toggle>
|
<mat-slide-toggle color="primary" formControlName="playbackGoesLive"></mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Prune Days</span><br>
|
<span class="mat-body">Prune Days</span><br>
|
||||||
<span class="mat-caption">Prune the database for data older than the specified number of days. Set to 0 to
|
<span class="mat-caption">Prune the database for data older than the specified number of days. Set to 0 to
|
||||||
disable.</span>
|
disable.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<input type="number" min="0" step="1" matInput formControlName="pruneDays">
|
<input type="number" min="0" step="1" matInput formControlName="pruneDays">
|
||||||
<mat-error *ngIf="form?.get('pruneDays')?.hasError('required')">
|
<mat-error *ngIf="form.get('pruneDays')?.hasError('required')">
|
||||||
Prune days is required
|
Prune days is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="form?.get('pruneDays')?.hasError('min')">
|
<mat-error *ngIf="form.get('pruneDays')?.hasError('min')">
|
||||||
Prune days is invalid
|
Prune days is invalid
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Show Listeners Count</span><br>
|
<span class="mat-body">Show Listeners Count</span><br>
|
||||||
<span class="mat-caption">Show listeners count on main screen.</span>
|
<span class="mat-caption">Show listeners count on main screen.</span>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<mat-slide-toggle color="primary" formControlName="showListenersCount"></mat-slide-toggle>
|
<mat-slide-toggle color="primary" formControlName="showListenersCount"></mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Sort Talkgroups</span><br>
|
<span class="mat-body">Sort Talkgroups</span><br>
|
||||||
<span class="mat-caption">Automatically sort talkgroups by their id.</span>
|
<span class="mat-caption">Automatically sort talkgroups by their id.</span>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<mat-slide-toggle color="primary" formControlName="sortTalkgroups"></mat-slide-toggle>
|
<mat-slide-toggle color="primary" formControlName="sortTalkgroups"></mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
@ -17,13 +17,19 @@
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-options',
|
selector: 'rdio-scanner-admin-options',
|
||||||
templateUrl: './options.component.html',
|
templateUrl: './options.component.html',
|
||||||
|
standalone: false,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminOptionsComponent {
|
export class RdioScannerAdminOptionsComponent {
|
||||||
@Input() form: FormGroup | undefined;
|
@Input() form: FormGroup | null = null;
|
||||||
|
|
||||||
|
get controls() {
|
||||||
|
return this.form ? this.form.controls : {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,57 @@
|
||||||
<div>
|
<div>
|
||||||
<div mat-dialog-title>Systems and talkgroups selection</div>
|
<div mat-dialog-title>Systems and talkgroups selection</div>
|
||||||
<div [formGroup]="select" mat-dialog-content>
|
<div [formGroup]="select" mat-dialog-content>
|
||||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="all"
|
<mat-checkbox color="primary" [disableRipple]="true" formControlName="all"
|
||||||
[indeterminate]="indeterminate.everything">
|
[indeterminate]="indeterminate.everything">
|
||||||
Everything
|
Everything
|
||||||
</mat-checkbox>
|
</mat-checkbox>
|
||||||
<h3 class="mat-caption">Groups</h3>
|
<h3 class="mat-caption">Groups</h3>
|
||||||
<div class="flex-row wrap" formArrayName="groups">
|
<div class="flex-row wrap" formArrayName="groups">
|
||||||
<div *ngFor="let group of configGroups; index as groupIndex" [formGroupName]="groupIndex">
|
<ng-container *ngFor="let group of configGroups; let groupIndex = index">
|
||||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"
|
<div [formGroupName]="groupIndex">
|
||||||
[indeterminate]="indeterminate.groups[groupIndex]">
|
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"
|
||||||
{{ group.value.label }}
|
[indeterminate]="indeterminate.groups[groupIndex]">
|
||||||
</mat-checkbox>
|
{{ group.value.label }}
|
||||||
</div>
|
</mat-checkbox>
|
||||||
</div>
|
|
||||||
<h3 class="mat-caption">Tags</h3>
|
|
||||||
<div class="flex-row wrap" formArrayName="tags">
|
|
||||||
<div *ngFor="let tag of configTags; index as tagIndex" [formGroupName]="tagIndex">
|
|
||||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"
|
|
||||||
[indeterminate]="indeterminate.tags[tagIndex]">
|
|
||||||
{{ tag.value.label }}
|
|
||||||
</mat-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h3 class="mat-caption">Systems</h3>
|
|
||||||
<div formArrayName="systems">
|
|
||||||
<div *ngFor="let system of configSystems; index as systemIndex" [formGroupName]="systemIndex">
|
|
||||||
<div class="flex-row margin">
|
|
||||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="all"
|
|
||||||
[indeterminate]="indeterminate.systems[systemIndex]">
|
|
||||||
{{ system.value.label }}
|
|
||||||
</mat-checkbox>
|
|
||||||
</div>
|
|
||||||
<div class="flex-row wrap indent margin" formArrayName="talkgroups">
|
|
||||||
<div *ngFor="let talkgroup of configTalkgroups[systemIndex]; index as talkgroupIndex"
|
|
||||||
[formGroupName]="talkgroupIndex">
|
|
||||||
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked">
|
|
||||||
{{ talkgroup.value.label }}
|
|
||||||
</mat-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div mat-dialog-actions>
|
<h3 class="mat-caption">Tags</h3>
|
||||||
<button mat-raised-button (click)="cancel()">Cancel</button>
|
<div class="flex-row wrap" formArrayName="tags">
|
||||||
<button mat-raised-button color="primary" (click)="accept()">Ok</button>
|
<ng-container *ngFor="let tag of configTags; let tagIndex = index">
|
||||||
|
<div [formGroupName]="tagIndex">
|
||||||
|
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked"
|
||||||
|
[indeterminate]="indeterminate.tags[tagIndex]">
|
||||||
|
{{ tag.value.label }}
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="mat-caption">Systems</h3>
|
||||||
|
<div formArrayName="systems">
|
||||||
|
<ng-container *ngFor="let system of configSystems; let systemIndex = index">
|
||||||
|
<div [formGroupName]="systemIndex">
|
||||||
|
<div class="flex-row margin">
|
||||||
|
<mat-checkbox color="primary" [disableRipple]="true" formControlName="all"
|
||||||
|
[indeterminate]="indeterminate.systems[systemIndex]">
|
||||||
|
{{ system.value.label }}
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row wrap indent margin" formArrayName="talkgroups">
|
||||||
|
<ng-container *ngFor="let talkgroup of configTalkgroups[systemIndex]; let talkgroupIndex = index">
|
||||||
|
<div [formGroupName]="talkgroupIndex">
|
||||||
|
<mat-checkbox color="primary" [disableRipple]="true" formControlName="checked">
|
||||||
|
{{ talkgroup.value.label }}
|
||||||
|
</mat-checkbox>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div mat-dialog-actions>
|
||||||
|
<button mat-raised-button (click)="cancel()">Cancel</button>
|
||||||
|
<button mat-raised-button color="primary" (click)="accept()">Ok</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -17,10 +17,11 @@
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, Inject, ViewEncapsulation } from '@angular/core';
|
import { Component, Inject, ViewEncapsulation, OnDestroy } from '@angular/core';
|
||||||
import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
import { FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { Access } from '../../../admin.service';
|
import { Access } from '../../../admin.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
interface System {
|
interface System {
|
||||||
all: boolean;
|
all: boolean;
|
||||||
|
|
@ -38,8 +39,9 @@ interface Talkgroup {
|
||||||
selector: 'rdio-scanner-admin-systems-selection',
|
selector: 'rdio-scanner-admin-systems-selection',
|
||||||
styleUrls: ['./select.component.scss'],
|
styleUrls: ['./select.component.scss'],
|
||||||
templateUrl: './select.component.html',
|
templateUrl: './select.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminSystemsSelectComponent {
|
export class RdioScannerAdminSystemsSelectComponent implements OnDestroy {
|
||||||
indeterminate = {
|
indeterminate = {
|
||||||
everything: false,
|
everything: false,
|
||||||
groups: [] as boolean[],
|
groups: [] as boolean[],
|
||||||
|
|
@ -53,22 +55,21 @@ export class RdioScannerAdminSystemsSelectComponent {
|
||||||
|
|
||||||
get configGroups(): FormGroup[] {
|
get configGroups(): FormGroup[] {
|
||||||
const faGroups = this.access.root.get('groups') as FormArray;
|
const faGroups = this.access.root.get('groups') as FormArray;
|
||||||
|
|
||||||
return faGroups.controls as FormGroup[];
|
return faGroups.controls as FormGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
get configSystems(): FormGroup[] {
|
get configSystems(): FormGroup[] {
|
||||||
const faSystems = this.access.root.get('systems') as FormArray;
|
const faSystems = this.access.root.get('systems') as FormArray;
|
||||||
|
|
||||||
return faSystems.controls as FormGroup[];
|
return faSystems.controls as FormGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
get configTags(): FormGroup[] {
|
get configTags(): FormGroup[] {
|
||||||
const faTags = this.access.root.get('tags') as FormArray;
|
const faTags = this.access.root.get('tags') as FormArray;
|
||||||
|
|
||||||
return faTags.controls as FormGroup[];
|
return faTags.controls as FormGroup[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private subs = new Subscription();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(MAT_DIALOG_DATA) public access: FormGroup,
|
@Inject(MAT_DIALOG_DATA) public access: FormGroup,
|
||||||
private matDialogRef: MatDialogRef<RdioScannerAdminSystemsSelectComponent>,
|
private matDialogRef: MatDialogRef<RdioScannerAdminSystemsSelectComponent>,
|
||||||
|
|
@ -76,7 +77,6 @@ export class RdioScannerAdminSystemsSelectComponent {
|
||||||
) {
|
) {
|
||||||
this.configTalkgroups = this.configSystems.map((fgSystem) => {
|
this.configTalkgroups = this.configSystems.map((fgSystem) => {
|
||||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||||
|
|
||||||
return faTalkgroups.controls as FormGroup[];
|
return faTalkgroups.controls as FormGroup[];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -92,179 +92,140 @@ export class RdioScannerAdminSystemsSelectComponent {
|
||||||
const faSystems = this.select.get('systems') as FormArray;
|
const faSystems = this.select.get('systems') as FormArray;
|
||||||
const faTags = this.select.get('tags') as FormArray;
|
const faTags = this.select.get('tags') as FormArray;
|
||||||
|
|
||||||
this.configGroups.forEach((configGroup) => {
|
for (const configGroup of this.configGroups) {
|
||||||
const fgGroup = this.ngFormBuilder.group({
|
const fgGroup = this.ngFormBuilder.group({
|
||||||
id: this.ngFormBuilder.control(configGroup.get('id')?.value),
|
id: this.ngFormBuilder.control(configGroup.get('id')?.value),
|
||||||
checked: this.ngFormBuilder.control(false),
|
checked: this.ngFormBuilder.control(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
faGroups.push(fgGroup);
|
faGroups.push(fgGroup);
|
||||||
|
this.subs.add(fgGroup.valueChanges.subscribe((vGroup) => {
|
||||||
fgGroup.valueChanges.subscribe((vGroup) => {
|
for (const fgSystem of faSystems.controls) {
|
||||||
faSystems.controls.forEach((fgSystem) => {
|
|
||||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||||
|
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
const ids = fgTalkgroup.get('groupIds')?.value;
|
||||||
if (fgTalkgroup.get('groupIds')?.value.includes(vGroup.id) && fgTalkgroup.get('checked')?.value !== vGroup.checked) {
|
if (ids && ids.includes(vGroup.id) && fgTalkgroup.get('checked')?.value !== vGroup.checked) {
|
||||||
fgTalkgroup.get('checked')?.setValue(vGroup.checked);
|
fgTalkgroup.get('checked')?.setValue(vGroup.checked);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
});
|
}));
|
||||||
});
|
}
|
||||||
|
|
||||||
this.configSystems.forEach((configSystem, index) => {
|
for (let index = 0; index < this.configSystems.length; index++) {
|
||||||
|
const configSystem = this.configSystems[index];
|
||||||
const fcSystemAll = this.ngFormBuilder.control(false);
|
const fcSystemAll = this.ngFormBuilder.control(false);
|
||||||
|
const faSystemTalkgroups = this.ngFormBuilder.array<FormGroup>([]);
|
||||||
const faSystemTalkgroups = this.ngFormBuilder.array<FormGroup<{
|
|
||||||
checked: FormControl<boolean>;
|
|
||||||
groupIds: FormControl<number[]>;
|
|
||||||
id: FormControl<number>;
|
|
||||||
tagId: FormControl<number>;
|
|
||||||
}>>([]);
|
|
||||||
|
|
||||||
const fgSystem = this.ngFormBuilder.group({
|
const fgSystem = this.ngFormBuilder.group({
|
||||||
all: fcSystemAll,
|
all: fcSystemAll,
|
||||||
id: this.ngFormBuilder.control(configSystem.get('systemRef')?.value),
|
id: this.ngFormBuilder.control(configSystem.get('systemRef')?.value),
|
||||||
talkgroups: faSystemTalkgroups
|
talkgroups: faSystemTalkgroups
|
||||||
});
|
});
|
||||||
|
|
||||||
this.configTalkgroups[index].forEach((configTalkgroup) => {
|
for (const configTalkgroup of this.configTalkgroups[index]) {
|
||||||
const fgSystemTalkgroup = this.ngFormBuilder.group({
|
const fgSystemTalkgroup = this.ngFormBuilder.group({
|
||||||
checked: this.ngFormBuilder.nonNullable.control(false),
|
checked: this.ngFormBuilder.nonNullable.control(false),
|
||||||
groupIds: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('groupIds')?.value),
|
groupIds: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('groupIds')?.value),
|
||||||
id: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('talkgroupRef')?.value),
|
id: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('talkgroupRef')?.value),
|
||||||
tagId: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('tagId')?.value),
|
tagId: this.ngFormBuilder.nonNullable.control(configTalkgroup.get('tagId')?.value),
|
||||||
});
|
});
|
||||||
|
|
||||||
faSystemTalkgroups.push(fgSystemTalkgroup);
|
faSystemTalkgroups.push(fgSystemTalkgroup);
|
||||||
|
this.subs.add(fgSystemTalkgroup.valueChanges.subscribe(() => {
|
||||||
fgSystemTalkgroup.valueChanges.subscribe(() => {
|
const vAll = faSystemTalkgroups.controls.every((t) => t.get('checked')?.value);
|
||||||
const vAll = faSystemTalkgroups.controls.every((systemTalkgroup) => systemTalkgroup.get('checked')?.value);
|
|
||||||
|
|
||||||
fcSystemAll.setValue(vAll, { emitEvent: false });
|
fcSystemAll.setValue(vAll, { emitEvent: false });
|
||||||
});
|
}));
|
||||||
});
|
}
|
||||||
|
|
||||||
faSystems.push(fgSystem);
|
faSystems.push(fgSystem);
|
||||||
|
|
||||||
fgSystem.valueChanges.subscribe(() => {
|
this.subs.add(fgSystem.valueChanges.subscribe(() => {
|
||||||
this.rebuildGroupIndeterminates();
|
this.rebuildGroupIndeterminates();
|
||||||
this.rebuildTagIndeterminates();
|
this.rebuildTagIndeterminates();
|
||||||
});
|
}));
|
||||||
|
|
||||||
fcSystemAll.valueChanges.subscribe((vAll) => {
|
this.subs.add(fcSystemAll.valueChanges.subscribe((vAll) => {
|
||||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||||
|
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||||
faTalkgroups.controls.forEach((fgTalkgroup) => fgTalkgroup.get('checked')?.setValue(vAll));
|
fgTalkgroup.get('checked')?.setValue(vAll);
|
||||||
|
}
|
||||||
this.rebuildGroupIndeterminates();
|
this.rebuildGroupIndeterminates();
|
||||||
this.rebuildTagIndeterminates();
|
this.rebuildTagIndeterminates();
|
||||||
});
|
}));
|
||||||
|
|
||||||
faSystemTalkgroups.valueChanges.subscribe((vSystemTalkgroups) => {
|
this.subs.add(faSystemTalkgroups.valueChanges.subscribe((vSystemTalkgroups) => {
|
||||||
let off = 0;
|
|
||||||
let on = 0;
|
let on = 0;
|
||||||
|
let off = 0;
|
||||||
vSystemTalkgroups.forEach((vSystemTalkgroup) => {
|
for (const v of vSystemTalkgroups) {
|
||||||
if (vSystemTalkgroup.checked) {
|
if (v.checked) on++; else off++;
|
||||||
on++;
|
}
|
||||||
|
|
||||||
} else {
|
|
||||||
off++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.indeterminate.systems[index] = !!off && !!on;
|
this.indeterminate.systems[index] = !!off && !!on;
|
||||||
|
|
||||||
faSystems.at(index).get('all')?.setValue(!off && on, { emitEvent: false });
|
faSystems.at(index).get('all')?.setValue(!off && on, { emitEvent: false });
|
||||||
});
|
}));
|
||||||
});
|
}
|
||||||
|
|
||||||
this.configTags.forEach((configTag) => {
|
for (const configTag of this.configTags) {
|
||||||
const fgTag = this.ngFormBuilder.group({
|
const fgTag = this.ngFormBuilder.group({
|
||||||
id: this.ngFormBuilder.control(configTag.value.id),
|
id: this.ngFormBuilder.control(configTag.value.id),
|
||||||
checked: this.ngFormBuilder.control(false),
|
checked: this.ngFormBuilder.control(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
faTags.push(fgTag);
|
faTags.push(fgTag);
|
||||||
|
this.subs.add(fgTag.valueChanges.subscribe((vTag) => {
|
||||||
fgTag.valueChanges.subscribe((vTag) => {
|
for (const fgSystem of faSystems.controls) {
|
||||||
faSystems.controls.forEach((fgSystem) => {
|
|
||||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||||
|
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
|
||||||
if (fgTalkgroup.value.tagId === vTag.id && fgTalkgroup.value.checked !== vTag.checked) {
|
if (fgTalkgroup.value.tagId === vTag.id && fgTalkgroup.value.checked !== vTag.checked) {
|
||||||
fgTalkgroup.get('checked')?.setValue(vTag.checked);
|
fgTalkgroup.get('checked')?.setValue(vTag.checked);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
fcAll.valueChanges.subscribe((vAll) => {
|
|
||||||
faSystems.controls.flatMap((fgSystem) => {
|
|
||||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
|
||||||
|
|
||||||
return faTalkgroups.controls;
|
|
||||||
}).concat(faGroups.controls, faTags.controls).forEach((control) => {
|
|
||||||
control.get('checked')?.setValue(vAll);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
faSystems.valueChanges.subscribe((vSystems) => {
|
|
||||||
let off = 0;
|
|
||||||
let on = 0;
|
|
||||||
|
|
||||||
vSystems.forEach((vSystem: System) => {
|
|
||||||
if (vSystem.all) {
|
|
||||||
on++;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
off++;
|
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subs.add(fcAll.valueChanges.subscribe((vAll) => {
|
||||||
|
for (const fgSystem of faSystems.controls) {
|
||||||
|
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||||
|
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||||
|
fgTalkgroup.get('checked')?.setValue(vAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const fg of faGroups.controls) fg.get('checked')?.setValue(vAll);
|
||||||
|
for (const fg of faTags.controls) fg.get('checked')?.setValue(vAll);
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.subs.add(faSystems.valueChanges.subscribe((vSystems: System[]) => {
|
||||||
|
let on = 0;
|
||||||
|
let off = 0;
|
||||||
|
for (const vSystem of vSystems) {
|
||||||
|
if (vSystem.all) on++; else off++;
|
||||||
|
}
|
||||||
this.indeterminate.everything = !!off && !!on;
|
this.indeterminate.everything = !!off && !!on;
|
||||||
|
|
||||||
fcAll.setValue(!off && on, { emitEvent: false });
|
fcAll.setValue(!off && on, { emitEvent: false });
|
||||||
});
|
}));
|
||||||
|
|
||||||
const vAccess: Access = this.access.value;
|
const vAccess: Access = this.access.value;
|
||||||
|
|
||||||
if (vAccess.systems === '*') {
|
if (vAccess.systems === '*') {
|
||||||
this.select.get('all')?.setValue(true);
|
this.select.get('all')?.setValue(true);
|
||||||
|
|
||||||
} else if (Array.isArray(vAccess.systems)) {
|
} else if (Array.isArray(vAccess.systems)) {
|
||||||
vAccess.systems.forEach((vSystem) => {
|
for (const vSystem of vAccess.systems) {
|
||||||
if (typeof vSystem === 'number') {
|
if (typeof vSystem === 'number') {
|
||||||
faSystems.controls.find((fgSystem) => fgSystem.get('id')?.value === vSystem)?.get('all')?.setValue(true);
|
faSystems.controls.find((fgSystem) => fgSystem.get('id')?.value === vSystem)?.get('all')?.setValue(true);
|
||||||
|
} else if (vSystem && typeof vSystem === 'object') {
|
||||||
} else if (vSystem !== null && typeof vSystem === 'object') {
|
const fgSystem = faSystems.controls.find((fg) => fg.get('id')?.value === vSystem.id);
|
||||||
const fgSystem = faSystems.controls.find((fg) => {
|
|
||||||
return fg.get('id')?.value === vSystem.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (fgSystem) {
|
if (fgSystem) {
|
||||||
if (vSystem.talkgroups === '*') {
|
if (vSystem.talkgroups === '*') {
|
||||||
fgSystem.get('all')?.setValue(true);
|
fgSystem.get('all')?.setValue(true);
|
||||||
|
|
||||||
} else if (Array.isArray(vSystem.talkgroups)) {
|
} else if (Array.isArray(vSystem.talkgroups)) {
|
||||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||||
|
for (const talkgroup of vSystem.talkgroups) {
|
||||||
vSystem.talkgroups.forEach((talkgroup: { id: number } | number) => {
|
|
||||||
const talkgroupId = typeof talkgroup === 'number' ? talkgroup : talkgroup.id;
|
const talkgroupId = typeof talkgroup === 'number' ? talkgroup : talkgroup.id;
|
||||||
|
|
||||||
const fgTalkgroup = faTalkgroups.controls.find((fg) => fg.get('id')?.value === talkgroupId);
|
const fgTalkgroup = faTalkgroups.controls.find((fg) => fg.get('id')?.value === talkgroupId);
|
||||||
|
|
||||||
fgTalkgroup?.get('checked')?.setValue(true);
|
fgTalkgroup?.get('checked')?.setValue(true);
|
||||||
});
|
}
|
||||||
|
fgSystem.updateValueAndValidity();
|
||||||
fgSystem?.updateValueAndValidity();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,11 +234,7 @@ export class RdioScannerAdminSystemsSelectComponent {
|
||||||
return system['all'] || system['talkgroups'].some((talkgroup: Talkgroup) => talkgroup.checked);
|
return system['all'] || system['talkgroups'].some((talkgroup: Talkgroup) => talkgroup.checked);
|
||||||
}).map((system: System) => {
|
}).map((system: System) => {
|
||||||
if (system['all']) {
|
if (system['all']) {
|
||||||
return {
|
return { id: system['id'], talkgroups: '*' };
|
||||||
id: system['id'],
|
|
||||||
talkgroups: '*',
|
|
||||||
};
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
id: system['id'],
|
id: system['id'],
|
||||||
|
|
@ -297,61 +254,47 @@ export class RdioScannerAdminSystemsSelectComponent {
|
||||||
|
|
||||||
private rebuildGroupIndeterminates(): void {
|
private rebuildGroupIndeterminates(): void {
|
||||||
const faGroups = this.select.get('groups') as FormArray;
|
const faGroups = this.select.get('groups') as FormArray;
|
||||||
|
|
||||||
const faSystems = this.select.get('systems') as FormArray;
|
const faSystems = this.select.get('systems') as FormArray;
|
||||||
|
|
||||||
faGroups.controls.forEach((fgGroup, index) => {
|
for (let index = 0; index < faGroups.length; index++) {
|
||||||
let off = 0;
|
const fgGroup = faGroups.at(index);
|
||||||
let on = 0;
|
let on = 0;
|
||||||
|
let off = 0;
|
||||||
faSystems.controls.forEach((fgSystem) => {
|
for (const fgSystem of faSystems.controls) {
|
||||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||||
|
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
|
||||||
if (fgTalkgroup.get('groupIds')?.value.includes(fgGroup.get('id')?.value)) {
|
if (fgTalkgroup.get('groupIds')?.value.includes(fgGroup.get('id')?.value)) {
|
||||||
if (fgTalkgroup.get('checked')?.value) {
|
if (fgTalkgroup.get('checked')?.value) on++; else off++;
|
||||||
on++;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
off++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
this.indeterminate.groups[index] = !!off && !!on;
|
this.indeterminate.groups[index] = !!off && !!on;
|
||||||
|
|
||||||
fgGroup.get('checked')?.setValue(!off && on, { emitEvent: false });
|
fgGroup.get('checked')?.setValue(!off && on, { emitEvent: false });
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private rebuildTagIndeterminates(): void {
|
private rebuildTagIndeterminates(): void {
|
||||||
const faTags = this.select.get('tags') as FormArray;
|
const faTags = this.select.get('tags') as FormArray;
|
||||||
|
|
||||||
const faSystems = this.select.get('systems') as FormArray;
|
const faSystems = this.select.get('systems') as FormArray;
|
||||||
|
|
||||||
faTags.controls.forEach((fgTag, index) => {
|
for (let index = 0; index < faTags.length; index++) {
|
||||||
let off = 0;
|
const fgTag = faTags.at(index);
|
||||||
let on = 0;
|
let on = 0;
|
||||||
|
let off = 0;
|
||||||
faSystems.controls.forEach((fgSystem) => {
|
for (const fgSystem of faSystems.controls) {
|
||||||
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
const faTalkgroups = fgSystem.get('talkgroups') as FormArray;
|
||||||
|
for (const fgTalkgroup of faTalkgroups.controls) {
|
||||||
faTalkgroups.controls.forEach((fgTalkgroup) => {
|
|
||||||
if (fgTalkgroup.value.tagId === fgTag.value.id) {
|
if (fgTalkgroup.value.tagId === fgTag.value.id) {
|
||||||
if (fgTalkgroup.value.checked) {
|
if (fgTalkgroup.value.checked) on++; else off++;
|
||||||
on++;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
off++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
this.indeterminate.tags[index] = !!off && !!on;
|
this.indeterminate.tags[index] = !!off && !!on;
|
||||||
|
|
||||||
fgTag.get('checked')?.setValue(!off && on, { emitEvent: false });
|
fgTag.get('checked')?.setValue(!off && on, { emitEvent: false });
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,43 @@
|
||||||
<ng-container *ngIf="form" [formGroup]="form">
|
<ng-container *ngIf="form as f" [formGroup]="f">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Id</span><br>
|
<span class="mat-body">Id</span><br>
|
||||||
<span class="mat-caption">Site identifier in decimal format.</span>
|
<span class="mat-caption">Site identifier in decimal format.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="1" step="1" matInput formControlName="siteRef" placeholder="Id">
|
<ng-container *ngIf="f.get('siteRef') as siteRef">
|
||||||
<mat-error *ngIf="form?.get('siteRef')?.hasError('duplicate')">
|
<input type="number" min="1" step="1" matInput formControlName="siteRef" placeholder="Id" />
|
||||||
Id is already defined
|
<mat-error *ngIf="siteRef.hasError('duplicate')">
|
||||||
</mat-error>
|
Id is already defined
|
||||||
<mat-error *ngIf="form?.get('siteRef')?.hasError('min')">
|
</mat-error>
|
||||||
Id is invalid
|
<mat-error *ngIf="siteRef.hasError('min')">
|
||||||
</mat-error>
|
Id is invalid
|
||||||
<mat-error *ngIf="form?.get('siteRef')?.hasError('required')">
|
</mat-error>
|
||||||
Id is required
|
<mat-error *ngIf="siteRef.hasError('required')">
|
||||||
</mat-error>
|
Id is required
|
||||||
</mat-form-field>
|
</mat-error>
|
||||||
</div>
|
</ng-container>
|
||||||
<div class="row">
|
</mat-form-field>
|
||||||
<p>
|
</div>
|
||||||
<span class="mat-body">Label</span><br>
|
|
||||||
<span class="mat-caption">Site label.</span>
|
<div class="row">
|
||||||
</p>
|
<p>
|
||||||
<mat-form-field floatLabel="auto">
|
<span class="mat-body">Label</span><br>
|
||||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
<span class="mat-caption">Site label.</span>
|
||||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
</p>
|
||||||
Label is required
|
<mat-form-field floatLabel="auto">
|
||||||
</mat-error>
|
<ng-container *ngIf="f.get('label') as labelCtrl">
|
||||||
</mat-form-field>
|
<input type="text" matInput formControlName="label" placeholder="Label" />
|
||||||
</div>
|
<mat-error *ngIf="labelCtrl.hasError('required')">
|
||||||
<div class="row bottom">
|
Label is required
|
||||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
</mat-error>
|
||||||
Delete site
|
</ng-container>
|
||||||
</button>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row bottom">
|
||||||
|
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
||||||
|
Delete site
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
@ -17,15 +17,36 @@
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup, AbstractControl } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-site',
|
selector: 'rdio-scanner-admin-site',
|
||||||
templateUrl: './site.component.html',
|
templateUrl: './site.component.html',
|
||||||
|
standalone: false,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminSiteComponent {
|
export class RdioScannerAdminSiteComponent {
|
||||||
@Input() form: FormGroup | undefined;
|
private _form: FormGroup | undefined;
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set form(f: FormGroup | undefined) {
|
||||||
|
this._form = f;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
get form(): FormGroup | undefined {
|
||||||
|
return this._form;
|
||||||
|
}
|
||||||
|
|
||||||
|
get siteRef(): AbstractControl | null | undefined {
|
||||||
|
return this._form?.get('siteRef');
|
||||||
|
}
|
||||||
|
|
||||||
|
get labelControl(): AbstractControl | null | undefined {
|
||||||
|
return this._form?.get('label');
|
||||||
|
}
|
||||||
|
|
||||||
@Output() remove = new EventEmitter<void>();
|
@Output() remove = new EventEmitter<void>();
|
||||||
|
|
||||||
|
constructor(private readonly cd: ChangeDetectorRef) {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,229 +1,228 @@
|
||||||
<ng-container [formGroup]="form">
|
<ng-container [formGroup]="form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Id</span><br>
|
<span class="mat-body">Id</span><br>
|
||||||
<span class="mat-caption">System identifier in decimal format.</span>
|
<span class="mat-caption">System identifier in decimal format.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="1" step="1" matInput formControlName="systemRef" placeholder="Id">
|
<input type="number" min="1" step="1" matInput formControlName="systemRef" placeholder="Id">
|
||||||
<mat-error *ngIf="form.get('systemRef')?.hasError('duplicate')">
|
<mat-error *ngIf="form.get('systemRef')?.hasError('duplicate')">
|
||||||
Id is already defined
|
Id is already defined
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="form.get('systemRef')?.hasError('min')">
|
<mat-error *ngIf="form.get('systemRef')?.hasError('min')">
|
||||||
Id is invalid
|
Id is invalid
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="form.get('systemRef')?.hasError('required')">
|
<mat-error *ngIf="form.get('systemRef')?.hasError('required')">
|
||||||
Id is required
|
Id is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Label</span><br>
|
<span class="mat-body">Label</span><br>
|
||||||
<span class="mat-caption">System label displayed on the main screen and on the search panel.</span>
|
<span class="mat-caption">System label displayed on the main screen and on the search panel.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||||
<mat-error *ngIf="form.get('label')?.hasError('required')">
|
<mat-error *ngIf="form.get('label')?.hasError('required')">
|
||||||
Label is required
|
Label is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Audio alert</span><br>
|
<span class="mat-body">Audio alert</span><br>
|
||||||
<span class="mat-caption">Prefixes an alert sound to all calls assigned from this system. The alert
|
<span class="mat-caption">Prefixes an alert sound to all calls assigned from this system. The alert
|
||||||
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
|
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
|
||||||
<mat-option value="">No Alert</mat-option>
|
<mat-option value="">No Alert</mat-option>
|
||||||
<mat-option *ngFor="let alert of alerts" [value]="alert">
|
<mat-option *ngFor="let alert of alerts" [value]="alert">
|
||||||
{{ alert | titlecase }}
|
{{ alert | titlecase }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Auto Populate</span><br>
|
<span class="mat-body">Auto Populate</span><br>
|
||||||
<span class="mat-caption">Allows the automatic creation of unconfigured talkgroups.</span>
|
<span class="mat-caption">Allows the automatic creation of unconfigured talkgroups.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-slide-toggle color="primary" formControlName="autoPopulate"></mat-slide-toggle>
|
<mat-slide-toggle color="primary" formControlName="autoPopulate"></mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Delay</span><br>
|
<span class="mat-body">Delay</span><br>
|
||||||
<span class="mat-caption">Delay in minutes before propagating calls to clients.</span>
|
<span class="mat-caption">Delay in minutes before propagating calls to clients.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="0" step="1" matInput formControlName="delay" placeholder="Minutes">
|
<input type="number" min="0" step="1" matInput formControlName="delay" placeholder="Minutes">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Led Color</span><br>
|
<span class="mat-body">Led Color</span><br>
|
||||||
<span class="mat-caption">The default LED color is green but it can be changed at several levels.
|
<span class="mat-caption">The default LED color is green but it can be changed at several levels.
|
||||||
The color assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
The color assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="led" placeholder="Color">
|
<mat-select formControlName="led" placeholder="Color">
|
||||||
<mat-option [value]="''">Default</mat-option>
|
<mat-option value="">Default</mat-option>
|
||||||
<mat-option *ngFor="let led of leds" [value]="led">
|
<mat-option *ngFor="let led of leds" [value]="led">
|
||||||
{{ led | titlecase }}
|
{{ led | titlecase }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Blacklists</span><br>
|
<span class="mat-body">Blacklists</span><br>
|
||||||
<span class="mat-caption">A comma separated list of talkgroup Ids to blacklist from this system. This only
|
<span class="mat-caption">A comma separated list of talkgroup Ids to blacklist from this system. This only
|
||||||
has an effect if the auto populate option is enabled.</span>
|
has an effect if the auto populate option is enabled.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<textarea type="text" matInput formControlName="blacklists" placeholder="Blacklists"></textarea>
|
<textarea matInput formControlName="blacklists" placeholder="Blacklists"></textarea>
|
||||||
<mat-error *ngIf="form?.get('blacklists')?.hasError('invalid')">
|
<mat-error *ngIf="form.get('blacklists')!.hasError('invalid')">
|
||||||
Comma separated list of talkgroup Ids
|
Comma separated list of talkgroup Ids
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Type</span><br>
|
<span class="mat-body">Type</span><br>
|
||||||
<span class="mat-caption">
|
<span class="mat-caption">
|
||||||
Type of radio system displayed on main screen.
|
Type of radio system displayed on main screen.
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>AM</b> - Amplitude modulation.</li>
|
<li><b>AM</b> - Amplitude modulation.</li>
|
||||||
<li><b>DMR</b> - Digital mobile radio system.</li>
|
<li><b>DMR</b> - Digital mobile radio system.</li>
|
||||||
<li><b>FM</b> - Frequency modulation.</li>
|
<li><b>FM</b> - Frequency modulation.</li>
|
||||||
<li><b>NFM</b> - Narrow frequency modulation.</li>
|
<li><b>NFM</b> - Narrow frequency modulation.</li>
|
||||||
<li><b>NXDN</b> - Next Generation Digital Narrowband radio system.</li>
|
<li><b>NXDN</b> - Next Generation Digital Narrowband radio system.</li>
|
||||||
<li><b>P25</b> - APCO-25 radio system.</li>
|
<li><b>P25</b> - APCO-25 radio system.</li>
|
||||||
<li><b>Provoice</b> - EDACS Provoice radio system.</li>
|
<li><b>Provoice</b> - EDACS Provoice radio system.</li>
|
||||||
<li><b>SmartNet</b> - Motorola SmartNet radio system.</li>
|
<li><b>SmartNet</b> - Motorola SmartNet radio system.</li>
|
||||||
<li><b>Tetra</b> - Trans european trunked radio system.</li>
|
<li><b>Tetra</b> - Trans european trunked radio system.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="type" placeholder="Type">
|
<mat-select formControlName="type" placeholder="Type">
|
||||||
<mat-option value="">No Type</mat-option>
|
<mat-option value="">No Type</mat-option>
|
||||||
<mat-option value="am">AM</mat-option>
|
<mat-option value="am">AM</mat-option>
|
||||||
<mat-option value="dmr">DMR</mat-option>
|
<mat-option value="dmr">DMR</mat-option>
|
||||||
<mat-option value="fm">FM</mat-option>
|
<mat-option value="fm">FM</mat-option>
|
||||||
<mat-option value="nfm">NFM</mat-option>
|
<mat-option value="nfm">NFM</mat-option>
|
||||||
<mat-option value="nxdn">NXDN</mat-option>
|
<mat-option value="nxdn">NXDN</mat-option>
|
||||||
<mat-option value="p25">P25</mat-option>
|
<mat-option value="p25">P25</mat-option>
|
||||||
<mat-option value="provoice">Provoice</mat-option>
|
<mat-option value="provoice">Provoice</mat-option>
|
||||||
<mat-option value="smartnet">SmartNet</mat-option>
|
<mat-option value="smartnet">SmartNet</mat-option>
|
||||||
<mat-option value="tetra">Tetra</mat-option>
|
<mat-option value="tetra">Tetra</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<mat-accordion displayMode="flat">
|
<mat-accordion displayMode="flat">
|
||||||
<mat-expansion-panel>
|
<mat-expansion-panel>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Sites
|
||||||
|
<mat-icon *ngIf="form.get('sites')?.invalid" color="warn">error</mat-icon>
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<ng-template matExpansionPanelContent>
|
||||||
|
<div class="row top">
|
||||||
|
<p class="mat-caption">System sites, you can drag and drop sites to rearrange them.</p>
|
||||||
|
<button type="button" mat-button color="accent" (click)="addSite()">New Site</button>
|
||||||
|
</div>
|
||||||
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="sites"
|
||||||
|
(cdkDropListDropped)="drop($event)">
|
||||||
|
<mat-expansion-panel *ngFor="let site of sites; let i = index; trackBy: trackById" [formGroup]="site" cdkDrag>
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
Sites
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
<mat-icon *ngIf="form.get('sites')?.invalid" color="warn">error</mat-icon>
|
{{ site.value.label?.trim() || 'NewSite' }}
|
||||||
</mat-panel-title>
|
<mat-icon *ngIf="site.invalid" color="warn">error</mat-icon>
|
||||||
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<ng-template matExpansionPanelContent>
|
<ng-template matExpansionPanelContent>
|
||||||
<div class="row top">
|
<rdio-scanner-admin-site [form]="site" (remove)="removeSite(i)"></rdio-scanner-admin-site>
|
||||||
<p class="mat-caption">System sites, you can drag and drop sites to rearrange them.</p>
|
|
||||||
<button type="button" mat-button color="accent" (click)="addSite()">New Site</button>
|
|
||||||
</div>
|
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="sites"
|
|
||||||
(cdkDropListDropped)="drop($event)">
|
|
||||||
<mat-expansion-panel *ngFor="let site of sites; index as i" [formGroup]="site" cdkDrag>
|
|
||||||
<mat-expansion-panel-header>
|
|
||||||
<mat-panel-title>
|
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
|
||||||
{{ site.value.label?.trim() || 'NewSite' }}
|
|
||||||
<mat-icon *ngIf="site.invalid" color="warn">error</mat-icon>
|
|
||||||
</mat-panel-title>
|
|
||||||
</mat-expansion-panel-header>
|
|
||||||
<ng-template matExpansionPanelContent>
|
|
||||||
<rdio-scanner-admin-site [form]="site" (remove)="removeSite(i)"></rdio-scanner-admin-site>
|
|
||||||
</ng-template>
|
|
||||||
</mat-expansion-panel>
|
|
||||||
</mat-accordion>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
<mat-accordion displayMode="flat">
|
</ng-template>
|
||||||
<mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
</mat-accordion>
|
||||||
|
<mat-accordion displayMode="flat">
|
||||||
|
<mat-expansion-panel>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Talkgroups
|
||||||
|
<mat-icon *ngIf="form.get('talkgroups')?.invalid" color="warn">error</mat-icon>
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<ng-template matExpansionPanelContent>
|
||||||
|
<div class="row top">
|
||||||
|
<p class="mat-caption">System talkgroups, you can drag and drop talkgroups to rearrange them. They
|
||||||
|
will appear with the same order on the talkgroup selection panel unless the sort talkgroups
|
||||||
|
option is enabled.</p>
|
||||||
|
<button type="button" mat-button color="accent" (click)="addTalkgroup()">New talkgroup</button>
|
||||||
|
</div>
|
||||||
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64"
|
||||||
|
[cdkDropListData]="talkgroups" (cdkDropListDropped)="drop($event)">
|
||||||
|
<mat-expansion-panel *ngFor="let talkgroup of talkgroups; let i = index; trackBy: trackById" [formGroup]="talkgroup" cdkDrag>
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
Talkgroups
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
<mat-icon *ngIf="form.get('talkgroups')?.invalid" color="warn">error</mat-icon>
|
{{ talkgroup.value.label?.trim() || 'NewTalkgroup' }}
|
||||||
</mat-panel-title>
|
<mat-icon *ngIf="talkgroup.invalid" color="warn">error</mat-icon>
|
||||||
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<ng-template matExpansionPanelContent>
|
<ng-template matExpansionPanelContent>
|
||||||
<div class="row top">
|
<rdio-scanner-admin-talkgroup [form]="talkgroup" (blacklist)="blacklistTalkgroup(i)"
|
||||||
<p class="mat-caption">System talkgroups, you can drag and drop talkgroups to rearrange them. They
|
(remove)="removeTalkgroup(i)">
|
||||||
will appear with the same order on the talkgroup selection panel unless the sort talkgroups
|
</rdio-scanner-admin-talkgroup>
|
||||||
option is enabled.</p>
|
|
||||||
<button type="button" mat-button color="accent" (click)="addTalkgroup()">New talkgroup</button>
|
|
||||||
</div>
|
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64
|
|
||||||
[cdkDropListData]="talkgroups" (cdkDropListDropped)="drop($event)">
|
|
||||||
<mat-expansion-panel *ngFor="let talkgroup of talkgroups; index as i" [formGroup]="talkgroup"
|
|
||||||
cdkDrag>
|
|
||||||
<mat-expansion-panel-header>
|
|
||||||
<mat-panel-title>
|
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
|
||||||
{{ talkgroup.value.label?.trim() || 'NewTalkgroup' }}
|
|
||||||
<mat-icon *ngIf="talkgroup.invalid" color="warn">error</mat-icon>
|
|
||||||
</mat-panel-title>
|
|
||||||
</mat-expansion-panel-header>
|
|
||||||
<ng-template matExpansionPanelContent>
|
|
||||||
<rdio-scanner-admin-talkgroup [form]="talkgroup" (blacklist)="blacklistTalkgroup(i)"
|
|
||||||
(remove)="removeTalkgroup(i)">
|
|
||||||
</rdio-scanner-admin-talkgroup>
|
|
||||||
</ng-template>
|
|
||||||
</mat-expansion-panel>
|
|
||||||
</mat-accordion>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
<mat-accordion displayMode="flat">
|
</ng-template>
|
||||||
<mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel-header>
|
</mat-accordion>
|
||||||
|
<mat-accordion displayMode="flat">
|
||||||
|
<mat-expansion-panel>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
|
<mat-panel-title>
|
||||||
|
Units
|
||||||
|
<mat-icon *ngIf="form.get('units')?.invalid" color="warn">error</mat-icon>
|
||||||
|
</mat-panel-title>
|
||||||
|
</mat-expansion-panel-header>
|
||||||
|
<ng-template matExpansionPanelContent>
|
||||||
|
<div class="row top">
|
||||||
|
<p class="mat-caption">Create units to display the label instead of showing the unit id on the main
|
||||||
|
screen.<br>You can drag and drop them to rearrange</p>
|
||||||
|
<button type="button" mat-button color="accent" (click)="addUnit()">New unit</button>
|
||||||
|
</div>
|
||||||
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="units"
|
||||||
|
(cdkDropListDropped)="drop($event)">
|
||||||
|
<mat-expansion-panel *ngFor="let unit of units; let i = index; trackBy: trackById" [formGroup]="unit" cdkDrag>
|
||||||
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
Units
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
<mat-icon *ngIf="form.get('units')?.invalid" color="warn">error</mat-icon>
|
{{ unit.value.label?.trim() || 'NewUnit' }}
|
||||||
|
<mat-icon *ngIf="unit.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<ng-template matExpansionPanelContent>
|
<ng-template matExpansionPanelContent>
|
||||||
<div class="row top">
|
<rdio-scanner-admin-unit [form]="unit" (remove)="removeUnit(i)">
|
||||||
<p class="mat-caption">Create units to display the label instead of showing the unit id on the main
|
</rdio-scanner-admin-unit>
|
||||||
screen.<br>You can drag and drop them to rearrange</p>
|
</ng-template>
|
||||||
<button type="button" mat-button color="accent" (click)="addUnit()">New unit</button>
|
</mat-expansion-panel>
|
||||||
</div>
|
</mat-accordion>
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="units"
|
</ng-template>
|
||||||
(cdkDropListDropped)="drop($event)">
|
</mat-expansion-panel>
|
||||||
<mat-expansion-panel *ngFor="let unit of units; index as i" [formGroup]="unit" cdkDrag>
|
</mat-accordion>
|
||||||
<mat-expansion-panel-header>
|
<div class="row bottom">
|
||||||
<mat-panel-title>
|
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
Delete system
|
||||||
{{ unit.value.label?.trim() || 'NewUnit' }}
|
</button>
|
||||||
<mat-icon *ngIf="unit.invalid" color="warn">error</mat-icon>
|
</div>
|
||||||
</mat-panel-title>
|
|
||||||
</mat-expansion-panel-header>
|
|
||||||
<ng-template matExpansionPanelContent>
|
|
||||||
<rdio-scanner-admin-unit [form]="unit" (remove)="removeUnit(i)">
|
|
||||||
</rdio-scanner-admin-unit>
|
|
||||||
</ng-template>
|
|
||||||
</mat-expansion-panel>
|
|
||||||
</mat-accordion>
|
|
||||||
</ng-template>
|
|
||||||
</mat-expansion-panel>
|
|
||||||
</mat-accordion>
|
|
||||||
<div class="row bottom">
|
|
||||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
|
||||||
Delete system
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
@ -27,6 +27,7 @@ import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-system',
|
selector: 'rdio-scanner-admin-system',
|
||||||
templateUrl: './system.component.html',
|
templateUrl: './system.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminSystemComponent {
|
export class RdioScannerAdminSystemComponent {
|
||||||
@Input() form = new FormGroup({});
|
@Input() form = new FormGroup({});
|
||||||
|
|
@ -47,23 +48,22 @@ export class RdioScannerAdminSystemComponent {
|
||||||
|
|
||||||
get sites(): FormGroup[] {
|
get sites(): FormGroup[] {
|
||||||
const sites = this.form.get('sites') as FormArray | null;
|
const sites = this.form.get('sites') as FormArray | null;
|
||||||
|
return (sites?.controls as FormGroup[]) || [];
|
||||||
return sites?.controls
|
|
||||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get talkgroups(): FormGroup[] {
|
get talkgroups(): FormGroup[] {
|
||||||
const talkgroups = this.form.get('talkgroups') as FormArray | null;
|
const talkgroups = this.form.get('talkgroups') as FormArray | null;
|
||||||
|
return (talkgroups?.controls as FormGroup[]) || [];
|
||||||
return talkgroups?.controls
|
|
||||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get units(): FormGroup[] {
|
get units(): FormGroup[] {
|
||||||
const units = this.form.get('units') as FormArray | null;
|
const units = this.form.get('units') as FormArray | null;
|
||||||
|
return (units?.controls as FormGroup[]) || [];
|
||||||
|
}
|
||||||
|
|
||||||
return units?.controls
|
// trackBy to preserve identity in ngFor lists
|
||||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
trackById(_index: number, group: FormGroup): any {
|
||||||
|
return group.get('id')?.value ?? _index;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
<div class="row top">
|
<div class="row top">
|
||||||
<p class="mat-body">This is where you define all your systems and talkgroups are defined. Audio files to unknown
|
<p class="mat-body">This is where you define all your systems and talkgroups are defined. Audio files to unknown
|
||||||
systems/talkgroups won't be ingested unless the auto populate option is activated. You can also drag and drop
|
systems/talkgroups won't be ingested unless the auto populate option is activated. You can also drag and drop
|
||||||
systems to rearrange their order.</p>
|
systems to rearrange their order.</p>
|
||||||
<button type="button" mat-button color="accent" (click)="add()">New system</button>
|
<button type="button" mat-button color="accent" (click)="add()">New system</button>
|
||||||
</div>
|
</div>
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="systems" (cdkDropListDropped)="drop($event)">
|
<mat-accordion *ngIf="systems" displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]="64" [cdkDropListData]="systems" (cdkDropListDropped)="drop($event)">
|
||||||
<mat-expansion-panel *ngFor="let system of systems; index as i" [formGroup]="system"
|
<mat-expansion-panel *ngFor="let system of systems; let i = index" [formGroup]="system"
|
||||||
(afterCollapse)="systemComponent.closeAll()" cdkDrag>
|
(afterCollapse)="systemComponent.closeAll()" cdkDrag>
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel-header>
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
{{ system.value.label?.trim() || 'NewSystem' }}
|
{{ system.value.label?.trim() || 'NewSystem' }}
|
||||||
<mat-icon *ngIf="system.invalid" color="warn">error</mat-icon>
|
<mat-icon *ngIf="system.invalid" color="warn">error</mat-icon>
|
||||||
</mat-panel-title>
|
</mat-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<rdio-scanner-admin-system #systemComponent [form]="system" (add)="add()" (remove)="remove(i)">
|
<rdio-scanner-admin-system #systemComponent [form]="system" (add)="add()" (remove)="remove(i)">
|
||||||
</rdio-scanner-admin-system>
|
</rdio-scanner-admin-system>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|
@ -18,26 +18,44 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { Component, Input, QueryList, ViewChildren } from '@angular/core';
|
import { Component, Input, QueryList, ViewChildren, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy } from '@angular/core';
|
||||||
import { FormArray, FormGroup } from '@angular/forms';
|
import { FormArray, FormGroup } from '@angular/forms';
|
||||||
import { MatExpansionPanel } from '@angular/material/expansion';
|
import { MatExpansionPanel } from '@angular/material/expansion';
|
||||||
import { RdioScannerAdminService } from '../../admin.service';
|
import { RdioScannerAdminService } from '../../admin.service';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-systems',
|
selector: 'rdio-scanner-admin-systems',
|
||||||
templateUrl: './systems.component.html',
|
templateUrl: './systems.component.html',
|
||||||
|
standalone: false,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminSystemsComponent {
|
export class RdioScannerAdminSystemsComponent implements OnDestroy {
|
||||||
@Input() form: FormArray | undefined;
|
private _form: FormArray | undefined;
|
||||||
|
private subs = new Subscription();
|
||||||
|
private _systemsCache: FormGroup[] = [];
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set form(f: FormArray | undefined) {
|
||||||
|
if (this._form === f) { return; }
|
||||||
|
this.subs.unsubscribe();
|
||||||
|
this.subs = new Subscription();
|
||||||
|
this._form = f;
|
||||||
|
this.updateCache();
|
||||||
|
if (this._form) {
|
||||||
|
const s = this._form.valueChanges.subscribe(() => this.updateCache());
|
||||||
|
this.subs.add(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get form(): FormArray | undefined { return this._form; }
|
||||||
|
|
||||||
get systems(): FormGroup[] {
|
get systems(): FormGroup[] {
|
||||||
return this.form?.controls
|
return this._systemsCache;
|
||||||
.sort((a, b) => (a.value.order || 0) - (b.value.order || 0)) as FormGroup[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
@ViewChildren(MatExpansionPanel) private panels: QueryList<MatExpansionPanel> | undefined;
|
||||||
|
|
||||||
constructor(private adminService: RdioScannerAdminService) { }
|
constructor(private adminService: RdioScannerAdminService, private cd: ChangeDetectorRef) { }
|
||||||
|
|
||||||
add(): void {
|
add(): void {
|
||||||
const system = this.adminService.newSystemForm();
|
const system = this.adminService.newSystemForm();
|
||||||
|
|
@ -47,6 +65,8 @@ export class RdioScannerAdminSystemsComponent {
|
||||||
this.form?.insert(0, system);
|
this.form?.insert(0, system);
|
||||||
|
|
||||||
this.form?.markAsDirty();
|
this.form?.markAsDirty();
|
||||||
|
|
||||||
|
this.updateCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAll(): void {
|
closeAll(): void {
|
||||||
|
|
@ -60,6 +80,8 @@ export class RdioScannerAdminSystemsComponent {
|
||||||
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
event.container.data.forEach((dat, idx) => dat.get('order')?.setValue(idx + 1, { emitEvent: false }));
|
||||||
|
|
||||||
this.form?.markAsDirty();
|
this.form?.markAsDirty();
|
||||||
|
|
||||||
|
this.updateCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,5 +89,23 @@ export class RdioScannerAdminSystemsComponent {
|
||||||
this.form?.removeAt(index);
|
this.form?.removeAt(index);
|
||||||
|
|
||||||
this.form?.markAsDirty();
|
this.form?.markAsDirty();
|
||||||
|
|
||||||
|
this.updateCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCache(): void {
|
||||||
|
if (!this._form) {
|
||||||
|
this._systemsCache = [];
|
||||||
|
this.cd.markForCheck();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const arr = [...this._form.controls] as FormGroup[];
|
||||||
|
arr.sort((a, b) => (a.value.order || 0) - (b.value.order || 0));
|
||||||
|
this._systemsCache = arr;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,168 +1,170 @@
|
||||||
<ng-container *ngIf="form" [formGroup]="form">
|
<ng-container *ngIf="form">
|
||||||
|
<ng-container [formGroup]="form">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Id</span><br>
|
<span class="mat-body">Id</span><br>
|
||||||
<span class="mat-caption">Talkgroup identifier in decimal format.</span>
|
<span class="mat-caption">Talkgroup identifier in decimal format.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="1" step="1" matInput formControlName="talkgroupRef" placeholder="Id">
|
<input type="number" min="1" step="1" matInput formControlName="talkgroupRef" placeholder="Id">
|
||||||
<mat-error *ngIf="form?.get('talkgroupRef')?.hasError('duplicate')">
|
<mat-error *ngIf="form.get('talkgroupRef')!.hasError('duplicate')">
|
||||||
Id is already defined
|
Id is already defined
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="form?.get('talkgroupRef')?.hasError('min')">
|
<mat-error *ngIf="form.get('talkgroupRef')!.hasError('min')">
|
||||||
Id is invalid
|
Id is invalid
|
||||||
</mat-error>
|
</mat-error>
|
||||||
<mat-error *ngIf="form?.get('talkgroupRef')?.hasError('required')">
|
<mat-error *ngIf="form.get('talkgroupRef')!.hasError('required')">
|
||||||
Id is required
|
Id is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Label</span><br>
|
<span class="mat-body">Label</span><br>
|
||||||
<span class="mat-caption">Talkgroup label displayed on the main screen and on buttons.</span>
|
<span class="mat-caption">Talkgroup label displayed on the main screen and on buttons.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
<mat-error *ngIf="form.get('label')!.hasError('required')">
|
||||||
Label is required
|
Label is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Name</span><br>
|
<span class="mat-body">Name</span><br>
|
||||||
<span class="mat-caption">Talkgroup name displayed on the main screen.</span>
|
<span class="mat-caption">Talkgroup name displayed on the main screen.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="text" matInput formControlName="name" placeholder="Name">
|
<input type="text" matInput formControlName="name" placeholder="Name">
|
||||||
<mat-error *ngIf="form?.get('name')?.hasError('required')">
|
<mat-error *ngIf="form.get('name')!.hasError('required')">
|
||||||
Name is required
|
Name is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Groups</span><br>
|
<span class="mat-body">Groups</span><br>
|
||||||
<span class="mat-caption">Groups to which this talkgoups belongs.</span>
|
<span class="mat-caption">Groups to which this talkgroups belongs.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="groupIds" placeholder="Groups" [multiple]="true">
|
<mat-select formControlName="groupIds" placeholder="Groups" [multiple]="true">
|
||||||
<mat-option *ngFor="let group of groups" [value]="group.id">
|
<mat-option *ngFor="let group of groups" [value]="group.id">
|
||||||
{{ group.label }}
|
{{ group.label }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-error *ngIf="form?.get('groupId')?.hasError('required')">
|
<mat-error *ngIf="form.get('groupIds')?.hasError('required')">
|
||||||
Group is required
|
Group is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Tag</span><br>
|
<span class="mat-body">Tag</span><br>
|
||||||
<span class="mat-caption">Tag to which this talkgoups belongs.</span>
|
<span class="mat-caption">Tag to which this talkgroups belongs.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="tagId" placeholder="Tag">
|
<mat-select formControlName="tagId" placeholder="Tag">
|
||||||
<mat-option *ngFor="let tag of tags" [value]="tag.id">
|
<mat-option *ngFor="let tag of tags" [value]="tag.id">
|
||||||
{{ tag.label }}
|
{{ tag.label }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
<mat-error *ngIf="form?.get('tagId')?.hasError('required')">
|
<mat-error *ngIf="form.get('tagId')!.hasError('required')">
|
||||||
Tag is required
|
Tag is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Audio alert</span><br>
|
<span class="mat-body">Audio alert</span><br>
|
||||||
<span class="mat-caption">Prefixes an alert sound to all calls assigned from this talkgroup. The alert
|
<span class="mat-caption">Prefixes an alert sound to all calls assigned from this talkgroup. The alert
|
||||||
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
|
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event.value)">
|
||||||
<mat-option value="">No Alert</mat-option>
|
<mat-option value="">No Alert</mat-option>
|
||||||
<mat-option *ngFor="let alert of alerts" [value]="alert">
|
<mat-option *ngFor="let alert of alerts" [value]="alert">
|
||||||
{{ alert | titlecase }}
|
{{ alert | titlecase }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Delay</span><br>
|
<span class="mat-body">Delay</span><br>
|
||||||
<span class="mat-caption">Delay in minutes before propagating calls to clients.</span>
|
<span class="mat-caption">Delay in minutes before propagating calls to clients.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="0" step="1" matInput formControlName="delay" placeholder="Minutes">
|
<input type="number" min="0" step="1" matInput formControlName="delay" placeholder="Minutes">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Frequency</span><br>
|
<span class="mat-body">Frequency</span><br>
|
||||||
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
|
<span class="mat-caption">Fake frequency in hertz displayed on the main screen.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="0" matInput formControlName="frequency" placeholder="Frequency">
|
<input type="number" min="0" matInput formControlName="frequency" placeholder="Frequency">
|
||||||
<mat-error *ngIf="form?.get('frequency')?.errors">
|
<mat-error *ngIf="form.get('frequency')?.errors">
|
||||||
Invalid frequency
|
Invalid frequency
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Led Color</span><br>
|
<span class="mat-body">Led Color</span><br>
|
||||||
<span class="mat-caption">The default LED color is green but it can be changed at several levels.
|
<span class="mat-caption">The default LED color is green but it can be changed at several levels.
|
||||||
The color assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
The color assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="led" placeholder="Color">
|
<mat-select formControlName="led" placeholder="Color">
|
||||||
<mat-option [value]="''">Default</mat-option>
|
<mat-option value="">Default</mat-option>
|
||||||
<mat-option *ngFor="let led of leds" [value]="led">
|
<mat-option *ngFor="let led of leds" [value]="led">
|
||||||
{{ led | titlecase }}
|
{{ led | titlecase }}
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Type</span><br>
|
<span class="mat-body">Type</span><br>
|
||||||
<span class="mat-caption">
|
<span class="mat-caption">
|
||||||
Type of radio system that will override the type defined at the system level, if any.
|
Type of radio system that will override the type defined at the system level, if any.
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>AM</b> - Amplitude modulation.</li>
|
<li><b>AM</b> - Amplitude modulation.</li>
|
||||||
<li><b>DMR</b> - Digital mobile radio system.</li>
|
<li><b>DMR</b> - Digital mobile radio system.</li>
|
||||||
<li><b>FM</b> - Frequency modulation.</li>
|
<li><b>FM</b> - Frequency modulation.</li>
|
||||||
<li><b>NFM</b> - Narrow frequency modulation.</li>
|
<li><b>NFM</b> - Narrow frequency modulation.</li>
|
||||||
<li><b>NXDN</b> - Next Generation Digital Narrowband radio system.</li>
|
<li><b>NXDN</b> - Next Generation Digital Narrowband radio system.</li>
|
||||||
<li><b>P25</b> - APCO-25 radio system.</li>
|
<li><b>P25</b> - APCO-25 radio system.</li>
|
||||||
<li><b>Provoice</b> - EDACS Provoice radio system.</li>
|
<li><b>Provoice</b> - EDACS Provoice radio system.</li>
|
||||||
<li><b>SmartNet</b> - Motorola SmartNet radio system.</li>
|
<li><b>SmartNet</b> - Motorola SmartNet radio system.</li>
|
||||||
<li><b>Tetra</b> - Trans european trunked radio system.</li>
|
<li><b>Tetra</b> - Trans european trunked radio system.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select formControlName="type" placeholder="Type">
|
<mat-select formControlName="type" placeholder="Type">
|
||||||
<mat-option value="">No Type</mat-option>
|
<mat-option value="">No Type</mat-option>
|
||||||
<mat-option value="am">AM</mat-option>
|
<mat-option value="am">AM</mat-option>
|
||||||
<mat-option value="dmr">DMR</mat-option>
|
<mat-option value="dmr">DMR</mat-option>
|
||||||
<mat-option value="fm">FM</mat-option>
|
<mat-option value="fm">FM</mat-option>
|
||||||
<mat-option value="nfm">NFM</mat-option>
|
<mat-option value="nfm">NFM</mat-option>
|
||||||
<mat-option value="nxdn">NXDN</mat-option>
|
<mat-option value="nxdn">NXDN</mat-option>
|
||||||
<mat-option value="p25">P25</mat-option>
|
<mat-option value="p25">P25</mat-option>
|
||||||
<mat-option value="provoice">Provoice</mat-option>
|
<mat-option value="provoice">Provoice</mat-option>
|
||||||
<mat-option value="smartnet">SmartNet</mat-option>
|
<mat-option value="smartnet">SmartNet</mat-option>
|
||||||
<mat-option value="tetra">Tetra</mat-option>
|
<mat-option value="tetra">Tetra</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row bottom">
|
<div class="row bottom">
|
||||||
<button *ngIf="form.get('id')?.value" type="button" mat-button (click)="blacklist.emit()">
|
<button *ngIf="form.get('id')?.value" type="button" mat-button (click)="blacklist.emit()">
|
||||||
Blacklist talkgroup
|
Blacklist talkgroup
|
||||||
</button>
|
</button>
|
||||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
||||||
Delete talkgroup
|
Delete talkgroup
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
@ -19,12 +19,12 @@
|
||||||
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { MatSelectChange } from '@angular/material/select';
|
|
||||||
import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
|
import { RdioScannerAdminService, Group, Tag } from '../../../admin.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-talkgroup',
|
selector: 'rdio-scanner-admin-talkgroup',
|
||||||
templateUrl: './talkgroup.component.html',
|
templateUrl: './talkgroup.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminTalkgroupComponent {
|
export class RdioScannerAdminTalkgroupComponent {
|
||||||
@Input() form: FormGroup | undefined;
|
@Input() form: FormGroup | undefined;
|
||||||
|
|
@ -33,25 +33,25 @@ export class RdioScannerAdminTalkgroupComponent {
|
||||||
|
|
||||||
@Output() remove = new EventEmitter<void>();
|
@Output() remove = new EventEmitter<void>();
|
||||||
|
|
||||||
leds: string[];
|
leds: string[] = [];
|
||||||
|
|
||||||
get alerts(): string[] {
|
get alerts(): string[] {
|
||||||
return Object.keys(this.adminService.Alerts || {});
|
return Object.keys(this.adminService.Alerts || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
get groups(): Group[] {
|
get groups(): Group[] {
|
||||||
return this.form?.root.get('groups')?.value as Group[];
|
return (this.form?.root.get('groups')?.value as Group[]) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get tags(): Tag[] {
|
get tags(): Tag[] {
|
||||||
return this.form?.root.get('tags')?.value as Tag[];
|
return (this.form?.root.get('tags')?.value as Tag[]) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private adminService: RdioScannerAdminService) {
|
constructor(private adminService: RdioScannerAdminService) {
|
||||||
this.leds = this.adminService.getLeds();
|
this.leds = this.adminService.getLeds() || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async playAlert(event: MatSelectChange): Promise<void> {
|
async playAlert(value: string | null): Promise<void> {
|
||||||
if (event.value) await this.adminService.playAlert(event.value);
|
if (value) await this.adminService.playAlert(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,52 @@
|
||||||
<ng-container *ngIf="form" [formGroup]="form">
|
<ng-container *ngIf="form as f">
|
||||||
|
<ng-container [formGroup]="f">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Id</span><br>
|
<span class="mat-body">Id</span><br>
|
||||||
<span class="mat-caption">Unit identifier in decimal format. You can define a unique unit identifier here,
|
<span class="mat-caption">Unit identifier in decimal format. You can define a unique unit identifier here,
|
||||||
but you can also use a range of identifiers using the fields below.</span>
|
but you can also use a range of identifiers using the fields below.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="1" step="1" matInput formControlName="unitRef" placeholder="Id">
|
<input type="number" min="1" step="1" matInput formControlName="unitRef" placeholder="Id">
|
||||||
<mat-error *ngIf="form?.get('unitRef')?.hasError('duplicate')">
|
<mat-error *ngIf="f.get('unitRef')!.hasError('duplicate')">Id value is already defined</mat-error>
|
||||||
Id value is already defined
|
<mat-error *ngIf="f.get('unitRef')!.hasError('min')">Id value is invalid</mat-error>
|
||||||
</mat-error>
|
<mat-error *ngIf="f.get('unitRef')!.hasError('required')">Id value is required</mat-error>
|
||||||
<mat-error *ngIf="form?.get('unitRef')?.hasError('min')">
|
</mat-form-field>
|
||||||
Id value is invalid
|
|
||||||
</mat-error>
|
|
||||||
<mat-error *ngIf="form?.get('unitRef')?.hasError('required')">
|
|
||||||
Id value is required
|
|
||||||
</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Range from</span><br>
|
<span class="mat-body">Range from</span><br>
|
||||||
<span class="mat-caption">Unit identifier range from.</span>
|
<span class="mat-caption">Unit identifier range from.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="1" step="1" matInput formControlName="unitFrom" placeholder="From">
|
<input type="number" min="1" step="1" matInput formControlName="unitFrom" placeholder="From">
|
||||||
<mat-error *ngIf="form?.get('unitFrom')?.hasError('range')">
|
<mat-error *ngIf="f.get('unitFrom')!.hasError('range')">From value should be lower than To value</mat-error>
|
||||||
From value should be lower than To value
|
<mat-error *ngIf="f.get('unitFrom')!.hasError('min')">From value is invalid</mat-error>
|
||||||
</mat-error>
|
</mat-form-field>
|
||||||
<mat-error *ngIf="form?.get('unitFrom')?.hasError('min')">
|
|
||||||
From value To is invalid
|
|
||||||
</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Range to</span><br>
|
<span class="mat-body">Range to</span><br>
|
||||||
<span class="mat-caption">Unit identifier range to.</span>
|
<span class="mat-caption">Unit identifier range to.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="number" min="1" step="1" matInput formControlName="unitTo" placeholder="To">
|
<input type="number" min="1" step="1" matInput formControlName="unitTo" placeholder="To">
|
||||||
<mat-error *ngIf="form?.get('unitTo')?.hasError('range')">
|
<mat-error *ngIf="f.get('unitTo')!.hasError('range')">To value should be higher than From value</mat-error>
|
||||||
To value should be higher than From value
|
<mat-error *ngIf="f.get('unitTo')!.hasError('min')">To value is invalid</mat-error>
|
||||||
</mat-error>
|
</mat-form-field>
|
||||||
<mat-error *ngIf="form?.get('unitTo')?.hasError('min')">
|
|
||||||
To value is invalid
|
|
||||||
</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body">Label</span><br>
|
<span class="mat-body">Label</span><br>
|
||||||
<span class="mat-caption">Unit label.</span>
|
<span class="mat-caption">Unit label.</span>
|
||||||
</p>
|
</p>
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
<mat-error *ngIf="f.get('label')!.hasError('required')">Label is required</mat-error>
|
||||||
Label is required
|
</mat-form-field>
|
||||||
</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row bottom">
|
<div class="row bottom">
|
||||||
<button type="button" mat-button color="warn" (click)="remove.emit()">
|
<button type="button" mat-button color="warn" (click)="remove.emit()">Delete unit</button>
|
||||||
Delete unit
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
@ -17,15 +17,17 @@
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-unit',
|
selector: 'rdio-scanner-admin-unit',
|
||||||
templateUrl: './unit.component.html',
|
templateUrl: './unit.component.html',
|
||||||
|
standalone: false,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminUnitComponent {
|
export class RdioScannerAdminUnitComponent {
|
||||||
@Input() form: FormGroup | undefined;
|
@Input() form: FormGroup | null = null;
|
||||||
|
|
||||||
@Output() remove = new EventEmitter<void>();
|
@Output() readonly remove = new EventEmitter<void>();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,79 @@
|
||||||
<div class="row top">
|
<div class="row top">
|
||||||
<p class="mat-body">All system talkgroups must be associated with a tag, which is then used to search for calls
|
<p class="mat-body">All system talkgroups must be associated with a tag, which is then used to search for calls
|
||||||
based on their tag.</p>
|
based on their tag.</p>
|
||||||
<button type="button" mat-button color="accent" (click)="add()">New Tag</button>
|
<button type="button" mat-button color="accent" (click)="add()">New Tag</button>
|
||||||
</div>
|
</div>
|
||||||
<p *ngIf="!tags?.length" class="mat-small text-center">No defined Tags</p>
|
@if (!tags.length) {
|
||||||
|
<p class="mat-small text-center">No defined Tags</p>
|
||||||
|
}
|
||||||
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="tags"
|
<mat-accordion displayMode="flat" cdkDropList [cdkDropListAutoScrollStep]=64 [cdkDropListData]="tags"
|
||||||
(cdkDropListDropped)="drop($event)">
|
(cdkDropListDropped)="drop($event)">
|
||||||
<mat-expansion-panel *ngFor="let tag of tags; index as i" cdkDrag>
|
@for (tag of tags; track tag; let i = $index) {
|
||||||
<mat-expansion-panel-header>
|
<mat-expansion-panel cdkDrag>
|
||||||
<mat-panel-title>
|
<mat-expansion-panel-header>
|
||||||
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
<mat-panel-title>
|
||||||
{{ tag.value.label || 'NewTag' }}
|
<mat-icon cdkDragHandle>drag_indicator</mat-icon>
|
||||||
<mat-icon *ngIf="tag.invalid" color="warn">error</mat-icon>
|
{{ tag.value.label || 'NewTag' }}
|
||||||
</mat-panel-title>
|
@if (tag.invalid) {
|
||||||
</mat-expansion-panel-header>
|
<mat-icon color="warn">error</mat-icon>
|
||||||
<ng-container [formGroup]="tag">
|
}
|
||||||
<div class="row">
|
</mat-panel-title>
|
||||||
<p>
|
</mat-expansion-panel-header>
|
||||||
<span class="mat-body">Label</span><br>
|
<ng-container [formGroup]="tag">
|
||||||
<span class="mat-caption">Tag label displayed on the main screen and on the search panel.</span>
|
<div class="row">
|
||||||
</p>
|
<p>
|
||||||
<mat-form-field floatLabel="auto">
|
<span class="mat-body">Label</span><br>
|
||||||
<input type="text" matInput formControlName="label" placeholder="Label">
|
<span class="mat-caption">Tag label displayed on the main screen and on the search panel.</span>
|
||||||
<mat-error *ngIf="form?.get('label')?.hasError('required')">
|
</p>
|
||||||
Label is required
|
<mat-form-field floatLabel="auto">
|
||||||
</mat-error>
|
<input type="text" matInput formControlName="label" placeholder="Label">
|
||||||
</mat-form-field>
|
@if (tag.get('label')!.hasError('required')) {
|
||||||
</div>
|
<mat-error>
|
||||||
<div class="row">
|
Label is required
|
||||||
<p>
|
</mat-error>
|
||||||
<span class="mat-body">Audio alert</span><br>
|
}
|
||||||
<span class="mat-caption">Prefixes an alert sound to all calls assigned to this tag. The alert
|
</mat-form-field>
|
||||||
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
</div>
|
||||||
</p>
|
<div class="row">
|
||||||
<mat-form-field floatLabel="auto">
|
<p>
|
||||||
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
|
<span class="mat-body">Audio alert</span><br>
|
||||||
<mat-option value="">No Alert</mat-option>
|
<span class="mat-caption">Prefixes an alert sound to all calls assigned to this tag. The alert
|
||||||
<mat-option *ngFor="let alert of alerts" [value]="alert">
|
assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||||
{{ alert | titlecase }}
|
</p>
|
||||||
</mat-option>
|
<mat-form-field floatLabel="auto">
|
||||||
</mat-select>
|
<mat-select formControlName="alert" placeholder="Alert" (selectionChange)="playAlert($event)">
|
||||||
</mat-form-field>
|
<mat-option value="">No Alert</mat-option>
|
||||||
</div>
|
@for (alert of alerts; track alert) {
|
||||||
<div class="row">
|
<mat-option [value]="alert">
|
||||||
<p>
|
{{ alert | titlecase }}
|
||||||
<span class="mat-body">Led Color</span><br>
|
</mat-option>
|
||||||
<span class="mat-caption">The default LED color is green but it can be changed at several levels.
|
}
|
||||||
The color assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
</mat-select>
|
||||||
</p>
|
</mat-form-field>
|
||||||
<mat-form-field floatLabel="auto">
|
</div>
|
||||||
<mat-select formControlName="led" placeholder="Color">
|
<div class="row">
|
||||||
<mat-option [value]="''">Default</mat-option>
|
<p>
|
||||||
<mat-option *ngFor="let led of leds" [value]="led">
|
<span class="mat-body">Led Color</span><br>
|
||||||
{{ led | titlecase }}
|
<span class="mat-caption">The default LED color is green but it can be changed at several levels.
|
||||||
</mat-option>
|
The color assignment order is as follows: Group -> Tag -> System -> Talkgroup.</span>
|
||||||
</mat-select>
|
</p>
|
||||||
</mat-form-field>
|
<mat-form-field floatLabel="auto">
|
||||||
</div>
|
<mat-select formControlName="led" placeholder="Color">
|
||||||
<div class="row bottom">
|
<mat-option [value]="''">Default</mat-option>
|
||||||
<button type="button" mat-button color="warn" (click)="remove(i)">
|
@for (led of leds; track led) {
|
||||||
Delete Tag
|
<mat-option [value]="led">
|
||||||
</button>
|
{{ led | titlecase }}
|
||||||
</div>
|
</mat-option>
|
||||||
</ng-container>
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="row bottom">
|
||||||
|
<button type="button" mat-button color="warn" (click)="remove(i)">
|
||||||
|
Delete Tag
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
|
}
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
|
@ -28,6 +28,7 @@ import { RdioScannerAdminService } from '../../admin.service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-tags',
|
selector: 'rdio-scanner-admin-tags',
|
||||||
templateUrl: './tags.component.html',
|
templateUrl: './tags.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminTagsComponent {
|
export class RdioScannerAdminTagsComponent {
|
||||||
@Input() form: FormArray | undefined;
|
@Input() form: FormArray | undefined;
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,17 @@
|
||||||
<p class="mat-body-2">Please enter the admin password to gain access to the administrative dashboard</p>
|
<p class="mat-body-2">Please enter the admin password to gain access to the administrative dashboard</p>
|
||||||
<mat-form-field hideRequiredMarker>
|
<mat-form-field hideRequiredMarker>
|
||||||
<mat-label>Password</mat-label>
|
<mat-label>Password</mat-label>
|
||||||
<input matInput formControlName="password" type="password" required>
|
<input
|
||||||
<mat-error>{{ message }}</mat-error>
|
matInput
|
||||||
|
formControlName="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
[attr.aria-invalid]="form.get('password')?.invalid"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<mat-error *ngIf="(form.get('password')?.invalid && form.get('password')?.touched) || message">
|
||||||
|
{{ message || 'Password is required' }}
|
||||||
|
</mat-error>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button mat-raised-button color="primary" type="submit" [disabled]="form.disabled || form.invalid">Login</button>
|
<button mat-raised-button color="primary" type="submit" [disabled]="form.invalid || form.disabled">Login</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -25,6 +25,7 @@ import { RdioScannerAdminService } from '../admin.service';
|
||||||
selector: 'rdio-scanner-admin-login',
|
selector: 'rdio-scanner-admin-login',
|
||||||
styleUrls: ['./login.component.scss'],
|
styleUrls: ['./login.component.scss'],
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminLoginComponent {
|
export class RdioScannerAdminLoginComponent {
|
||||||
@Output() loggedIn = new EventEmitter<void>();
|
@Output() loggedIn = new EventEmitter<void>();
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,91 @@
|
||||||
<mat-table [dataSource]="logs">
|
<mat-table [dataSource]="logs">
|
||||||
<ng-container matColumnDef="level">
|
<ng-container matColumnDef="level">
|
||||||
<mat-header-cell *matHeaderCellDef>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<span>Level</span>
|
<span>Level</span>
|
||||||
</mat-header-cell>
|
</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let log">
|
<mat-cell *matCellDef="let log">
|
||||||
<mat-icon *ngIf="log?.level === 'error'" class="error">error</mat-icon>
|
@if (log?.level === 'error') {
|
||||||
<mat-icon *ngIf="log?.level === 'info'" class="info">notifications</mat-icon>
|
<mat-icon class="error">error</mat-icon>
|
||||||
<mat-icon *ngIf="log?.level === 'warn'" class="warn">warning</mat-icon>
|
}
|
||||||
</mat-cell>
|
@if (log?.level === 'info') {
|
||||||
</ng-container>
|
<mat-icon class="info">notifications</mat-icon>
|
||||||
<ng-container matColumnDef="date">
|
}
|
||||||
<mat-header-cell *matHeaderCellDef>
|
@if (log?.level === 'warn') {
|
||||||
<span>Date</span>
|
<mat-icon class="warn">warning</mat-icon>
|
||||||
</mat-header-cell>
|
}
|
||||||
<mat-cell *matCellDef="let log">
|
</mat-cell>
|
||||||
<span>{{ log?.dateTime | date:'MM/dd' }}</span>
|
</ng-container>
|
||||||
</mat-cell>
|
<ng-container matColumnDef="date">
|
||||||
</ng-container>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<ng-container matColumnDef="time">
|
<span>Date</span>
|
||||||
<mat-header-cell *matHeaderCellDef>
|
</mat-header-cell>
|
||||||
<span>Time</span>
|
<mat-cell *matCellDef="let log">
|
||||||
</mat-header-cell>
|
<span>{{ log?.dateTime | date:'MM/dd' }}</span>
|
||||||
<mat-cell *matCellDef="let log">
|
</mat-cell>
|
||||||
<span>{{ log?.dateTime | date:'HH:mm' }}</span>
|
</ng-container>
|
||||||
</mat-cell>
|
<ng-container matColumnDef="time">
|
||||||
</ng-container>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<ng-container matColumnDef="message">
|
<span>Time</span>
|
||||||
<mat-header-cell *matHeaderCellDef>
|
</mat-header-cell>
|
||||||
<span>Message</span>
|
<mat-cell *matCellDef="let log">
|
||||||
</mat-header-cell>
|
<span>{{ log?.dateTime | date:'HH:mm' }}</span>
|
||||||
<mat-cell *matCellDef="let log">
|
</mat-cell>
|
||||||
<span>{{ log?.message }}</span>
|
</ng-container>
|
||||||
</mat-cell>
|
<ng-container matColumnDef="message">
|
||||||
</ng-container>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<mat-header-row *matHeaderRowDef="['level', 'date', 'time', 'message']">
|
<span>Message</span>
|
||||||
</mat-header-row>
|
</mat-header-cell>
|
||||||
<mat-row *matRowDef="let row; columns: ['level', 'date', 'time', 'message']">
|
<mat-cell *matCellDef="let log">
|
||||||
</mat-row>
|
<span>{{ log?.message }}</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<mat-header-row *matHeaderRowDef="['level', 'date', 'time', 'message']">
|
||||||
|
</mat-header-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: ['level', 'date', 'time', 'message']">
|
||||||
|
</mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
<mat-progress-bar color="primary" [mode]="logsQueryPending ? 'query' : 'determinate'">
|
<mat-progress-bar color="primary" [mode]="logsQueryPending ? 'query' : 'determinate'">
|
||||||
</mat-progress-bar>
|
</mat-progress-bar>
|
||||||
<mat-paginator [disabled]="logsQueryPending" [length]="logsQuery?.count" [hidePageSize]="true"
|
<mat-paginator [disabled]="logsQueryPending" [length]="logsQuery?.count" [hidePageSize]="true"
|
||||||
[pageSize]="logs.value.length" [showFirstLastButtons]="true" (page)="refresh()">
|
[pageSize]="logs.value.length" [showFirstLastButtons]="true" (page)="refresh()">
|
||||||
</mat-paginator>
|
</mat-paginator>
|
||||||
<form autocomplete="off" [formGroup]="form">
|
<form autocomplete="off" [formGroup]="form">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>
|
<mat-label>
|
||||||
Sort order
|
Sort order
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<mat-select formControlName="sort" (selectionChange)="formHandler()">
|
<mat-select formControlName="sort" (selectionChange)="formHandler()">
|
||||||
<mat-option [value]="1">
|
<mat-option [value]="1">
|
||||||
Ascending
|
Ascending
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-option [value]="-1">
|
<mat-option [value]="-1">
|
||||||
Descending
|
Descending
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>
|
<mat-label>
|
||||||
Date
|
Date
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<input matInput type="datetime-local" formControlName="date" [max]="logsQuery?.dateStop"
|
<input matInput type="datetime-local" formControlName="date" [max]="logsQuery?.dateStop"
|
||||||
[min]="logsQuery?.dateStart" placeholder="Choose a date" (change)="formHandler()">
|
[min]="logsQuery?.dateStart" placeholder="Choose a date" (change)="formHandler()">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>
|
<mat-label>
|
||||||
Level
|
Level
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<mat-select formControlName="level" (selectionChange)="formHandler()">
|
<mat-select formControlName="level" (selectionChange)="formHandler()">
|
||||||
<mat-option value="info">Informative</mat-option>
|
<mat-option value="info">Informative</mat-option>
|
||||||
<mat-option value="warn">Warning</mat-option>
|
<mat-option value="warn">Warning</mat-option>
|
||||||
<mat-option value="error">Error</mat-option>
|
<mat-option value="error">Error</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="reset">
|
<div class="reset">
|
||||||
<button type="button" mat-raised-button [disabled]="logsQueryPending" (click)="reload()">
|
<button type="button" mat-raised-button [disabled]="logsQueryPending" (click)="reload()">
|
||||||
Reload
|
Reload
|
||||||
</button>
|
</button>
|
||||||
<button type="button" mat-raised-button [disabled]="logsQueryPending || form.pristine" (click)="reset()">
|
<button type="button" mat-raised-button [disabled]="logsQueryPending || form.pristine" (click)="reset()">
|
||||||
Reset
|
Reset
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -27,6 +27,7 @@ import { Log, LogsQuery, LogsQueryOptions, RdioScannerAdminService } from '../ad
|
||||||
selector: 'rdio-scanner-admin-logs',
|
selector: 'rdio-scanner-admin-logs',
|
||||||
styleUrls: ['./logs.component.scss'],
|
styleUrls: ['./logs.component.scss'],
|
||||||
templateUrl: './logs.component.html',
|
templateUrl: './logs.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminLogsComponent {
|
export class RdioScannerAdminLogsComponent {
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
<mat-card appearance="outlined" *ngIf="todos.length">
|
<ng-container *ngIf="todos.length">
|
||||||
|
<mat-card appearance="outlined">
|
||||||
<mat-card-title>Todos</mat-card-title>
|
<mat-card-title>Todos</mat-card-title>
|
||||||
<mat-list>
|
<mat-list>
|
||||||
<mat-list-item *ngFor="let todo of todos">
|
<mat-list-item *ngFor="let todo of todos">
|
||||||
<mat-icon *ngIf="todo.level === 'info'" class="info" matListItemIcon>notifications</mat-icon>
|
<mat-icon *ngIf="todo.level === 'info'" class="info" matListItemIcon>notifications</mat-icon>
|
||||||
<mat-icon *ngIf="todo.level === 'warn'" class="warn" matListItemIcon>warning</mat-icon>
|
<mat-icon *ngIf="todo.level === 'warn'" class="warn" matListItemIcon>warning</mat-icon>
|
||||||
<div mat-line>
|
<div mat-line>
|
||||||
<span class="todo">{{ todo.message }}</span>
|
<span class="todo">{{ todo.message }}</span>
|
||||||
</div>
|
</div>
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
</ng-container>
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
.info {
|
.mat-mdc-list-item .info {
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warn {
|
.mat-mdc-list-item .warn {
|
||||||
color: orange;
|
color: orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
.todo {
|
.mat-mdc-list-item .todo {
|
||||||
text-overflow: unset;
|
text-overflow: unset;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ interface Todo {
|
||||||
selector: 'rdio-scanner-admin-todos',
|
selector: 'rdio-scanner-admin-todos',
|
||||||
styleUrls: ['./todos.component.scss'],
|
styleUrls: ['./todos.component.scss'],
|
||||||
templateUrl: './todos.component.html',
|
templateUrl: './todos.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminTodosComponent implements OnDestroy, OnInit {
|
export class RdioScannerAdminTodosComponent implements OnDestroy, OnInit {
|
||||||
todos: Todo[] = [];
|
todos: Todo[] = [];
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DOCUMENT } from '@angular/common';
|
|
||||||
import { Component, EventEmitter, Inject, Output } from '@angular/core';
|
import { Component, EventEmitter, Inject, Output, DOCUMENT } from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Config, RdioScannerAdminService } from '../../admin.service';
|
import { Config, RdioScannerAdminService } from '../../admin.service';
|
||||||
import packageInfo from '../../../../../../../package.json';
|
import packageInfo from '../../../../../../../package.json';
|
||||||
|
|
@ -27,6 +27,7 @@ import packageInfo from '../../../../../../../package.json';
|
||||||
selector: 'rdio-scanner-admin-import-export-config',
|
selector: 'rdio-scanner-admin-import-export-config',
|
||||||
styleUrls: ['./import-export-config.component.scss'],
|
styleUrls: ['./import-export-config.component.scss'],
|
||||||
templateUrl: './import-export-config.component.html',
|
templateUrl: './import-export-config.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminImportExportConfigComponent {
|
export class RdioScannerAdminImportExportConfigComponent {
|
||||||
@Output() config = new EventEmitter<Config>();
|
@Output() config = new EventEmitter<Config>();
|
||||||
|
|
|
||||||
|
|
@ -1,113 +1,119 @@
|
||||||
<p class="mat-body note">Before going any further, note that there is no need to preload talkgroups into a system as the
|
<p class="mat-body note">Before going any further, note that there is no need to preload talkgroups into a system as the
|
||||||
autopopulate feature will automatically create them as audio files are ingested. So give the autopopulate feature a
|
autopopulate feature will automatically create them as audio files are ingested. So give the autopopulate feature a
|
||||||
chance before trying to import talkgroups.</p>
|
chance before trying to import talkgroups.</p>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p class="mat-body">step 1: read the CSV file</p>
|
<p class="mat-body">step 1: read the CSV file</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<ng-container *ngIf="csv.length === 0">
|
@if (csv.length === 0) {
|
||||||
<button mat-raised-button (click)="input.click()">Read the CSV file</button>
|
<button mat-raised-button (click)="input.click()">Read the CSV file</button>
|
||||||
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
|
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
|
||||||
<br>
|
<br>
|
||||||
<p>
|
<p>
|
||||||
<span class="mat-body-1">Column format is: </span>
|
<span class="mat-body-1">Column format is: </span>
|
||||||
<span class="mat-body-1">Id</span>,
|
<span class="mat-body-1">Id</span>,
|
||||||
<span>unused</span>,
|
<span>unused</span>,
|
||||||
<span class="mat-body-1">Label</span>,
|
<span class="mat-body-1">Label</span>,
|
||||||
<span>unused</span>,
|
<span>unused</span>,
|
||||||
<span class="mat-body-1">Name</span>,
|
<span class="mat-body-1">Name</span>,
|
||||||
<span class="mat-body-1">Tag</span>,
|
<span class="mat-body-1">Tag</span>,
|
||||||
<span class="mat-body-1">Group</span>
|
<span class="mat-body-1">Group</span>
|
||||||
</p>
|
</p>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngIf="csv.length !== 0">
|
@if (csv.length !== 0) {
|
||||||
<button mat-raised-button (click)="csv=[]">Reset</button>
|
<button mat-raised-button (click)="csv=[]">Reset</button>
|
||||||
</ng-container>
|
}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p class="mat-body">step 2: select system</p>
|
<p class="mat-body">step 2: select system</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select [(ngModel)]="system">
|
<mat-select [(ngModel)]="system">
|
||||||
<mat-option *ngFor="let system of baseConfig.systems" [value]="system">
|
@for (system of baseConfig.systems; track system) {
|
||||||
{{ system.label}}
|
<mat-option [value]="system">
|
||||||
</mat-option>
|
{{ system.label}}
|
||||||
</mat-select>
|
</mat-option>
|
||||||
</mat-form-field>
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p class="mat-body">step 3: review talkgroups to import</p>
|
<p class="mat-body">step 3: review talkgroups to import</p>
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<mat-table #table [dataSource]="csv">
|
<mat-table #table [dataSource]="csv">
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<mat-header-cell *matHeaderCellDef>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<span>Id</span>
|
<span>Id</span>
|
||||||
</mat-header-cell>
|
</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg">
|
<mat-cell *matCellDef="let tg">
|
||||||
<span>{{ tg[0] }}</span>
|
<span>{{ tg[0] }}</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="label">
|
<ng-container matColumnDef="label">
|
||||||
<mat-header-cell *matHeaderCellDef>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<span>Label</span>
|
<span>Label</span>
|
||||||
</mat-header-cell>
|
</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg">
|
<mat-cell *matCellDef="let tg">
|
||||||
<span>{{ tg[2] }}</span>
|
<span>{{ tg[2] }}</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="description">
|
<ng-container matColumnDef="description">
|
||||||
<mat-header-cell *matHeaderCellDef>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<span>Description</span>
|
<span>Description</span>
|
||||||
</mat-header-cell>
|
</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg">
|
<mat-cell *matCellDef="let tg">
|
||||||
<span>{{ tg[4] }}</span>
|
<span>{{ tg[4] }}</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="tag">
|
<ng-container matColumnDef="tag">
|
||||||
<mat-header-cell *matHeaderCellDef>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<span>Tag</span>
|
<span>Tag</span>
|
||||||
</mat-header-cell>
|
</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg">
|
<mat-cell *matCellDef="let tg">
|
||||||
<span>{{ tg[5] }}</span>
|
<span>{{ tg[5] }}</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="group">
|
<ng-container matColumnDef="group">
|
||||||
<mat-header-cell *matHeaderCellDef>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<span>Group</span>
|
<span>Group</span>
|
||||||
</mat-header-cell>
|
</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg">
|
<mat-cell *matCellDef="let tg">
|
||||||
<span>{{ tg[6] }}</span>
|
<span>{{ tg[6] }}</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="action">
|
<ng-container matColumnDef="action">
|
||||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg; index as i">
|
<mat-cell *matCellDef="let tg; index as i">
|
||||||
<button mat-icon-button (click)="csv.splice(i, 1); table.renderRows()">
|
<button mat-icon-button (click)="csv.splice(i, 1); table.renderRows()">
|
||||||
<mat-icon>clear</mat-icon>
|
<mat-icon>clear</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<mat-header-row *matHeaderRowDef="tableColumns"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="tableColumns"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
|
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
</div>
|
</div>
|
||||||
<p *ngIf="!csv.length" class="mat-body text-center">No CSV data yet</p>
|
@if (!csv.length) {
|
||||||
</section>
|
<p class="mat-body text-center">No CSV data yet</p>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p class="mat-body">step 4: import to configuration</p>
|
<p class="mat-body">step 4: import to configuration</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<button mat-raised-button [disabled]="!csv.length" (click)="import()">
|
<button mat-raised-button [disabled]="!csv.length" (click)="import()">
|
||||||
Import to configuration
|
Import to configuration
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="csv.length > 800" class="mat-caption mat-error text-center text-uppercase">
|
@if (csv.length > 800) {
|
||||||
|
<p class="mat-caption mat-error text-center text-uppercase">
|
||||||
Warning, you are trying to import more than 800 talkgroups, {{csv.length}} in this case. The graphic interface
|
Warning, you are trying to import more than 800 talkgroups, {{csv.length}} in this case. The graphic interface
|
||||||
has not designed for this number of talkgroups. You can continue, but expect Rdio Scanner to be quite slow, if
|
has not designed for this number of talkgroups. You can continue, but expect Rdio Scanner to be quite slow, if
|
||||||
not unusable.
|
not unusable.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
}
|
||||||
|
</section>
|
||||||
|
|
@ -24,6 +24,7 @@ import { Config, RdioScannerAdminService, System } from '../../admin.service';
|
||||||
selector: 'rdio-scanner-admin-import-talkgroups',
|
selector: 'rdio-scanner-admin-import-talkgroups',
|
||||||
styleUrls: ['./import-talkgroups.component.scss'],
|
styleUrls: ['./import-talkgroups.component.scss'],
|
||||||
templateUrl: './import-talkgroups.component.html',
|
templateUrl: './import-talkgroups.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminImportTalkgroupsComponent implements OnInit {
|
export class RdioScannerAdminImportTalkgroupsComponent implements OnInit {
|
||||||
@Output() config = new EventEmitter<Config>();
|
@Output() config = new EventEmitter<Config>();
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,86 @@
|
||||||
<section>
|
<section>
|
||||||
<p class="mat-body">step 1: read the CSV file</p>
|
<p class="mat-body">step 1: read the CSV file</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<ng-container *ngIf="csv.length === 0">
|
@if (csv.length === 0) {
|
||||||
<button mat-raised-button (click)="input.click()">Read the CSV file</button>
|
<button mat-raised-button (click)="input.click()">Read the CSV file</button>
|
||||||
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
|
<input #input type="file" accept=".csv" style="display: none" (change)="read($event)">
|
||||||
<br>
|
<br>
|
||||||
<p class="mat-body-1">
|
<p class="mat-body-1">
|
||||||
<span>Column format is: </span>
|
<span>Column format is: </span>
|
||||||
<span>Id</span>,
|
<span>Id</span>,
|
||||||
<span>Label</span>,
|
<span>Label</span>,
|
||||||
</p>
|
</p>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngIf="csv.length !== 0">
|
@if (csv.length !== 0) {
|
||||||
<button mat-raised-button (click)="csv=[]">Reset</button>
|
<button mat-raised-button (click)="csv=[]">Reset</button>
|
||||||
</ng-container>
|
}
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p class="mat-body">step 2: select system</p>
|
<p class="mat-body">step 2: select system</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<mat-form-field floatLabel="auto">
|
<mat-form-field floatLabel="auto">
|
||||||
<mat-select [(ngModel)]="system">
|
<mat-select [(ngModel)]="system">
|
||||||
<mat-option *ngFor="let system of baseConfig.systems" [value]="system">
|
@for (system of baseConfig.systems; track system) {
|
||||||
{{ system.label}}
|
<mat-option [value]="system">
|
||||||
</mat-option>
|
{{ system.label}}
|
||||||
</mat-select>
|
</mat-option>
|
||||||
</mat-form-field>
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p class="mat-body">step 3: review talkgroups to import</p>
|
<p class="mat-body">step 3: review talkgroups to import</p>
|
||||||
<div class="scroll">
|
<div class="scroll">
|
||||||
<mat-table #table [dataSource]="csv">
|
<mat-table #table [dataSource]="csv">
|
||||||
<ng-container matColumnDef="id">
|
<ng-container matColumnDef="id">
|
||||||
<mat-header-cell *matHeaderCellDef>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<span>Id</span>
|
<span>Id</span>
|
||||||
</mat-header-cell>
|
</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg">
|
<mat-cell *matCellDef="let tg">
|
||||||
<span>{{ tg[0] }}</span>
|
<span>{{ tg[0] }}</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="label">
|
<ng-container matColumnDef="label">
|
||||||
<mat-header-cell *matHeaderCellDef>
|
<mat-header-cell *matHeaderCellDef>
|
||||||
<span>Label</span>
|
<span>Label</span>
|
||||||
</mat-header-cell>
|
</mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg">
|
<mat-cell *matCellDef="let tg">
|
||||||
<span>{{ tg[1] }}</span>
|
<span>{{ tg[1] }}</span>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container matColumnDef="action">
|
<ng-container matColumnDef="action">
|
||||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||||
<mat-cell *matCellDef="let tg; index as i">
|
<mat-cell *matCellDef="let tg; index as i">
|
||||||
<button mat-icon-button (click)="csv.splice(i, 1); table.renderRows()">
|
<button mat-icon-button (click)="csv.splice(i, 1); table.renderRows()">
|
||||||
<mat-icon>clear</mat-icon>
|
<mat-icon>clear</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-cell>
|
</mat-cell>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<mat-header-row *matHeaderRowDef="tableColumns"></mat-header-row>
|
<mat-header-row *matHeaderRowDef="tableColumns"></mat-header-row>
|
||||||
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
|
<mat-row *matRowDef="let row; columns: tableColumns"></mat-row>
|
||||||
</mat-table>
|
</mat-table>
|
||||||
</div>
|
</div>
|
||||||
<p *ngIf="!csv.length" class="mat-body text-center">No CSV data yet</p>
|
@if (!csv.length) {
|
||||||
</section>
|
<p class="mat-body text-center">No CSV data yet</p>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<p class="mat-body">step 4: import to configuration</p>
|
<p class="mat-body">step 4: import to configuration</p>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
<button mat-raised-button [disabled]="!csv.length || system === undefined" (click)="import()">
|
<button mat-raised-button [disabled]="!csv.length || system === undefined" (click)="import()">
|
||||||
Import to configuration
|
Import to configuration
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<p *ngIf="csv.length > 1000" class="mat-caption mat-error text-center text-uppercase">
|
@if (csv.length > 1000) {
|
||||||
|
<p class="mat-caption mat-error text-center text-uppercase">
|
||||||
Warning, you are trying to import more than 1000 units, {{csv.length}} in this case. The graphic interface has
|
Warning, you are trying to import more than 1000 units, {{csv.length}} in this case. The graphic interface has
|
||||||
not designed for this number of units. You can continue, but expect Rdio Scanner to be quite slow, if not
|
not designed for this number of units. You can continue, but expect Rdio Scanner to be quite slow, if not
|
||||||
unusable.
|
unusable.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
}
|
||||||
|
</section>
|
||||||
|
|
@ -24,6 +24,7 @@ import { Config, RdioScannerAdminService, System } from '../../admin.service';
|
||||||
selector: 'rdio-scanner-admin-import-units',
|
selector: 'rdio-scanner-admin-import-units',
|
||||||
styleUrls: ['./import-units.component.scss'],
|
styleUrls: ['./import-units.component.scss'],
|
||||||
templateUrl: './import-units.component.html',
|
templateUrl: './import-units.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminImportUnitsComponent implements OnInit{
|
export class RdioScannerAdminImportUnitsComponent implements OnInit{
|
||||||
@Output() config = new EventEmitter<Config>();
|
@Output() config = new EventEmitter<Config>();
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,39 @@
|
||||||
<p class="mat-body">
|
<p class="mat-body">
|
||||||
Change the administrator password which gives access to the server administration dashboard.
|
Change the administrator password which gives access to the server administration dashboard.
|
||||||
</p>
|
</p>
|
||||||
<form [formGroup]="form">
|
<form [formGroup]="form" (ngSubmit)="save()">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>Current password</mat-label>
|
<input matInput type="text" name="username" value="admin" autocomplete="username" tabindex="-1" aria-label="Username (for password manager)" readonly>
|
||||||
<input type="password" matInput formControlName="currentPassword" required>
|
</mat-form-field>
|
||||||
<mat-error *ngIf="form.get('currentPassword')?.hasError('required')">
|
<mat-form-field>
|
||||||
Current password is required
|
<mat-label>Current password</mat-label>
|
||||||
</mat-error>
|
<input type="password" matInput formControlName="currentPassword" required autocomplete="current-password">
|
||||||
</mat-form-field>
|
<mat-error *ngIf="form.get('currentPassword')?.hasError('required')">
|
||||||
<mat-form-field>
|
Current password is required
|
||||||
<mat-label>New password</mat-label>
|
</mat-error>
|
||||||
<input type="password" matInput formControlName="newPassword" required>
|
</mat-form-field>
|
||||||
<mat-error *ngIf="form.get('newPassword')?.hasError('required')">
|
<mat-form-field>
|
||||||
New password is required
|
<mat-label>New password</mat-label>
|
||||||
</mat-error>
|
<input type="password" matInput formControlName="newPassword" required minlength="8" autocomplete="new-password">
|
||||||
<mat-error *ngIf="form.get('newPassword')?.hasError('minlength')">
|
<mat-error *ngIf="form.get('newPassword')?.hasError('required')">
|
||||||
Password is too short
|
New password is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
<mat-error *ngIf="form.get('newPassword')?.hasError('minlength')">
|
||||||
<mat-form-field>
|
Password is too short
|
||||||
<mat-label>Verify new password</mat-label>
|
</mat-error>
|
||||||
<input type="password" matInput formControlName="verifyNewPassword" required>
|
</mat-form-field>
|
||||||
<mat-error *ngIf="form.get('verifyNewPassword')?.hasError('required')">
|
<mat-form-field>
|
||||||
New password verification is required
|
<mat-label>Verify new password</mat-label>
|
||||||
</mat-error>
|
<input type="password" matInput formControlName="verifyNewPassword" required autocomplete="new-password">
|
||||||
<mat-error *ngIf="form.get('verifyNewPassword')?.hasError('invalid')">
|
<mat-error *ngIf="form.get('verifyNewPassword')?.hasError('required')">
|
||||||
Password don't match
|
New password verification is required
|
||||||
</mat-error>
|
</mat-error>
|
||||||
</mat-form-field>
|
<mat-error *ngIf="form.hasError('passwordMismatch')">
|
||||||
<div class="row bottom">
|
Passwords don't match
|
||||||
<button type="button" mat-raised-button [disabled]="form.disabled || form.untouched"
|
</mat-error>
|
||||||
(click)="reset();">Reset</button>
|
</mat-form-field>
|
||||||
<button type="button" mat-raised-button color="primary"
|
<div class="row bottom">
|
||||||
[disabled]="form.disabled || form.pristine || !form.valid" (click)="save()">Save</button>
|
<button type="button" mat-raised-button [disabled]="form.disabled || form.untouched" (click)="reset()">Reset</button>
|
||||||
</div>
|
<button type="submit" mat-raised-button color="primary" [disabled]="form.disabled || form.pristine || form.invalid">Save</button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -18,61 +18,68 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { AbstractControl, FormBuilder, FormGroup, ValidatorFn, ValidationErrors, Validators } from '@angular/forms';
|
import { AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validators } from '@angular/forms';
|
||||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { RdioScannerAdminService } from '../../admin.service';
|
import { RdioScannerAdminService } from '../../admin.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-password',
|
selector: 'rdio-scanner-admin-password',
|
||||||
styleUrls: ['./password.component.scss'],
|
styleUrls: ['./password.component.scss'],
|
||||||
templateUrl: './password.component.html',
|
templateUrl: './password.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminPasswordComponent {
|
export class RdioScannerAdminPasswordComponent {
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private adminService: RdioScannerAdminService,
|
private readonly adminService: RdioScannerAdminService,
|
||||||
private matSnackBar: MatSnackBar,
|
private readonly matSnackBar: MatSnackBar,
|
||||||
private ngFormBuilder: FormBuilder,
|
private readonly ngFormBuilder: FormBuilder,
|
||||||
) {
|
) {
|
||||||
this.form = this.ngFormBuilder.group({
|
this.form = this.ngFormBuilder.group({
|
||||||
currentPassword: [null, Validators.required],
|
currentPassword: [null, Validators.required],
|
||||||
newPassword: [null, [Validators.required, Validators.minLength(8)]],
|
newPassword: [null, [Validators.required, Validators.minLength(8)]],
|
||||||
verifyNewPassword: [null, [Validators.required, this.validatePassword()]],
|
verifyNewPassword: [null, Validators.required],
|
||||||
});
|
}, { validators: this.passwordsMatchValidator });
|
||||||
|
|
||||||
|
this.form.get('newPassword')?.valueChanges.subscribe(() =>
|
||||||
|
this.form.get('verifyNewPassword')?.updateValueAndValidity({ onlySelf: true })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private validatePassword(): ValidatorFn {
|
private passwordsMatchValidator(group: AbstractControl): ValidationErrors | null {
|
||||||
return (control: AbstractControl): ValidationErrors | null => {
|
const newPwd = group.get('newPassword')?.value;
|
||||||
return typeof control.value === 'string' && control.value.length
|
const verify = group.get('verifyNewPassword')?.value;
|
||||||
? control.value === control.parent?.value.newPassword
|
return newPwd && verify && newPwd !== verify ? { passwordMismatch: true } : null;
|
||||||
? null : { invalid: true } : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get currentPassword() { return this.form.get('currentPassword'); }
|
||||||
|
get newPassword() { return this.form.get('newPassword'); }
|
||||||
|
get verifyNewPassword() { return this.form.get('verifyNewPassword'); }
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.form.reset();
|
this.form.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(): Promise<void> {
|
async save(): Promise<void> {
|
||||||
const config: MatSnackBarConfig = { duration: 5000 };
|
if (this.form.invalid) {
|
||||||
const currentPassword = this.form.get('currentPassword')?.value;
|
this.form.markAllAsTouched();
|
||||||
const newPassword = this.form.get('newPassword')?.value;
|
return;
|
||||||
|
|
||||||
this.form.disable();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (currentPassword && newPassword) {
|
|
||||||
await this.adminService.changePassword(currentPassword, newPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.matSnackBar.open('Password changed successfully', '', config);
|
|
||||||
|
|
||||||
this.form.reset();
|
|
||||||
} catch (_) {
|
|
||||||
this.matSnackBar.open('Unable to change password', '', config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.form.enable();
|
const config = { duration: 5000 };
|
||||||
|
const currentPassword = this.currentPassword?.value;
|
||||||
|
const newPassword = this.newPassword?.value;
|
||||||
|
|
||||||
|
this.form.disable();
|
||||||
|
try {
|
||||||
|
await this.adminService.changePassword(currentPassword, newPassword);
|
||||||
|
this.matSnackBar.open('Password changed successfully', '', config);
|
||||||
|
this.form.reset();
|
||||||
|
} catch {
|
||||||
|
this.matSnackBar.open('Unable to change password', '', config);
|
||||||
|
} finally {
|
||||||
|
this.form.enable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import { Config } from '../admin.service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-admin-tools',
|
selector: 'rdio-scanner-admin-tools',
|
||||||
templateUrl: './tools.component.html',
|
templateUrl: './tools.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminToolsComponent {
|
export class RdioScannerAdminToolsComponent {
|
||||||
@Output() config = new EventEmitter<Config>();
|
@Output() config = new EventEmitter<Config>();
|
||||||
|
|
|
||||||
|
|
@ -1,177 +1,193 @@
|
||||||
<div class="rdio-status">
|
<div class="rdio-status">
|
||||||
<div class="branding">{{ branding }}</div>
|
<div class="branding">{{ branding }}</div>
|
||||||
<div class="led" [ngClass]="ledStyle"></div>
|
<div class="led" [ngClass]="ledStyle"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rdio-display" [ngClass]="{ idle: !dimmer }" (dblclick)="toggleFullscreen.emit()">
|
<div class="rdio-display" [ngClass]="{ idle: !dimmer }" (dblclick)="toggleFullscreen.emit()">
|
||||||
<div class="row three">
|
<div class="row three">
|
||||||
<div>
|
<div>
|
||||||
<span>{{ clock | date:timeFormat }}</span>
|
<span>{{ clock | date:timeFormat }}</span>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span *ngIf="!linked">NO LINK</span>
|
|
||||||
<span *ngIf="linked && showListenersCount">L: {{ listeners }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>Q: {{ callQueue }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row small"></div>
|
<div>
|
||||||
<div class="row">
|
@if (!linked) {
|
||||||
<div>
|
<span>NO LINK</span>
|
||||||
<span>{{ callSystem }}</span>
|
}
|
||||||
</div>
|
@if (linked && showListenersCount) {
|
||||||
<div>
|
<span>L: {{ listeners }}</span>
|
||||||
<span>{{ type | uppercase }}</span>
|
}
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span>{{ callTag }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div>
|
||||||
<div>
|
<span>Q: {{ callQueue }}</span>
|
||||||
<span>{{ callTalkgroup }}</span>
|
|
||||||
</div>
|
|
||||||
<div></div>
|
|
||||||
<div>
|
|
||||||
<span>{{ callDate | date:'MM/dd' }} </span>
|
|
||||||
<span>{{ callProgress | date:timeFormat }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row big full">
|
</div>
|
||||||
<span>{{ callTalkgroupName }}</span>
|
<div class="row small"></div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<span>{{ callSystem }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div>
|
||||||
<div>
|
<span>{{ type | uppercase }}</span>
|
||||||
<span>F: {{ callFrequency || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
<div></div>
|
|
||||||
<div>
|
|
||||||
<span>TGID: {{ callTalkgroupId || 0 }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div>
|
||||||
<div>
|
<span>{{ callTag }}</span>
|
||||||
<span>E: {{ callError || 0 }} S: {{ callSpike || 0 }} </span>
|
|
||||||
</div>
|
|
||||||
<div></div>
|
|
||||||
<div>
|
|
||||||
<span *ngIf="callUnit">UID: {{ callUnit }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row right small">
|
</div>
|
||||||
<div></div>
|
<div class="row">
|
||||||
<div></div>
|
<div>
|
||||||
|
<span>{{ callTalkgroup }}</span>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
<span>{{ callDate | date:'MM/dd' }} </span>
|
||||||
|
<span>{{ callProgress | date:timeFormat }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row big full">
|
||||||
|
<span>{{ callTalkgroupName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<span>F: {{ callFrequency || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
<span>TGID: {{ callTalkgroupId || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<span>E: {{ callError || 0 }} S: {{ callSpike || 0 }} </span>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
@if (callUnit) {
|
||||||
|
<span>UID: {{ callUnit }}</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row right small">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
@if (tempAvoid) {
|
||||||
<div>
|
<div>
|
||||||
<div *ngIf="tempAvoid">
|
<span class="flag" [ngClass]="{ flaged: avoided || patched }">⏲︎ {{ tempAvoid }}M</span>
|
||||||
<span class="flag" [ngClass]="{ flaged: avoided || patched }">⏲︎ {{ tempAvoid }}M</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="flag" [ngClass]="{ flaged: avoided }">AVOID</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="flag" [ngClass]="{ flaged: patched }">PATCH</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="flag" [ngClass]="{ flaged: delayed }">DELAYED</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<span class="flag" [ngClass]="{ flaged: avoided }">AVOID</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="flag" [ngClass]="{ flaged: patched }">PATCH</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="flag" [ngClass]="{ flaged: delayed }">DELAYED</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="wrapper">
|
</div>
|
||||||
<div class="auth" [ngClass]="{ visible: auth }">
|
<div class="wrapper">
|
||||||
<form class="mat-elevation-z8" autocomplete="off" [formGroup]="authForm" (ngSubmit)="authenticate()">
|
<div class="auth" [ngClass]="{ visible: auth }">
|
||||||
<mat-form-field>
|
<form class="mat-elevation-z8" autocomplete="off" [formGroup]="authForm" (ngSubmit)="authenticate()">
|
||||||
<input #password matInput type="password" autocomplete="off" formControlName="password"
|
<mat-form-field>
|
||||||
placeholder="Unlock code" (blur)="authFocus()">
|
<input #password matInput type="password" autocomplete="off" formControlName="password"
|
||||||
<mat-error *ngIf="authForm.get('password')?.hasError('expired')">
|
placeholder="Unlock code" (blur)="authFocus()">
|
||||||
This unlock code has expired
|
@if (authForm.get('password')?.hasError('expired')) {
|
||||||
</mat-error>
|
<mat-error>
|
||||||
<mat-error *ngIf="authForm.get('password')?.hasError('tooMany')">
|
This unlock code has expired
|
||||||
Too many connections
|
</mat-error>
|
||||||
</mat-error>
|
}
|
||||||
</mat-form-field>
|
@if (authForm.get('password')?.hasError('tooMany')) {
|
||||||
</form>
|
<mat-error>
|
||||||
</div>
|
Too many connections
|
||||||
<table class="history">
|
</mat-error>
|
||||||
<thead>
|
}
|
||||||
<tr>
|
</mat-form-field>
|
||||||
<th>
|
</form>
|
||||||
<span>Time</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<span>System</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<span>Talkgroup</span>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<span>Name</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let previousCall of callHistory"
|
|
||||||
[ngClass]="{ 'replaying': previousCall?.id !== null && previousCall?.id === call?.id} ">
|
|
||||||
<td>
|
|
||||||
<span>{{ previousCall?.dateTime | date:timeFormat }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>{{ previousCall?.systemData?.label || previousCall?.system}}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>{{ previousCall?.talkgroupData?.label || previousCall?.talkgroup }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span>{{ previousCall?.talkgroupData?.name || previousCall?.frequency }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
<table class="history">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<span>Time</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span>System</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span>Talkgroup</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span>Name</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (previousCall of callHistory; track $index) {
|
||||||
|
<tr
|
||||||
|
[ngClass]="{ 'replaying': previousCall?.id !== null && previousCall?.id === call?.id} ">
|
||||||
|
<td>
|
||||||
|
<span>{{ previousCall?.dateTime | date:timeFormat }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>{{ previousCall?.systemData?.label || previousCall?.system}}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>{{ previousCall?.talkgroupData?.label || previousCall?.talkgroup }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>{{ previousCall?.talkgroupData?.name || previousCall?.frequency }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rdio-control">
|
<div class="rdio-control">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button class="rdio-button" (click)="livefeed()"
|
<button class="rdio-button" (click)="livefeed()"
|
||||||
[ngClass]="{ off: livefeedOffline && !playbackMode, on: livefeedOnline, partial: playbackMode }">
|
[ngClass]="{ off: livefeedOffline && !playbackMode, on: livefeedOnline, partial: playbackMode }">
|
||||||
LIVE<br>FEED
|
LIVE<br>FEED
|
||||||
</button>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<button class="rdio-button" (click)="holdSystem()" [ngClass]="{ off: !holdSys, on: holdSys }">
|
|
||||||
HOLD<br>SYS
|
|
||||||
</button>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<button class="rdio-button" (click)="holdTalkgroup()" [ngClass]="{ off: !holdTg, on: holdTg }">
|
|
||||||
HOLD<br>TG
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button class="rdio-button" (click)="replay()">
|
|
||||||
REPLAY<br>LAST
|
|
||||||
</button>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<button class="rdio-button" (click)="skip()">
|
|
||||||
SKIP<br>NEXT
|
|
||||||
</button>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<button class="rdio-button" (click)="avoid()">
|
|
||||||
AVOID
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button class="rdio-button" (click)="showSearchPanel()">
|
|
||||||
SEARCH<br>CALL
|
|
||||||
</button>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<button class="rdio-button" (click)="pause()" [ngClass]="{ off: !livefeedPaused, on: livefeedPaused }">
|
|
||||||
PAUSE
|
|
||||||
</button>
|
|
||||||
<div class="spacer"></div>
|
|
||||||
<button class="rdio-button" (click)="showSelectPanel()">
|
|
||||||
SELECT<br>TG
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="rdio-help" *ngIf="email">
|
|
||||||
<button mat-icon-button (click)="showHelp()">
|
|
||||||
<mat-icon>help_center</mat-icon>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
<div class="spacer"></div>
|
||||||
|
<button class="rdio-button" (click)="holdSystem()" [ngClass]="{ off: !holdSys, on: holdSys }">
|
||||||
|
HOLD<br>SYS
|
||||||
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="rdio-button" (click)="holdTalkgroup()" [ngClass]="{ off: !holdTg, on: holdTg }">
|
||||||
|
HOLD<br>TG
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="rdio-button" (click)="replay()">
|
||||||
|
REPLAY<br>LAST
|
||||||
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="rdio-button" (click)="skip()">
|
||||||
|
SKIP<br>NEXT
|
||||||
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="rdio-button" (click)="avoid()">
|
||||||
|
AVOID
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="rdio-button" (click)="showSearchPanel()">
|
||||||
|
SEARCH<br>CALL
|
||||||
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="rdio-button" (click)="pause()" [ngClass]="{ off: !livefeedPaused, on: livefeedPaused }">
|
||||||
|
PAUSE
|
||||||
|
</button>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<button class="rdio-button" (click)="showSelectPanel()">
|
||||||
|
SELECT<br>TG
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (email) {
|
||||||
|
<div class="rdio-help">
|
||||||
|
<button mat-icon-button (click)="showHelp()">
|
||||||
|
<mat-icon>help_center</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
@ -42,6 +42,7 @@ import { RdioScannerSupportComponent } from './support/support.component';
|
||||||
'./main.component.scss',
|
'./main.component.scss',
|
||||||
],
|
],
|
||||||
templateUrl: './main.component.html',
|
templateUrl: './main.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerMainComponent implements OnDestroy, OnInit {
|
export class RdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
auth = false;
|
auth = false;
|
||||||
|
|
@ -530,7 +531,7 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
private isAfsSystem(call: RdioScannerCall): boolean {
|
private isAfsSystem(call: RdioScannerCall): boolean {
|
||||||
return (call.systemData?.type == 'provoice' ?? false) || (call.talkgroupData?.type === 'provoice' ?? false);
|
return (call.systemData?.type === 'provoice') || (call.talkgroupData?.type === 'provoice');
|
||||||
}
|
}
|
||||||
|
|
||||||
private syncClock(): void {
|
private syncClock(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<span class="support">
|
<span class="support">
|
||||||
<ng-container *ngIf="email">
|
@if (email) {
|
||||||
For further assistance please contact <b><a href="mailto:{{ email }}" target="_blank">{{ email }}</a></b>
|
For further assistance please contact <b><a href="mailto:{{ email }}" target="_blank">{{ email }}</a></b>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngIf="!email">
|
@if (!email) {
|
||||||
For further assistance please go to <b><a href="https://github.com/chuot/rdio-scanner/discussions"
|
For further assistance please go to <b><a href="https://github.com/chuot/rdio-scanner/discussions"
|
||||||
target="_blank">Rdio Scanner discussions</a></b>
|
target="_blank">Rdio Scanner discussions</a></b>
|
||||||
</ng-container>
|
}
|
||||||
</span>
|
</span>
|
||||||
<span class="mat-caption countdown">{{ countdown }}</span>
|
<span class="mat-caption countdown">{{ countdown }}</span>
|
||||||
|
|
@ -25,6 +25,7 @@ import { timer } from 'rxjs';
|
||||||
selector: 'RdioScannerSupport',
|
selector: 'RdioScannerSupport',
|
||||||
styleUrls: ['./support.component.scss'],
|
styleUrls: ['./support.component.scss'],
|
||||||
templateUrl: './support.component.html',
|
templateUrl: './support.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerSupportComponent {
|
export class RdioScannerSupportComponent {
|
||||||
countdown = 10;
|
countdown = 10;
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-store-badge',
|
selector: 'app-store-badge',
|
||||||
templateUrl: './app-store-badge.component.svg',
|
templateUrl: './app-store-badge.component.svg',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AppStoreBadgeComponent {}
|
export class AppStoreBadgeComponent {}
|
||||||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'google-play-badge',
|
selector: 'google-play-badge',
|
||||||
templateUrl: './google-play-badge.component.svg',
|
templateUrl: './google-play-badge.component.svg',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class GooglePlayBadgeComponent {}
|
export class GooglePlayBadgeComponent {}
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<p>Do you know that Rdio Scanner works natively on {{ deviceType }} devices?</p>
|
<p>Do you know that Rdio Scanner works natively on {{ deviceType }} devices?</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ng-container *ngIf="isAndroid">
|
@if (isAndroid) {
|
||||||
<a href="https://play.google.com/store/apps/details?id=solutions.saubeo.rdioScanner" target="_blank">
|
<a href="https://play.google.com/store/apps/details?id=solutions.saubeo.rdioScanner" target="_blank">
|
||||||
<google-play-badge></google-play-badge>
|
<google-play-badge></google-play-badge>
|
||||||
</a>
|
</a>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngIf="isApple">
|
@if (isApple) {
|
||||||
<a href="https://apps.apple.com/us/app/rdio-scanner/id1563065667#?platform=iphone" target="_blank">
|
<a href="https://apps.apple.com/us/app/rdio-scanner/id1563065667#?platform=iphone" target="_blank">
|
||||||
<app-store-badge></app-store-badge>
|
<app-store-badge></app-store-badge>
|
||||||
</a>
|
</a>
|
||||||
</ng-container>
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="countdown">{{ countdown }}</div>
|
<div class="countdown">{{ countdown }}</div>
|
||||||
|
|
@ -25,6 +25,7 @@ import { timer } from 'rxjs';
|
||||||
selector: 'RdioScannerNative',
|
selector: 'RdioScannerNative',
|
||||||
styleUrls: ['./native.component.scss'],
|
styleUrls: ['./native.component.scss'],
|
||||||
templateUrl: './native.component.html',
|
templateUrl: './native.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerNativeComponent implements OnInit {
|
export class RdioScannerNativeComponent implements OnInit {
|
||||||
countdown = 10;
|
countdown = 10;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { RdioScannerNativeComponent } from './native/native.component';
|
||||||
selector: 'rdio-scanner',
|
selector: 'rdio-scanner',
|
||||||
styleUrls: ['./rdio-scanner.component.scss'],
|
styleUrls: ['./rdio-scanner.component.scss'],
|
||||||
templateUrl: './rdio-scanner.component.html',
|
templateUrl: './rdio-scanner.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerComponent implements OnDestroy, OnInit {
|
export class RdioScannerComponent implements OnDestroy, OnInit {
|
||||||
private eventSubscription;
|
private eventSubscription;
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DOCUMENT } from '@angular/common';
|
|
||||||
import { EventEmitter, Inject, Injectable, OnDestroy } from '@angular/core';
|
import { EventEmitter, Inject, Injectable, OnDestroy, DOCUMENT } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { interval, Subscription, timer } from 'rxjs';
|
import { interval, Subscription, timer } from 'rxjs';
|
||||||
import { takeWhile } from 'rxjs/operators';
|
import { takeWhile } from 'rxjs/operators';
|
||||||
|
|
|
||||||
|
|
@ -1,165 +1,181 @@
|
||||||
<mat-card appearance="outlined" class="rdio-list">
|
<mat-card appearance="outlined" class="rdio-list">
|
||||||
<mat-table [dataSource]="results">
|
<mat-table [dataSource]="results">
|
||||||
<ng-container matColumnDef="control">
|
<ng-container matColumnDef="control">
|
||||||
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef></mat-header-cell>
|
||||||
<mat-cell *matCellDef="let row">
|
<mat-cell *matCellDef="let row">
|
||||||
<button *ngIf="downloadMode.checked && row" mat-icon-button (click)="download(+row.id)">
|
@if (downloadMode.checked && row) {
|
||||||
<mat-icon>save_alt</mat-icon>
|
<button mat-icon-button (click)="download(+row.id)">
|
||||||
</button>
|
|
||||||
<button *ngIf="row && !downloadMode.checked && !paused && row?.id != call?.id && row?.id != callPending"
|
|
||||||
mat-icon-button (click)="play(+row.id)">
|
|
||||||
<mat-icon>play_arrow</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="row && !downloadMode.checked && row?.id == callPending" mat-icon-button>
|
|
||||||
<mat-icon class="spin">cached</mat-icon>
|
|
||||||
</button>
|
|
||||||
<button *ngIf="row && !downloadMode.checked && row?.id == call?.id" mat-icon-button (click)="stop()">
|
|
||||||
<mat-icon>stop</mat-icon>
|
|
||||||
</button>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container matColumnDef="date">
|
|
||||||
<mat-header-cell *matHeaderCellDef>
|
|
||||||
<span>Date</span>
|
|
||||||
</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
<span>{{ row?.dateTime | date:'MM/dd' }}</span>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container matColumnDef="time">
|
|
||||||
<mat-header-cell *matHeaderCellDef [ngClass]="{'time12h': time12h}">
|
|
||||||
<span>Time</span>
|
|
||||||
</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row" [ngClass]="{'time12h': time12h}">
|
|
||||||
<span>{{ row?.dateTime | date: time12h ? 'h:mm a' : 'HH:mm' }}</span>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container matColumnDef="system">
|
|
||||||
<mat-header-cell *matHeaderCellDef>
|
|
||||||
<span>System</span>
|
|
||||||
</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
<span>{{ row?.systemData?.label || row?.system }}</span>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container matColumnDef="alpha">
|
|
||||||
<mat-header-cell *matHeaderCellDef>
|
|
||||||
<span>Talkgroup</span>
|
|
||||||
</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
<span>{{ row?.talkgroupData?.label || row?.talkgroup }}</span>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container matColumnDef="name">
|
|
||||||
<mat-header-cell *matHeaderCellDef>
|
|
||||||
<span>Name</span>
|
|
||||||
</mat-header-cell>
|
|
||||||
<mat-cell *matCellDef="let row">
|
|
||||||
<span>{{ row?.talkgroupData?.name }}</span>
|
|
||||||
</mat-cell>
|
|
||||||
</ng-container>
|
|
||||||
<mat-header-row *matHeaderRowDef="['control', 'date', 'time', 'system', 'alpha', 'name']">
|
|
||||||
</mat-header-row>
|
|
||||||
<mat-row *matRowDef="let row; columns: ['control', 'date', 'time', 'system', 'alpha', 'name']">
|
|
||||||
</mat-row>
|
|
||||||
</mat-table>
|
|
||||||
<mat-progress-bar color="primary" [mode]="resultsPending ? 'query' : 'determinate'">
|
|
||||||
</mat-progress-bar>
|
|
||||||
<div class="paginator">
|
|
||||||
<mat-slide-toggle #downloadMode color="primary" labelPosition="before">
|
|
||||||
<mat-icon>save_alt</mat-icon>
|
<mat-icon>save_alt</mat-icon>
|
||||||
</mat-slide-toggle>
|
</button>
|
||||||
<mat-paginator [disabled]="livefeedPlayback || resultsPending" [length]="playbackList?.count"
|
}
|
||||||
[hidePageSize]="true" [pageSize]="results.value.length" [showFirstLastButtons]="true"
|
@if (row && !downloadMode.checked && !paused && row?.id != call?.id && row?.id != callPending) {
|
||||||
(page)="refreshResults()">
|
<button
|
||||||
</mat-paginator>
|
mat-icon-button (click)="play(+row.id)">
|
||||||
</div>
|
<mat-icon>play_arrow</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (row && !downloadMode.checked && row?.id == callPending) {
|
||||||
|
<button mat-icon-button>
|
||||||
|
<mat-icon class="spin">cached</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
@if (row && !downloadMode.checked && row?.id == call?.id) {
|
||||||
|
<button mat-icon-button (click)="stop()">
|
||||||
|
<mat-icon>stop</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="date">
|
||||||
|
<mat-header-cell *matHeaderCellDef>
|
||||||
|
<span>Date</span>
|
||||||
|
</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row">
|
||||||
|
<span>{{ row?.dateTime | date:'MM/dd' }}</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="time">
|
||||||
|
<mat-header-cell *matHeaderCellDef [ngClass]="{'time12h': time12h}">
|
||||||
|
<span>Time</span>
|
||||||
|
</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row" [ngClass]="{'time12h': time12h}">
|
||||||
|
<span>{{ row?.dateTime | date: time12h ? 'h:mm a' : 'HH:mm' }}</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="system">
|
||||||
|
<mat-header-cell *matHeaderCellDef>
|
||||||
|
<span>System</span>
|
||||||
|
</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row">
|
||||||
|
<span>{{ row?.systemData?.label || row?.system }}</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="alpha">
|
||||||
|
<mat-header-cell *matHeaderCellDef>
|
||||||
|
<span>Talkgroup</span>
|
||||||
|
</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row">
|
||||||
|
<span>{{ row?.talkgroupData?.label || row?.talkgroup }}</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<ng-container matColumnDef="name">
|
||||||
|
<mat-header-cell *matHeaderCellDef>
|
||||||
|
<span>Name</span>
|
||||||
|
</mat-header-cell>
|
||||||
|
<mat-cell *matCellDef="let row">
|
||||||
|
<span>{{ row?.talkgroupData?.name }}</span>
|
||||||
|
</mat-cell>
|
||||||
|
</ng-container>
|
||||||
|
<mat-header-row *matHeaderRowDef="['control', 'date', 'time', 'system', 'alpha', 'name']">
|
||||||
|
</mat-header-row>
|
||||||
|
<mat-row *matRowDef="let row; columns: ['control', 'date', 'time', 'system', 'alpha', 'name']">
|
||||||
|
</mat-row>
|
||||||
|
</mat-table>
|
||||||
|
<mat-progress-bar color="primary" [mode]="resultsPending ? 'query' : 'determinate'">
|
||||||
|
</mat-progress-bar>
|
||||||
|
<div class="paginator">
|
||||||
|
<mat-slide-toggle #downloadMode color="primary" labelPosition="before">
|
||||||
|
<mat-icon>save_alt</mat-icon>
|
||||||
|
</mat-slide-toggle>
|
||||||
|
<mat-paginator [disabled]="livefeedPlayback || resultsPending" [length]="playbackList?.count"
|
||||||
|
[hidePageSize]="true" [pageSize]="results.value.length" [showFirstLastButtons]="true"
|
||||||
|
(page)="refreshResults()">
|
||||||
|
</mat-paginator>
|
||||||
|
</div>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
<mat-card appearance="outlined" class="rdio-filters">
|
<mat-card appearance="outlined" class="rdio-filters">
|
||||||
<form autocomplete="off" [formGroup]="form">
|
<form autocomplete="off" [formGroup]="form">
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>
|
<mat-label>
|
||||||
Sort order
|
Sort order
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<mat-select formControlName="sort" (selectionChange)="formChangeHandler()">
|
<mat-select formControlName="sort" (selectionChange)="formChangeHandler()">
|
||||||
<mat-option [value]="1">
|
<mat-option [value]="1">
|
||||||
Ascending
|
Ascending
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-option [value]="-1">
|
<mat-option [value]="-1">
|
||||||
Descending
|
Descending
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>
|
<mat-label>
|
||||||
Date
|
Date
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<input matInput type="datetime-local" formControlName="date" [max]="playbackList?.dateStop"
|
<input matInput type="datetime-local" formControlName="date" [max]="playbackList?.dateStop"
|
||||||
[min]="playbackList?.dateStart" placeholder="Choose a date" (change)="formChangeHandler()">
|
[min]="playbackList?.dateStart" placeholder="Choose a date" (change)="formChangeHandler()">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field>
|
<mat-form-field>
|
||||||
<mat-label>
|
<mat-label>
|
||||||
System
|
System
|
||||||
</mat-label>
|
</mat-label>
|
||||||
<mat-select formControlName="system" (selectionChange)="formChangeHandler()">
|
<mat-select formControlName="system" (selectionChange)="formChangeHandler()">
|
||||||
<mat-option [value]="-1">
|
<mat-option [value]="-1">
|
||||||
All Systems
|
All Systems
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-option *ngFor="let system of optionsSystem; index as i" [value]="i">
|
@for (system of optionsSystem; track system; let i = $index) {
|
||||||
{{ system }}
|
<mat-option [value]="i">
|
||||||
</mat-option>
|
{{ system }}
|
||||||
</mat-select>
|
</mat-option>
|
||||||
</mat-form-field>
|
}
|
||||||
<mat-form-field>
|
</mat-select>
|
||||||
<mat-label>
|
</mat-form-field>
|
||||||
Talkgroup
|
<mat-form-field>
|
||||||
</mat-label>
|
<mat-label>
|
||||||
<mat-select formControlName="talkgroup" (selectionChange)="formChangeHandler()">
|
Talkgroup
|
||||||
<mat-option [value]="-1">
|
</mat-label>
|
||||||
All Talkgroups
|
<mat-select formControlName="talkgroup" (selectionChange)="formChangeHandler()">
|
||||||
</mat-option>
|
<mat-option [value]="-1">
|
||||||
<mat-option *ngFor="let talkgroup of optionsTalkgroup; index as i" [value]="i">
|
All Talkgroups
|
||||||
{{ talkgroup }}
|
</mat-option>
|
||||||
</mat-option>
|
@for (talkgroup of optionsTalkgroup; track talkgroup; let i = $index) {
|
||||||
</mat-select>
|
<mat-option [value]="i">
|
||||||
</mat-form-field>
|
{{ talkgroup }}
|
||||||
<mat-form-field>
|
</mat-option>
|
||||||
<mat-label>
|
}
|
||||||
Group
|
</mat-select>
|
||||||
</mat-label>
|
</mat-form-field>
|
||||||
<mat-select formControlName="group" (selectionChange)="formChangeHandler()">
|
<mat-form-field>
|
||||||
<mat-option [value]="-1">
|
<mat-label>
|
||||||
All Groups
|
Group
|
||||||
</mat-option>
|
</mat-label>
|
||||||
<mat-option *ngFor="let group of optionsGroup; index as i" [value]="i">
|
<mat-select formControlName="group" (selectionChange)="formChangeHandler()">
|
||||||
{{ group }}
|
<mat-option [value]="-1">
|
||||||
</mat-option>
|
All Groups
|
||||||
</mat-select>
|
</mat-option>
|
||||||
</mat-form-field>
|
@for (group of optionsGroup; track group; let i = $index) {
|
||||||
<mat-form-field>
|
<mat-option [value]="i">
|
||||||
<mat-label>
|
{{ group }}
|
||||||
Tag
|
</mat-option>
|
||||||
</mat-label>
|
}
|
||||||
<mat-select formControlName="tag" (selectionChange)="formChangeHandler()">
|
</mat-select>
|
||||||
<mat-option [value]="-1">
|
</mat-form-field>
|
||||||
All Tags
|
<mat-form-field>
|
||||||
</mat-option>
|
<mat-label>
|
||||||
<mat-option *ngFor="let tag of optionsTag; index as i" [value]="i">
|
Tag
|
||||||
{{ tag }}
|
</mat-label>
|
||||||
</mat-option>
|
<mat-select formControlName="tag" (selectionChange)="formChangeHandler()">
|
||||||
</mat-select>
|
<mat-option [value]="-1">
|
||||||
</mat-form-field>
|
All Tags
|
||||||
<mat-form-field>
|
</mat-option>
|
||||||
<mat-label>
|
@for (tag of optionsTag; track tag; let i = $index) {
|
||||||
Unit
|
<mat-option [value]="i">
|
||||||
</mat-label>
|
{{ tag }}
|
||||||
<input type="number" matInput>
|
</mat-option>
|
||||||
<mat-icon matSuffix>search</mat-icon>
|
}
|
||||||
</mat-form-field>
|
</mat-select>
|
||||||
<div class="reset">
|
</mat-form-field>
|
||||||
<button mat-raised-button type="button" [disabled]="resultsPending" (click)="resetForm()">
|
<mat-form-field>
|
||||||
Reset
|
<mat-label>
|
||||||
</button>
|
Unit
|
||||||
</div>
|
</mat-label>
|
||||||
</form>
|
<input type="number" matInput>
|
||||||
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
|
</mat-form-field>
|
||||||
|
<div class="reset">
|
||||||
|
<button mat-raised-button type="button" [disabled]="resultsPending" (click)="resetForm()">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|
@ -37,6 +37,7 @@ import { RdioScannerService } from '../rdio-scanner.service';
|
||||||
selector: 'rdio-scanner-search',
|
selector: 'rdio-scanner-search',
|
||||||
styleUrls: ['./search.component.scss'],
|
styleUrls: ['./search.component.scss'],
|
||||||
templateUrl: './search.component.html',
|
templateUrl: './search.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerSearchComponent implements OnDestroy {
|
export class RdioScannerSearchComponent implements OnDestroy {
|
||||||
call: RdioScannerCall | undefined;
|
call: RdioScannerCall | undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,47 @@
|
||||||
<fieldset *ngIf="categories?.length && (categories?.length || 0) >= 2" class="fieldset">
|
@if (categories?.length && (categories?.length || 0) >= 2) {
|
||||||
|
<fieldset class="fieldset">
|
||||||
<div>
|
<div>
|
||||||
<button *ngFor="let category of categories" class="rdio-button" [ngClass]="category.status"
|
@for (category of categories; track category) {
|
||||||
(click)="toggle(category)">
|
<button class="rdio-button" [ngClass]="category.status"
|
||||||
{{ category.label }}
|
(click)="toggle(category)">
|
||||||
</button>
|
{{ category.label }}
|
||||||
<button class="rdio-button-mini all-off" (click)="avoid({ all: false })">
|
|
||||||
OFF
|
|
||||||
</button>
|
|
||||||
<button class="rdio-button-mini all-on" (click)="avoid({ all: true })">
|
|
||||||
ON
|
|
||||||
</button>
|
</button>
|
||||||
|
}
|
||||||
|
<button class="rdio-button-mini all-off" (click)="avoid({ all: false })">
|
||||||
|
OFF
|
||||||
|
</button>
|
||||||
|
<button class="rdio-button-mini all-on" (click)="avoid({ all: true })">
|
||||||
|
ON
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<ng-container *ngFor="let system of systems">
|
}
|
||||||
<fieldset *ngIf="system.talkgroups.length" class="fieldset">
|
@for (system of systems; track system) {
|
||||||
<legend>
|
@if (system.talkgroups.length) {
|
||||||
{{ system.label }}
|
<fieldset class="fieldset">
|
||||||
</legend>
|
<legend>
|
||||||
<div>
|
{{ system.label }}
|
||||||
<button *ngFor="let talkgroup of system.talkgroups" class="rdio-button"
|
</legend>
|
||||||
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]).active, on: map[system.id] && map[system.id][talkgroup.id].active, blink: map[system.id][talkgroup.id].minutes }"
|
<div>
|
||||||
(click)="avoid({ system: system, talkgroup: talkgroup })">
|
@for (talkgroup of system.talkgroups; track talkgroup) {
|
||||||
{{ talkgroup.label }}
|
<button class="rdio-button"
|
||||||
</button>
|
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]).active, on: map[system.id] && map[system.id][talkgroup.id].active, blink: map[system.id][talkgroup.id].minutes }"
|
||||||
<ng-container *ngIf="system.talkgroups.length > 1">
|
(click)="avoid({ system: system, talkgroup: talkgroup })">
|
||||||
<button class="rdio-button-mini all-off" (click)="avoid({ system: system, status: false })">
|
{{ talkgroup.label }}
|
||||||
OFF
|
</button>
|
||||||
</button>
|
}
|
||||||
<button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })">
|
@if (system.talkgroups.length > 1) {
|
||||||
ON
|
<button class="rdio-button-mini all-off" (click)="avoid({ system: system, status: false })">
|
||||||
</button>
|
OFF
|
||||||
</ng-container>
|
</button>
|
||||||
</div>
|
<button class="rdio-button-mini all-on" (click)="avoid({ system: system, status: true })">
|
||||||
|
ON
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</ng-container>
|
}
|
||||||
<ng-container *ngIf="systems?.length === 0">
|
}
|
||||||
No talkgroup configured!
|
@if (systems?.length === 0) {
|
||||||
</ng-container>
|
No talkgroup configured!
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ import { RdioScannerService } from '../rdio-scanner.service';
|
||||||
'./select.component.scss',
|
'./select.component.scss',
|
||||||
],
|
],
|
||||||
templateUrl: './select.component.html',
|
templateUrl: './select.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerSelectComponent implements OnDestroy {
|
export class RdioScannerSelectComponent implements OnDestroy {
|
||||||
categories: RdioScannerCategory[] | undefined;
|
categories: RdioScannerCategory[] | undefined;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import packageInfo from '../../../../../package.json';
|
||||||
selector: 'rdio-scanner-admin-page',
|
selector: 'rdio-scanner-admin-page',
|
||||||
styleUrls: ['./admin.component.scss'],
|
styleUrls: ['./admin.component.scss'],
|
||||||
templateUrl: './admin.component.html',
|
templateUrl: './admin.component.html',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerAdminPageComponent {
|
export class RdioScannerAdminPageComponent {
|
||||||
version = packageInfo.version;
|
version = packageInfo.version;
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-main-page',
|
selector: 'rdio-scanner-main-page',
|
||||||
template: '<rdio-scanner></rdio-scanner>',
|
template: '<rdio-scanner></rdio-scanner>',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerMainPageComponent { }
|
export class RdioScannerMainPageComponent { }
|
||||||
|
|
|
||||||
|
|
@ -22,5 +22,6 @@ import { Component } from '@angular/core';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'rdio-scanner-page',
|
selector: 'rdio-scanner-page',
|
||||||
template: '<router-outlet></router-outlet>',
|
template: '<router-outlet></router-outlet>',
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class RdioScannerPageComponent { }
|
export class RdioScannerPageComponent { }
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: './update.component.html',
|
templateUrl: './update.component.html',
|
||||||
styleUrls: ['./update.component.scss'],
|
styleUrls: ['./update.component.scss'],
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class AppUpdateComponent { }
|
export class AppUpdateComponent { }
|
||||||
|
|
@ -17,8 +17,8 @@
|
||||||
* ****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
import { platformBrowser } from '@angular/platform-browser';
|
||||||
|
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
import { environment } from './environments/environment';
|
import { environment } from './environments/environment';
|
||||||
|
|
@ -27,4 +27,4 @@ if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
platformBrowserDynamic().bootstrapModule(AppModule).catch((err) => console.error(err));
|
platformBrowser().bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()], }).catch((err) => console.error(err));
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
@use "@angular/material" as mat;
|
@use "@angular/material" as mat;
|
||||||
|
|
||||||
@import "./styles/material-icons";
|
@use "./styles/material-icons" as *;
|
||||||
@import "./styles/roboto-fonts";
|
@use "./styles/roboto-fonts" as *;
|
||||||
|
|
||||||
@include mat.core();
|
@include mat.elevation-classes();
|
||||||
|
@include mat.app-background();
|
||||||
|
|
||||||
$rdio-scanner-font-family: Roboto, "Helvetica Neue", sans-serif;
|
$rdio-scanner-font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||||
|
|
||||||
$rdio-scanner-primary: mat.define-palette(mat.$grey-palette, 800, 50);
|
$rdio-scanner-primary: mat.m2-define-palette(mat.$m2-grey-palette, 800, 50);
|
||||||
$rdio-scanner-accent: mat.define-palette(mat.$green-palette);
|
$rdio-scanner-accent: mat.m2-define-palette(mat.$m2-green-palette);
|
||||||
$rdio-scanner-warn: mat.define-palette(mat.$red-palette);
|
$rdio-scanner-warn: mat.m2-define-palette(mat.$m2-red-palette);
|
||||||
$rdio-scanner-typography: mat.define-typography-config(
|
$rdio-scanner-typography: mat.m2-define-typography-config(
|
||||||
$font-family: $rdio-scanner-font-family,
|
$font-family: $rdio-scanner-font-family,
|
||||||
);
|
);
|
||||||
|
|
||||||
$rdio-scanner-theme: mat.define-light-theme(
|
$rdio-scanner-theme: mat.m2-define-light-theme(
|
||||||
(
|
(
|
||||||
color: (
|
color: (
|
||||||
primary: $rdio-scanner-primary,
|
primary: $rdio-scanner-primary,
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,19 @@
|
||||||
"outDir": "./dist/out-tsc",
|
"outDir": "./dist/out-tsc",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"declaration": false,
|
"declaration": false,
|
||||||
"downlevelIteration": true,
|
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "bundler",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"target": "es2022",
|
"target": "es2022",
|
||||||
"module": "es2022",
|
"module": "es2022",
|
||||||
"lib": [
|
"resolveJsonModule": true
|
||||||
"es2022",
|
|
||||||
"dom"
|
|
||||||
],
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue