From 086bb90064f4baea8521ba0c9e1e4c490fb390d1 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Mon, 30 Dec 2024 16:58:02 -0500 Subject: [PATCH 01/27] wip --- .../src/app/talkgroups/talkgroups.service.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 76be1c3..0c8d7d2 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -26,7 +26,6 @@ export interface TalkgroupsPaginated { providedIn: 'root', }) export class TalkgroupService { - private readonly _getTalkgroup = new Map>(); private tgs$: Observable; private tags$!: Observable; private fetchAll = new BehaviorSubject<'fetch'>('fetch'); @@ -50,15 +49,9 @@ export class TalkgroupService { } getTalkgroup(sys: number, tg: number): Observable { - const key = this.tgKey(sys, tg); - if (!this._getTalkgroup.get(key)) { - return this.tgs$.pipe( - switchMap((talkg) => - talkg.filter((tgv) => tgv.tgid == tg && tgv.system_id == sys), - ), - ); - } - return this._getTalkgroup.get(key)!; + return this.tgs$.pipe( + switchMap((tgs) => tgs.filter(t => t.system_id === sys && t.tgid === tg)) + ); } putTalkgroup(tu: TalkgroupUpdate): Observable { -- 2.48.1 From b16b3810cce1936357bf11732434d0e2c21342f7 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 31 Dec 2024 00:18:29 -0500 Subject: [PATCH 02/27] Initial incident record --- client/stillbox/src/app/app.routes.ts | 8 +++ .../incident/incident.component.html | 16 ++++- .../incident/incident.component.scss | 32 +++++++++ .../incidents/incident/incident.component.ts | 70 ++++++++++++++++++- .../src/app/incidents/incidents.component.ts | 13 ++-- .../src/app/incidents/incidents.service.ts | 4 ++ .../talkgroup-record.component.ts | 2 +- .../src/app/talkgroups/talkgroups.service.ts | 15 ++-- 8 files changed, 147 insertions(+), 13 deletions(-) diff --git a/client/stillbox/src/app/app.routes.ts b/client/stillbox/src/app/app.routes.ts index d1a67e2..6bd9e8d 100644 --- a/client/stillbox/src/app/app.routes.ts +++ b/client/stillbox/src/app/app.routes.ts @@ -65,6 +65,14 @@ export const routes: Routes = [ ), data: { title: 'Incidents' }, }, + { + path: 'incidents/:id', + loadComponent: () => + import('./incidents/incident/incident.component').then( + (m) => m.IncidentComponent, + ), + data: { title: 'View Incident' }, + }, { path: 'alerts', loadComponent: () => diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index 5afe3dc..78c2992 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -1 +1,15 @@ -

incident works!

+@let inc = inc$ | async; + +

{{ inc?.name }}

+
+
Start
+
+ {{ inc?.startTime | fmtDate }} +
+
End
+
{{ inc?.endTime | fmtDate }}
+
+
+ {{ inc?.description }} +
+
diff --git a/client/stillbox/src/app/incidents/incident/incident.component.scss b/client/stillbox/src/app/incidents/incident/incident.component.scss index e69de29..9c7d43f 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.scss +++ b/client/stillbox/src/app/incidents/incident/incident.component.scss @@ -0,0 +1,32 @@ +.incident { + margin: 50px 50px 50px 50px; + padding: 50px 50px 50px 50px; + display: flex; + flex-flow: column; + width: 50%; + margin-left: auto; + margin-right: auto; +} + +.inc-heading, +.inc-description { + display: flex; + flex-flow: row wrap; + flex: 1 1; +} + +.inc-heading, +.inc-description { + margin-bottom: 30px; +} + +.field { + flex: 1 1; +} + +.field-label { + font-weight: bolder; +} +.field-label::after { + content: ":"; +} diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 27d0508..8893e4c 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,9 +1,73 @@ -import { Component } from '@angular/core'; +import { Component, computed, inject, ViewChild } from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { debounceTime } from 'rxjs/operators'; +import { + Talkgroup, + TalkgroupUpdate, + IconMap, + iconMapping, +} from '../../talkgroup'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { TalkgroupService } from '../../talkgroups/talkgroups.service'; +import { + MatAutocomplete, + MatAutocompleteModule, + MatAutocompleteSelectedEvent, + MatAutocompleteActivatedEvent, +} from '@angular/material/autocomplete'; +import { CommonModule, DatePipe } from '@angular/common'; +import { BehaviorSubject, catchError, of, Subscription } from 'rxjs'; +import { shareReplay } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { + ReactiveFormsModule, + FormGroup, + FormControl, + FormsModule, +} from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { MatInputModule } from '@angular/material/input'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; +import { IncidentsService } from '../incidents.service'; +import { IncidentRecord } from '../../incidents'; +import { MatCardModule } from '@angular/material/card'; +import { FmtDatePipe } from '../incidents.component'; @Component({ selector: 'app-incident', - imports: [], + imports: [ + CommonModule, + ReactiveFormsModule, + FormsModule, + MatInputModule, + MatFormFieldModule, + MatCheckboxModule, + MatIconModule, + MatCardModule, + FmtDatePipe, + ], templateUrl: './incident.component.html', styleUrl: './incident.component.scss', }) -export class IncidentComponent {} +export class IncidentComponent { + inc$!: Observable; + subscriptions: Subscription = new Subscription(); + + constructor( + private route: ActivatedRoute, + private incSvc: IncidentsService, + ) {} + + saveIncName(ev: Event) {} + + ngOnInit() { + const incID = this.route.snapshot.paramMap.get('id')!; + this.inc$ = this.incSvc.getIncident(incID); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } +} diff --git a/client/stillbox/src/app/incidents/incidents.component.ts b/client/stillbox/src/app/incidents/incidents.component.ts index 06624aa..8795eeb 100644 --- a/client/stillbox/src/app/incidents/incidents.component.ts +++ b/client/stillbox/src/app/incidents/incidents.component.ts @@ -34,12 +34,17 @@ import { ToolbarContextService } from '../navigation/toolbar-context.service'; standalone: true, pure: true, }) -export class DatePipe implements PipeTransform { - transform(ts: string, args?: any): string { +export class FmtDatePipe implements PipeTransform { + transform(ts: string | Date | null | undefined, args?: any): string { if (!ts) { return '\u2014'; } - const timestamp = new Date(ts); + let timestamp: Date; + if (ts instanceof Date) { + timestamp = ts; + } else { + timestamp = new Date(ts); + } return ( timestamp.getMonth() + 1 + @@ -61,7 +66,7 @@ const reqPageSize = 200; selector: 'app-incidents', imports: [ MatIconModule, - DatePipe, + FmtDatePipe, MatPaginatorModule, MatTableModule, AsyncPipe, diff --git a/client/stillbox/src/app/incidents/incidents.service.ts b/client/stillbox/src/app/incidents/incidents.service.ts index e432914..6244052 100644 --- a/client/stillbox/src/app/incidents/incidents.service.ts +++ b/client/stillbox/src/app/incidents/incidents.service.ts @@ -47,4 +47,8 @@ export class IncidentsService { updateIncident(id: string, inp: IncidentRecord): Observable { return this.http.patch('/api/incident/' + id, inp); } + + getIncident(id: string): Observable { + return this.http.get('/api/incident/' + id); + } } diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts index 27ea769..3469bd0 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts @@ -17,7 +17,7 @@ import { MatAutocompleteActivatedEvent, } from '@angular/material/autocomplete'; import { CommonModule } from '@angular/common'; -import { BehaviorSubject, catchError, of, Subscription } from 'rxjs'; +import { catchError, of, Subscription } from 'rxjs'; import { shareReplay } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index 0c8d7d2..d50bba4 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -26,6 +26,7 @@ export interface TalkgroupsPaginated { providedIn: 'root', }) export class TalkgroupService { + private readonly _getTalkgroup = new Map>(); private tgs$: Observable; private tags$!: Observable; private fetchAll = new BehaviorSubject<'fetch'>('fetch'); @@ -45,13 +46,19 @@ export class TalkgroupService { } getTalkgroups(): Observable { - return this.http.get('/api/talkgroup/'); + return this.http.get('/api/talkgroup/').pipe(shareReplay()); } getTalkgroup(sys: number, tg: number): Observable { - return this.tgs$.pipe( - switchMap((tgs) => tgs.filter(t => t.system_id === sys && t.tgid === tg)) - ); + const key = this.tgKey(sys, tg); + if (!this._getTalkgroup.get(key)) { + return this.tgs$.pipe( + switchMap((talkg) => + talkg.filter((tgv) => tgv.tgid == tg && tgv.system_id == sys), + ), + ); + } + return this._getTalkgroup.get(key)!; } putTalkgroup(tu: TalkgroupUpdate): Observable { -- 2.48.1 From 9d3c285947187247da9ac7f6519ad44fb7058a6e Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 31 Dec 2024 09:31:42 -0500 Subject: [PATCH 03/27] clean up tgrec --- .../talkgroup-record.component.html | 12 ++++-------- .../talkgroup-record.component.scss | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html index 9676174..ba66d42 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html @@ -28,7 +28,7 @@ /> -
+
Frequency -
-
Weight @@ -95,12 +93,10 @@
-
- Alert - +
+ Alert
-
- Rules: +
diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss index c97ed42..4d511c2 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss @@ -1,9 +1,22 @@ -mat-form-field { +form div { width: 30rem; + flex: 0 0 30rem; + display: flex; + flex-flow: row nowrap; +} + +mat-form-field, .alert { + width: 30rem; + margin-right: 5px; + margin-left: 5px; } .tgRecord { display: flex; + flex-flow: column; + width: 31rem; justify-content: center; margin-top: 20px; + margin-left: auto; + margin-right: auto; } -- 2.48.1 From 4db1cf3ce70525db224bfba909e146ae0e67f63e Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Tue, 31 Dec 2024 12:39:27 -0500 Subject: [PATCH 04/27] Dialog WIP --- client/stillbox/src/app/app.routes.ts | 8 ---- .../talkgroup-record.component.html | 10 ++++- .../talkgroup-record.component.ts | 38 +++++++++++++------ .../talkgroup-table.component.html | 4 +- .../talkgroup-table.component.ts | 28 +++++++++++++- 5 files changed, 62 insertions(+), 26 deletions(-) diff --git a/client/stillbox/src/app/app.routes.ts b/client/stillbox/src/app/app.routes.ts index 6bd9e8d..9898a2e 100644 --- a/client/stillbox/src/app/app.routes.ts +++ b/client/stillbox/src/app/app.routes.ts @@ -43,14 +43,6 @@ export const routes: Routes = [ ), data: { title: 'Export Talkgroups' }, }, - { - path: 'talkgroups/:sys/:tg', - loadComponent: () => - import( - './talkgroups/talkgroup-record/talkgroup-record.component' - ).then((m) => m.TalkgroupRecordComponent), - data: { title: 'Edit Talkgroup' }, - }, { path: 'calls', loadComponent: () => diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html index ba66d42..b919226 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html @@ -1,5 +1,7 @@ +

Edit {{tgid.sys}}:{{tgid.tg}}

+
-
+
Name @@ -99,6 +101,10 @@
-
+ + + + + \ No newline at end of file diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts index 3469bd0..b591f7d 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts @@ -6,6 +6,7 @@ import { TalkgroupUpdate, IconMap, iconMapping, + TGID, } from '../../talkgroup'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { TalkgroupService } from '../talkgroups.service'; @@ -26,12 +27,20 @@ import { FormControl, FormsModule, } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; @Component({ selector: 'talkgroup-record', @@ -46,11 +55,17 @@ import { MatIconModule } from '@angular/material/icon'; MatChipsModule, MatIconModule, MatAutocompleteModule, + MatDialogTitle, + MatDialogContent, + MatDialogActions, + MatDialogClose, ], templateUrl: './talkgroup-record.component.html', styleUrl: './talkgroup-record.component.scss', }) export class TalkgroupRecordComponent { + dialogRef = inject(MatDialogRef); + tgid = inject(MAT_DIALOG_DATA); tg!: Talkgroup; iconMapping: IconMap = iconMapping; tgService: TalkgroupService = inject(TalkgroupService); @@ -83,8 +98,6 @@ export class TalkgroupRecordComponent { subscriptions = new Subscription(); constructor( - private route: ActivatedRoute, - private router: Router, ) { this._allTags = this.tgService.allTags().pipe(shareReplay()); } @@ -138,12 +151,9 @@ export class TalkgroupRecordComponent { } ngOnInit() { - const sysId = this.route.snapshot.paramMap.get('sys'); - const tgId = this.route.snapshot.paramMap.get('tg'); - this.subscriptions.add( this.tgService - .getTalkgroup(Number(sysId), Number(tgId)) + .getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg)) .subscribe((data: Talkgroup) => { this.tg = data; this.form.controls['name'].setValue(this.tg.name); @@ -168,7 +178,7 @@ export class TalkgroupRecordComponent { this.subscriptions.unsubscribe(); } - submit() { + save() { let tgu: TalkgroupUpdate = { system_id: this.tg.system_id, tgid: this.tg.tgid, @@ -208,15 +218,19 @@ export class TalkgroupRecordComponent { }); } } - this.tgService + this.subscriptions.add(this.tgService .putTalkgroup(tgu) .pipe( catchError(() => { return of(null); }), ) - .subscribe((event) => { - this.router.navigate(['/talkgroups/']); - }); + .subscribe((newTG) => { + this.dialogRef.close(newTG); + })); + } + + cancel() { + this.dialogRef.close(); } } diff --git a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html index 505dcf0..780f1b3 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.html @@ -64,8 +64,8 @@ Edit - - + edit diff --git a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts index d95f529..4463081 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts @@ -10,7 +10,7 @@ import { } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { TalkgroupService, TalkgroupsPaginated } from '../talkgroups.service'; -import { Talkgroup, iconMapping } from '../../talkgroup'; +import { TGID, Talkgroup, iconMapping } from '../../talkgroup'; import { ActivatedRoute } from '@angular/router'; import { RouterModule, RouterLink } from '@angular/router'; import { CommonModule } from '@angular/common'; @@ -27,6 +27,16 @@ import { MatChipsModule } from '@angular/material/chips'; import { SelectionModel } from '@angular/cdk/collections'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { Observable, Subscription } from 'rxjs'; +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogActions, + MatDialogClose, + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; +import { TalkgroupRecordComponent } from '../talkgroup-record/talkgroup-record.component'; @Pipe({ standalone: true, @@ -59,7 +69,6 @@ export class SanitizeHtmlPipe implements PipeTransform { selector: 'talkgroup-table', imports: [ RouterModule, - RouterLink, MatIconModule, CommonModule, IconifyPipe, @@ -104,6 +113,7 @@ export class TalkgroupTableComponent { @Input() resetPage!: Observable; @ViewChild('paginator') paginator!: MatPaginator; suppress = false; + dialog = inject(MatDialog); constructor(private route: ActivatedRoute) {} @@ -156,4 +166,18 @@ export class TalkgroupTableComponent { ? this.selection.clear() : this.dataSource.data.forEach((row) => this.selection.select(row)); } + + editTG(idx: number, sys: number, tg: number) { + const tgid = {sys: sys, tg: tg}; + const dialogRef = this.dialog.open(TalkgroupRecordComponent, { + data: tgid, + }); + + dialogRef.afterClosed().subscribe((res) => + { + if (res !== undefined) { + this.dataSource.data[idx] = res; + } + }); + } } -- 2.48.1 From 2b0f6253088b9bd3657469b5927f1d77b86d88c0 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 1 Jan 2025 13:47:36 -0500 Subject: [PATCH 05/27] Talkgroup in dialog --- client/stillbox/src/app/talkgroup.ts | 7 + .../talkgroup-record.component.html | 226 ++++++++++-------- .../talkgroup-record.component.scss | 3 +- .../talkgroup-record.component.ts | 53 ++-- .../talkgroup-table.component.ts | 6 +- .../src/app/talkgroups/talkgroups.service.ts | 4 +- 6 files changed, 166 insertions(+), 133 deletions(-) diff --git a/client/stillbox/src/app/talkgroup.ts b/client/stillbox/src/app/talkgroup.ts index 97469ac..475d810 100644 --- a/client/stillbox/src/app/talkgroup.ts +++ b/client/stillbox/src/app/talkgroup.ts @@ -89,6 +89,13 @@ export class Talkgroup { iconMap(icon: string): string { return iconMapping[icon]!; } + + tgTuple(): TGID { + return { + sys: this.system_id, + tg: this.tgid, + }; + } } export interface TalkgroupUI extends Talkgroup { diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html index b919226..8af8e85 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.html @@ -1,110 +1,124 @@ -

Edit {{tgid.sys}}:{{tgid.tg}}

+

Edit {{ tgid.sys }}:{{ tgid.tg }}

-
-
-
- - Name - - -
-
- - Alpha Tag - -
-
- - Group - -
-
- - Frequency - - - Weight - -
-
- - Icon - + +
+
+ + Alpha Tag + +
+
+ + Group + +
+
+ + Frequency + + + Weight + +
+
+ + Icon + + +
+
+ + Tags + + @for (tag of tg.tags; track tag) { + + {{ tag }} + + + } + + + - {{ opt.key | titlecase }} - - } - - -
-
- - Tags - - @for (tag of tg.tags; track tag) { - - {{ tag }} - - - } - - - - @for (tag of filteredTags(); track tag) { - {{ tag }} - } - - -
-
- Alert -
-
- -
-
-
+ @for (tag of filteredTags(); track tag) { + {{ tag }} + } + + +
+
+ Alert +
+
+ +
+ + } @else { +
+ +
+ } +
- - - \ No newline at end of file + + + diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss index 4d511c2..370c939 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss @@ -5,7 +5,8 @@ form div { flex-flow: row nowrap; } -mat-form-field, .alert { +mat-form-field, +.alert { width: 30rem; margin-right: 5px; margin-left: 5px; diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts index b591f7d..fd3253f 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts @@ -1,6 +1,6 @@ import { Component, computed, inject, ViewChild } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; -import { debounceTime } from 'rxjs/operators'; +import { debounceTime, tap } from 'rxjs/operators'; import { Talkgroup, TalkgroupUpdate, @@ -34,13 +34,14 @@ import { MatChipInputEvent, MatChipsModule } from '@angular/material/chips'; import { MatIconModule } from '@angular/material/icon'; import { MAT_DIALOG_DATA, - MatDialog, MatDialogActions, - MatDialogClose, MatDialogContent, MatDialogRef, MatDialogTitle, } from '@angular/material/dialog'; +import { BrowserModule } from '@angular/platform-browser'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatButtonModule } from '@angular/material/button'; @Component({ selector: 'talkgroup-record', @@ -58,7 +59,8 @@ import { MatDialogTitle, MatDialogContent, MatDialogActions, - MatDialogClose, + MatProgressSpinnerModule, + MatButtonModule, ], templateUrl: './talkgroup-record.component.html', styleUrl: './talkgroup-record.component.scss', @@ -66,6 +68,7 @@ import { export class TalkgroupRecordComponent { dialogRef = inject(MatDialogRef); tgid = inject(MAT_DIALOG_DATA); + tg$!: Observable; tg!: Talkgroup; iconMapping: IconMap = iconMapping; tgService: TalkgroupService = inject(TalkgroupService); @@ -97,8 +100,7 @@ export class TalkgroupRecordComponent { active: string | null = null; subscriptions = new Subscription(); - constructor( - ) { + constructor() { this._allTags = this.tgService.allTags().pipe(shareReplay()); } @@ -151,6 +153,16 @@ export class TalkgroupRecordComponent { } ngOnInit() { + this.tg$ = this.tgService + .getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg)) + .pipe( + tap((tg) => { + this.form.patchValue(tg); + this.form.controls['tagInput'].setValue(''); + this.form.controls['tagsControl'].setValue(this.tg?.tags ?? []); + }), + ); + /* this.subscriptions.add( this.tgService .getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg)) @@ -166,7 +178,7 @@ export class TalkgroupRecordComponent { this.form.controls['tagInput'].setValue(''); this.form.controls['tagsControl'].setValue(this.tg?.tags ?? []); }), - ); + ); */ this.subscriptions.add( this._allTags.subscribe((event) => { this.allTags = event; @@ -180,9 +192,8 @@ export class TalkgroupRecordComponent { save() { let tgu: TalkgroupUpdate = { - system_id: this.tg.system_id, - tgid: this.tg.tgid, - id: this.tg.id, + system_id: this.tgid.sys, + tgid: this.tgid.tg, }; if (this.form.controls['name'].dirty) { tgu.name = this.form.controls['name'].value; @@ -218,19 +229,21 @@ export class TalkgroupRecordComponent { }); } } - this.subscriptions.add(this.tgService - .putTalkgroup(tgu) - .pipe( - catchError(() => { - return of(null); + this.subscriptions.add( + this.tgService + .putTalkgroup(tgu) + .pipe( + catchError(() => { + return of(null); + }), + ) + .subscribe((newTG) => { + this.dialogRef.close(newTG); }), - ) - .subscribe((newTG) => { - this.dialogRef.close(newTG); - })); + ); } cancel() { - this.dialogRef.close(); + this.dialogRef.close(); } } diff --git a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts index 4463081..602dd9f 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-table/talkgroup-table.component.ts @@ -168,15 +168,15 @@ export class TalkgroupTableComponent { } editTG(idx: number, sys: number, tg: number) { - const tgid = {sys: sys, tg: tg}; + const tgid = { sys: sys, tg: tg }; const dialogRef = this.dialog.open(TalkgroupRecordComponent, { data: tgid, }); - dialogRef.afterClosed().subscribe((res) => - { + dialogRef.afterClosed().subscribe((res) => { if (res !== undefined) { this.dataSource.data[idx] = res; + this.dataSource.data = this.dataSource.data; } }); } diff --git a/client/stillbox/src/app/talkgroups/talkgroups.service.ts b/client/stillbox/src/app/talkgroups/talkgroups.service.ts index d50bba4..c309d01 100644 --- a/client/stillbox/src/app/talkgroups/talkgroups.service.ts +++ b/client/stillbox/src/app/talkgroups/talkgroups.service.ts @@ -64,7 +64,7 @@ export class TalkgroupService { putTalkgroup(tu: TalkgroupUpdate): Observable { let tgid = this.tgKey(tu.system_id, tu.tgid); - this.http + return this.http .put(`/api/talkgroup/${tu.system_id}/${tu.tgid}`, tu) .pipe( switchMap((tg) => { @@ -78,8 +78,6 @@ export class TalkgroupService { return tObs; }), ); - - return this._getTalkgroup.get(tgid)!; } putTalkgroups( -- 2.48.1 From e4f0dd826f65ccbcbb24f761d43985ef59a843cd Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Wed, 1 Jan 2025 19:17:45 -0500 Subject: [PATCH 06/27] Incident record editor wip --- .../incident-editor-dialog.component.html | 55 +++++++++++ .../incident/incident.component.html | 5 +- .../incident/incident.component.scss | 38 +++++-- .../incidents/incident/incident.component.ts | 99 +++++++++++++++---- client/stillbox/src/styles.scss | 3 +- 5 files changed, 170 insertions(+), 30 deletions(-) create mode 100644 client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html diff --git a/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html b/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html new file mode 100644 index 0000000..5ff7a2f --- /dev/null +++ b/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html @@ -0,0 +1,55 @@ +

Edit Incident

+ +
+ @let inc = inc$ | async; + @if (inc) { +
+
+ + Name + + + + Start + + + + End + + + + Description + + +
+
+ } @else { +
+ +
+ } +
+
+ + + + diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index 78c2992..b7b7d36 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -1,6 +1,9 @@ @let inc = inc$ | async; -

{{ inc?.name }}

+
+

{{ inc?.name }}

+ edit +
Start
diff --git a/client/stillbox/src/app/incidents/incident/incident.component.scss b/client/stillbox/src/app/incidents/incident/incident.component.scss index 9c7d43f..b3e6716 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.scss +++ b/client/stillbox/src/app/incidents/incident/incident.component.scss @@ -8,16 +8,14 @@ margin-right: auto; } -.inc-heading, -.inc-description { +.inc-heading { display: flex; - flex-flow: row wrap; - flex: 1 1; + margin-bottom: 20px; } -.inc-heading, -.inc-description { - margin-bottom: 30px; +.cardHdr { + display: flex; + flex-flow: row wrap; } .field { @@ -30,3 +28,29 @@ .field-label::after { content: ":"; } + +.cardHdr h1 { + flex: 1 1; +} + +.cardHdr a { + flex: 0 0; + justify-content: flex-end; + align-content: center; + cursor: pointer; +} + +form mat-form-field { + width: 60rem; + flex: 0 0; + display: flex; +} + +.incRecord { + display: flex; + flex-flow: column nowrap; + justify-content: center; + margin-top: 20px; + margin-left: auto; + margin-right: auto; +} diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 8893e4c..f97ba64 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,23 +1,7 @@ import { Component, computed, inject, ViewChild } from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { debounceTime } from 'rxjs/operators'; -import { - Talkgroup, - TalkgroupUpdate, - IconMap, - iconMapping, -} from '../../talkgroup'; -import { COMMA, ENTER } from '@angular/cdk/keycodes'; -import { TalkgroupService } from '../../talkgroups/talkgroups.service'; -import { - MatAutocomplete, - MatAutocompleteModule, - MatAutocompleteSelectedEvent, - MatAutocompleteActivatedEvent, -} from '@angular/material/autocomplete'; +import { map, tap } from 'rxjs/operators'; import { CommonModule, DatePipe } from '@angular/common'; -import { BehaviorSubject, catchError, of, Subscription } from 'rxjs'; -import { shareReplay } from 'rxjs/operators'; +import { Subject, Subscription } from 'rxjs'; import { Observable } from 'rxjs'; import { ReactiveFormsModule, @@ -25,7 +9,7 @@ import { FormControl, FormsModule, } from '@angular/forms'; -import { Router, ActivatedRoute } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -34,6 +18,63 @@ import { IncidentsService } from '../incidents.service'; import { IncidentRecord } from '../../incidents'; import { MatCardModule } from '@angular/material/card'; import { FmtDatePipe } from '../incidents.component'; +import { + MAT_DIALOG_DATA, + MatDialog, + MatDialogActions, + MatDialogContent, + MatDialogRef, + MatDialogTitle, +} from '@angular/material/dialog'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatButtonModule } from '@angular/material/button'; + +@Component({ + selector: 'app-incident-editor', + imports: [ + MatIconModule, + MatFormFieldModule, + ReactiveFormsModule, + FormsModule, + MatInputModule, + MatCheckboxModule, + CommonModule, + MatProgressSpinnerModule, + MatDialogTitle, + MatDialogActions, + MatDialogContent, + MatButtonModule, + ], + templateUrl: './incident-editor-dialog.component.html', + styleUrl: './incident.component.scss', +}) +export class IncidentEditDialogComponent { + dialogRef = inject(MatDialogRef); + incID = inject(MAT_DIALOG_DATA); + inc$!: Observable; + form = new FormGroup({ + name: new FormControl(''), + start: new FormControl(), + end: new FormControl(), + description: new FormControl(''), + }); + + constructor(private incSvc: IncidentsService) {} + + ngOnInit() { + this.inc$ = this.incSvc.getIncident(this.incID).pipe( + tap((inc) => { + this.form.patchValue(inc); + }), + ); + } + + save() {} + + cancel() { + this.dialogRef.close(); + } +} @Component({ selector: 'app-incident', @@ -52,8 +93,11 @@ import { FmtDatePipe } from '../incidents.component'; styleUrl: './incident.component.scss', }) export class IncidentComponent { + incPrime = new Subject(); inc$!: Observable; subscriptions: Subscription = new Subscription(); + dialog = inject(MatDialog); + incID!: string; constructor( private route: ActivatedRoute, @@ -63,8 +107,21 @@ export class IncidentComponent { saveIncName(ev: Event) {} ngOnInit() { - const incID = this.route.snapshot.paramMap.get('id')!; - this.inc$ = this.incSvc.getIncident(incID); + this.incID = this.route.snapshot.paramMap.get('id')!; + this.inc$ = this.incPrime.pipe(map((inc) => inc)); + this.incSvc.getIncident(this.incID).subscribe(this.incPrime); + } + + editIncident(incID: string) { + const dialogRef = this.dialog.open(IncidentEditDialogComponent, { + data: incID, + }); + + dialogRef.afterClosed().subscribe((res) => { + if (res !== undefined) { + this.incPrime.next(res); + } + }); } ngOnDestroy() { diff --git a/client/stillbox/src/styles.scss b/client/stillbox/src/styles.scss index bb21ba1..7d6fd0b 100644 --- a/client/stillbox/src/styles.scss +++ b/client/stillbox/src/styles.scss @@ -222,7 +222,8 @@ body { width: 100px; } -input { +input, +textarea { caret-color: var(--color-dark-fg) !important; } -- 2.48.1 From d92d3aa64e5a52766554b9b47ebf2be1dcab1293 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 2 Jan 2025 20:16:47 -0500 Subject: [PATCH 07/27] style --- .../src/app/incidents/incident/incident.component.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.scss b/client/stillbox/src/app/incidents/incident/incident.component.scss index b3e6716..7e8578f 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.scss +++ b/client/stillbox/src/app/incidents/incident/incident.component.scss @@ -3,11 +3,16 @@ padding: 50px 50px 50px 50px; display: flex; flex-flow: column; - width: 50%; margin-left: auto; margin-right: auto; } +@media not screen and (max-width: 768px) { + .incident { + width: 75%; + } +} + .inc-heading { display: flex; margin-bottom: 20px; @@ -20,6 +25,7 @@ .field { flex: 1 1; + width: 5rem; } .field-label { -- 2.48.1 From b6d1cc6d639107e429bc1fba8e49f5549734d000 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 2 Jan 2025 22:18:32 -0500 Subject: [PATCH 08/27] split web build --- Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index a0a54b2..a48dec7 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,13 @@ buildpprof: client/stillbox/dist: cd client/stillbox && npm install && ng build -c production -web: - cd client/stillbox && npm install && ng build -c production +web: web-install web-build + +web-build: + cd client/stillbox && ng build -c production + +web-install: + cd client/stillbox && npm install clean: rm -rf client/calls/ client/stillbox/dist/ client/stillbox/node_modules/ -- 2.48.1 From 1812511de0a1a0f448d6a0a6bd7b6c9674affe27 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 2 Jan 2025 22:35:25 -0500 Subject: [PATCH 09/27] remove dead --- .../talkgroup-record.component.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts index fd3253f..60e0504 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.ts @@ -162,23 +162,7 @@ export class TalkgroupRecordComponent { this.form.controls['tagsControl'].setValue(this.tg?.tags ?? []); }), ); - /* - this.subscriptions.add( - this.tgService - .getTalkgroup(Number(this.tgid.sys), Number(this.tgid.tg)) - .subscribe((data: Talkgroup) => { - this.tg = data; - this.form.controls['name'].setValue(this.tg.name); - this.form.controls['alpha_tag'].setValue(this.tg.alpha_tag); - this.form.controls['tg_group'].setValue(this.tg.tg_group); - this.form.controls['frequency'].setValue(this.tg.frequency); - this.form.controls['alert'].setValue(this.tg.alert); - this.form.controls['weight'].setValue(this.tg.weight); - this.form.controls['icon'].setValue(this.tg?.metadata?.icon ?? ''); - this.form.controls['tagInput'].setValue(''); - this.form.controls['tagsControl'].setValue(this.tg?.tags ?? []); - }), - ); */ + this.subscriptions.add( this._allTags.subscribe((event) => { this.allTags = event; -- 2.48.1 From 3ca54695dc31dc2214d71134d64c6eb0fdd62b5b Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 2 Jan 2025 22:59:29 -0500 Subject: [PATCH 10/27] call player --- .../src/app/calls/calls.component.html | 4 +- .../stillbox/src/app/calls/calls.component.ts | 9 +---- .../call-player/call-player.component.html | 9 +++++ .../call-player/call-player.component.scss | 0 .../call-player/call-player.component.spec.ts | 22 +++++++++++ .../call-player/call-player.component.ts | 37 +++++++++++++++++++ .../talkgroup-record.component.scss | 3 +- 7 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 client/stillbox/src/app/calls/player/call-player/call-player.component.html create mode 100644 client/stillbox/src/app/calls/player/call-player/call-player.component.scss create mode 100644 client/stillbox/src/app/calls/player/call-player/call-player.component.spec.ts create mode 100644 client/stillbox/src/app/calls/player/call-player/call-player.component.ts diff --git a/client/stillbox/src/app/calls/calls.component.html b/client/stillbox/src/app/calls/calls.component.html index 67a38fd..c98b368 100644 --- a/client/stillbox/src/app/calls/calls.component.html +++ b/client/stillbox/src/app/calls/calls.component.html @@ -109,9 +109,7 @@ - play_arrow + diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index 030f217..9113069 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -29,6 +29,7 @@ import { MatInputModule } from '@angular/material/input'; import { debounceTime } from 'rxjs/operators'; import { ToolbarContextService } from '../navigation/toolbar-context.service'; import { MatSelectModule } from '@angular/material/select'; +import { CallPlayerComponent } from './player/call-player/call-player.component'; @Pipe({ name: 'grabDate', @@ -139,6 +140,7 @@ const reqPageSize = 200; CommonModule, MatProgressSpinnerModule, MatSelectModule, + CallPlayerComponent, ], templateUrl: './calls.component.html', styleUrl: './calls.component.scss', @@ -243,13 +245,6 @@ export class CallsComponent { return now.toISOString().slice(0, 16); } - playAudio(ev: Event, call: CallRecord) { - let au = new Audio(); - au.src = this.callsSvc.callAudioURL(call.id); - au.load(); - au.play(); - } - setPage(p: PageEvent, force?: boolean) { this.selection.clear(); this.curPage = p; diff --git a/client/stillbox/src/app/calls/player/call-player/call-player.component.html b/client/stillbox/src/app/calls/player/call-player/call-player.component.html new file mode 100644 index 0000000..1aab7d8 --- /dev/null +++ b/client/stillbox/src/app/calls/player/call-player/call-player.component.html @@ -0,0 +1,9 @@ +@if (playing) { + stop +} @else { + play_arrow +} diff --git a/client/stillbox/src/app/calls/player/call-player/call-player.component.scss b/client/stillbox/src/app/calls/player/call-player/call-player.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/client/stillbox/src/app/calls/player/call-player/call-player.component.spec.ts b/client/stillbox/src/app/calls/player/call-player/call-player.component.spec.ts new file mode 100644 index 0000000..1a06ffb --- /dev/null +++ b/client/stillbox/src/app/calls/player/call-player/call-player.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CallPlayerComponent } from './call-player.component'; + +describe('CallPlayerComponent', () => { + let component: CallPlayerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CallPlayerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CallPlayerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/stillbox/src/app/calls/player/call-player/call-player.component.ts b/client/stillbox/src/app/calls/player/call-player/call-player.component.ts new file mode 100644 index 0000000..4a2eb86 --- /dev/null +++ b/client/stillbox/src/app/calls/player/call-player/call-player.component.ts @@ -0,0 +1,37 @@ +import { Component, Input } from '@angular/core'; +import { CallsService } from '../../calls.service'; +import { CallRecord } from '../../../calls'; +import { MatIconModule } from '@angular/material/icon'; +import { fromEvent, Observable, Subscription } from 'rxjs'; + +@Component({ + selector: 'call-player', + imports: [MatIconModule], + templateUrl: './call-player.component.html', + styleUrl: './call-player.component.scss', +}) +export class CallPlayerComponent { + @Input() call!: CallRecord; + playing = false; + playSub!: Subscription; + au!: HTMLAudioElement; + + constructor(private callsSvc: CallsService) {} + + stopAudio(ev: Event) { + this.au.pause(); + this.playing = false; + } + + playAudio(ev: Event) { + this.au = new Audio(); + this.playSub = fromEvent(this.au, 'ended').subscribe((ev) => { + this.playing = false; + this.playSub.unsubscribe(); + }); + this.playing = true; + this.au.src = this.callsSvc.callAudioURL(this.call.id); + this.au.load(); + this.au.play(); + } +} diff --git a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss index 370c939..5981fc8 100644 --- a/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss +++ b/client/stillbox/src/app/talkgroups/talkgroup-record/talkgroup-record.component.scss @@ -6,7 +6,8 @@ form div { } mat-form-field, -.alert { +.alert, +alert-rule-builder { width: 30rem; margin-right: 5px; margin-left: 5px; -- 2.48.1 From 728815411e3faca1147bb84789f63834400b1d00 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Thu, 2 Jan 2025 23:02:58 -0500 Subject: [PATCH 11/27] set call --- client/stillbox/src/app/calls/calls.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/stillbox/src/app/calls/calls.component.html b/client/stillbox/src/app/calls/calls.component.html index c98b368..a2b5857 100644 --- a/client/stillbox/src/app/calls/calls.component.html +++ b/client/stillbox/src/app/calls/calls.component.html @@ -109,7 +109,7 @@ - + -- 2.48.1 From 16a0606e4c6bc07676895e0d132d671bcc18ea33 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 4 Jan 2025 10:38:52 -0500 Subject: [PATCH 12/27] Cannot tag excluded field --- pkg/calls/call.go | 6 +++--- pkg/database/calls.sql.go | 2 +- pkg/database/querier.go | 2 +- pkg/rest/incidents.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/calls/call.go b/pkg/calls/call.go index 5c784cb..8c1617d 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -42,18 +42,18 @@ type CallAudio struct { } type Call struct { - ID uuid.UUID `json:"-"` + ID uuid.UUID `json:"id"` Audio []byte `json:"audio,omitempty" filenameField:"AudioName"` AudioName string `json:"audioName,omitempty"` AudioType string `json:"audioType,omitempty"` - Duration CallDuration `json:"-"` + Duration CallDuration `json:"duration,omitempty"` DateTime time.Time `json:"dateTime,omitempty"` Frequencies []int `json:"frequencies,omitempty"` Frequency int `json:"frequency,omitempty"` Patches []int `json:"patches,omitempty"` Source int `json:"source,omitempty"` System int `json:"system,omitempty"` - Submitter *auth.UserID `json:"-,omitempty"` + Submitter *auth.UserID `json:"submitter,omitempty"` SystemLabel string `json:"systemLabel,omitempty"` Talkgroup int `json:"talkgroup,omitempty"` TalkgroupGroup *string `json:"talkgroupGroup,omitempty"` diff --git a/pkg/database/calls.sql.go b/pkg/database/calls.sql.go index 1129b5e..a58588f 100644 --- a/pkg/database/calls.sql.go +++ b/pkg/database/calls.sql.go @@ -147,7 +147,6 @@ WITH to_sweep AS ( WHERE call_id IN (SELECT id FROM to_sweep) ` -// This is used to sweep calls that are part of an incident prior to pruning a partition. func (q *Queries) CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) { result, err := q.db.Exec(ctx, cleanupSweptCalls, rangeStart, rangeEnd) if err != nil { @@ -366,6 +365,7 @@ WITH to_sweep AS ( ) INSERT INTO swept_calls SELECT id, submitter, system, talkgroup, call_date, audio_name, audio_blob, duration, audio_type, audio_url, frequency, frequencies, patches, tg_label, tg_alpha_tag, tg_group, source, transcript FROM to_sweep ` +// This is used to sweep calls that are part of an incident prior to pruning a partition. func (q *Queries) SweepCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) { result, err := q.db.Exec(ctx, sweepCalls, rangeStart, rangeEnd) if err != nil { diff --git a/pkg/database/querier.go b/pkg/database/querier.go index f8f15e2..4ef8f5e 100644 --- a/pkg/database/querier.go +++ b/pkg/database/querier.go @@ -16,7 +16,6 @@ type Querier interface { AddCall(ctx context.Context, arg AddCallParams) error AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error) AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error - // This is used to sweep calls that are part of an incident prior to pruning a partition. CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error) CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error) @@ -62,6 +61,7 @@ type Querier interface { SetTalkgroupTags(ctx context.Context, tags []string, systemID int32, tGID int32) error StoreDeletedTGVersion(ctx context.Context, systemID *int32, tGID *int32, submitter *int32) error StoreTGVersion(ctx context.Context, arg []StoreTGVersionParams) *StoreTGVersionBatchResults + // This is used to sweep calls that are part of an incident prior to pruning a partition. SweepCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error) UpdateCallIncidentNotes(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID) error UpdateIncident(ctx context.Context, arg UpdateIncidentParams) (Incident, error) diff --git a/pkg/rest/incidents.go b/pkg/rest/incidents.go index b586345..e69f5f2 100644 --- a/pkg/rest/incidents.go +++ b/pkg/rest/incidents.go @@ -56,7 +56,7 @@ func (ia *incidentsAPI) listIncidents(w http.ResponseWriter, r *http.Request) { res := struct { Incidents []incstore.Incident `json:"incidents"` - Count int `json:"count"` + Count int `json:"count"` }{} res.Incidents, res.Count, err = incs.Incidents(ctx, p) -- 2.48.1 From 59513572ba4336428e377312d46f3388c426beb5 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 4 Jan 2025 11:00:41 -0500 Subject: [PATCH 13/27] Marshal CallDuration as ms --- pkg/calls/call.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/calls/call.go b/pkg/calls/call.go index 8c1617d..f55b30f 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -1,6 +1,7 @@ package calls import ( + "encoding/json" "fmt" "time" @@ -29,6 +30,10 @@ func (d CallDuration) MsInt32Ptr() *int32 { return &i } +func (d CallDuration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Duration().Milliseconds()) +} + func (d CallDuration) Seconds() int32 { return int32(time.Duration(d).Seconds()) } -- 2.48.1 From 955cced4df3f3b121b76e214a1035ee948056f88 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sat, 4 Jan 2025 11:01:38 -0500 Subject: [PATCH 14/27] Incident call list wip --- .../incident/incident.component.html | 70 +++++++++++++++++++ .../incidents/incident/incident.component.ts | 63 +++++++++++++++-- 2 files changed, 128 insertions(+), 5 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index b7b7d36..9c19fe7 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -15,4 +15,74 @@
{{ inc?.description }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + download + Date + {{ call.call_date | grabDate }} + Time + {{ call.call_date | time }} + System + {{ call | talkgroup: "system" | async }} + Group + {{ call | talkgroup: "group" | async }} + Talkgroup + {{ call | talkgroup: "alpha" | async }} + Duration + {{ call.duration | fixedPoint: 1000 : 2 }}s +
diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index f97ba64..a5278cb 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,7 +1,7 @@ import { Component, computed, inject, ViewChild } from '@angular/core'; import { map, tap } from 'rxjs/operators'; -import { CommonModule, DatePipe } from '@angular/common'; -import { Subject, Subscription } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { BehaviorSubject, Subject, Subscription } from 'rxjs'; import { Observable } from 'rxjs'; import { ReactiveFormsModule, @@ -15,9 +15,8 @@ import { MatFormFieldModule } from '@angular/material/form-field'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatIconModule } from '@angular/material/icon'; import { IncidentsService } from '../incidents.service'; -import { IncidentRecord } from '../../incidents'; +import { IncidentCall, IncidentRecord } from '../../incidents'; import { MatCardModule } from '@angular/material/card'; -import { FmtDatePipe } from '../incidents.component'; import { MAT_DIALOG_DATA, MatDialog, @@ -28,6 +27,18 @@ import { } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatButtonModule } from '@angular/material/button'; +import { CallRecord } from '../../calls'; +import { SelectionModel } from '@angular/cdk/collections'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { + FixedPointPipe, + TalkgroupPipe, + TimePipe, + DatePipe, + DownloadURLPipe, +} from '../../calls/calls.component'; +import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component'; +import { FmtDatePipe } from '../incidents.component'; @Component({ selector: 'app-incident-editor', @@ -87,7 +98,14 @@ export class IncidentEditDialogComponent { MatCheckboxModule, MatIconModule, MatCardModule, + FixedPointPipe, + TimePipe, + DatePipe, + TalkgroupPipe, + DownloadURLPipe, + CallPlayerComponent, FmtDatePipe, + MatTableModule, ], templateUrl: './incident.component.html', styleUrl: './incident.component.scss', @@ -98,6 +116,19 @@ export class IncidentComponent { subscriptions: Subscription = new Subscription(); dialog = inject(MatDialog); incID!: string; + columns = [ + 'select', + 'play', + 'download', + 'date', + 'time', + 'system', + 'group', + 'talkgroup', + 'duration', + ]; + callsResult = new MatTableDataSource(); + selection = new SelectionModel(true, []); constructor( private route: ActivatedRoute, @@ -109,7 +140,17 @@ export class IncidentComponent { ngOnInit() { this.incID = this.route.snapshot.paramMap.get('id')!; this.inc$ = this.incPrime.pipe(map((inc) => inc)); - this.incSvc.getIncident(this.incID).subscribe(this.incPrime); + this.incSvc + .getIncident(this.incID) + .pipe( + tap((inc) => { + if (inc.calls) { + console.log(inc.calls); + this.callsResult.data = inc.calls; + } + }), + ) + .subscribe(this.incPrime); } editIncident(incID: string) { @@ -127,4 +168,16 @@ export class IncidentComponent { ngOnDestroy() { this.subscriptions.unsubscribe(); } + + isAllSelected() { + const numSelected = this.selection.selected.length; + const numRows = this.callsResult.data.length; + return numSelected === numRows; + } + + masterToggle() { + this.isAllSelected() + ? this.selection.clear() + : this.callsResult.data.forEach((row) => this.selection.select(row)); + } } -- 2.48.1 From 064e8d8a97f407c748c1924a24ac45d54d5b26e1 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 10:50:19 -0500 Subject: [PATCH 15/27] Unify JSON output of calls endpoints for UI --- internal/forms/marshal_test.go | 2 +- pkg/calls/call.go | 39 +++++++++++++++++------------- pkg/database/incidents.sql.go | 29 ++++++++++++++++------ pkg/incidents/incstore/store.go | 4 +-- pkg/sinks/relay.go | 2 +- sql/postgres/queries/incidents.sql | 17 ++++++++++++- 6 files changed, 64 insertions(+), 29 deletions(-) diff --git a/internal/forms/marshal_test.go b/internal/forms/marshal_test.go index b4aa217..ae624c2 100644 --- a/internal/forms/marshal_test.go +++ b/internal/forms/marshal_test.go @@ -32,7 +32,7 @@ func call(url string, call *calls.Call) error { var buf bytes.Buffer body := multipart.NewWriter(&buf) - err := forms.Marshal(call, body, forms.WithTag("json")) + err := forms.Marshal(call, body, forms.WithTag("relayOut")) if err != nil { return fmt.Errorf("relay form parse: %w", err) } diff --git a/pkg/calls/call.go b/pkg/calls/call.go index f55b30f..effdbce 100644 --- a/pkg/calls/call.go +++ b/pkg/calls/call.go @@ -46,24 +46,29 @@ type CallAudio struct { AudioBlob []byte `json:"audioBlob"` } +// The tags here are snake_case for compatibility with sqlc generated +// struct tags in ListCallsPRow. This allows the heavier-weight calls +// queries/endpoints to render DB output directly to the wire without +// further transformation. relayOut exists for compatibility with http +// source CallUploadRequest as used in the relay sink. type Call struct { - ID uuid.UUID `json:"id"` - Audio []byte `json:"audio,omitempty" filenameField:"AudioName"` - AudioName string `json:"audioName,omitempty"` - AudioType string `json:"audioType,omitempty"` - Duration CallDuration `json:"duration,omitempty"` - DateTime time.Time `json:"dateTime,omitempty"` - Frequencies []int `json:"frequencies,omitempty"` - Frequency int `json:"frequency,omitempty"` - Patches []int `json:"patches,omitempty"` - Source int `json:"source,omitempty"` - System int `json:"system,omitempty"` - Submitter *auth.UserID `json:"submitter,omitempty"` - SystemLabel string `json:"systemLabel,omitempty"` - Talkgroup int `json:"talkgroup,omitempty"` - TalkgroupGroup *string `json:"talkgroupGroup,omitempty"` - TalkgroupLabel *string `json:"talkgroupLabel,omitempty"` - TGAlphaTag *string `json:"talkgroupTag,omitempty"` + ID uuid.UUID `json:"id" relayOut:"id"` + Audio []byte `json:"audio,omitempty" relayOut:"audio,omitempty" filenameField:"AudioName"` + AudioName string `json:"audioName,omitempty" relayOut:"audioName,omitempty"` + AudioType string `json:"audioType,omitempty" relayOut:"audioType,omitempty"` + Duration CallDuration `json:"duration,omitempty" relayOut:"duration,omitempty"` + DateTime time.Time `json:"call_date,omitempty" relayOut:"dateTime,omitempty"` + Frequencies []int `json:"frequencies,omitempty" relayOut:"frequencies,omitempty"` + Frequency int `json:"frequency,omitempty" relayOut:"frequency,omitempty"` + Patches []int `json:"patches,omitempty" relayOut:"patches,omitempty"` + Source int `json:"source,omitempty" relayOut:"source,omitempty"` + System int `json:"system_id,omitempty" relayOut:"system,omitempty"` + Submitter *auth.UserID `json:"submitter,omitempty" relayOut:"submitter,omitempty"` + SystemLabel string `json:"system_name,omitempty" relayOut:"systemLabel,omitempty"` + Talkgroup int `json:"tgid,omitempty" relayOut:"talkgroup,omitempty"` + TalkgroupGroup *string `json:"talkgroupGroup,omitempty" relayOut:"talkgroupGroup,omitempty"` + TalkgroupLabel *string `json:"talkgroupLabel,omitempty" relayOut:"talkgroupLabel,omitempty"` + TGAlphaTag *string `json:"tg_name,omitempty" relayOut:"talkgroupTag,omitempty"` shouldStore bool `json:"-"` } diff --git a/pkg/database/incidents.sql.go b/pkg/database/incidents.sql.go index a49f5aa..28a46a2 100644 --- a/pkg/database/incidents.sql.go +++ b/pkg/database/incidents.sql.go @@ -132,7 +132,22 @@ func (q *Queries) GetIncident(ctx context.Context, id uuid.UUID) (Incident, erro } const getIncidentCalls = `-- name: GetIncidentCalls :many -SELECT ic.call_id, ic.call_date, ic.notes, c.submitter, c.system, c.talkgroup, c.audio_name, c.duration, c.audio_type, c.audio_url, c.frequency, c.frequencies, c.patches, c.source, c.transcript +SELECT + ic.call_id, + ic.call_date, + c.duration, + c.system system_id, + c.talkgroup tgid, + ic.notes, + c.submitter, + c.audio_name, + c.audio_type, + c.audio_url, + c.frequency, + c.frequencies, + c.patches, + c.source, + c.transcript FROM incidents_calls ic, LATERAL ( SELECT ca.submitter, @@ -170,12 +185,12 @@ WHERE ic.incident_id = $1 type GetIncidentCallsRow struct { CallID uuid.UUID `json:"call_id"` CallDate pgtype.Timestamptz `json:"call_date"` + Duration *int32 `json:"duration"` + SystemID int `json:"system_id"` + TGID int `json:"tgid"` Notes []byte `json:"notes"` Submitter *int32 `json:"submitter"` - System int `json:"system"` - Talkgroup int `json:"talkgroup"` AudioName *string `json:"audio_name"` - Duration *int32 `json:"duration"` AudioType *string `json:"audio_type"` AudioUrl *string `json:"audio_url"` Frequency int `json:"frequency"` @@ -197,12 +212,12 @@ func (q *Queries) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetInci if err := rows.Scan( &i.CallID, &i.CallDate, + &i.Duration, + &i.SystemID, + &i.TGID, &i.Notes, &i.Submitter, - &i.System, - &i.Talkgroup, &i.AudioName, - &i.Duration, &i.AudioType, &i.AudioUrl, &i.Frequency, diff --git a/pkg/incidents/incstore/store.go b/pkg/incidents/incstore/store.go index 79785ed..0a4ba6e 100644 --- a/pkg/incidents/incstore/store.go +++ b/pkg/incidents/incstore/store.go @@ -240,9 +240,9 @@ func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall { Frequency: v.Frequency, Patches: v.Patches, Source: v.Source, - System: v.System, + System: v.SystemID, Submitter: sub, - Talkgroup: v.Talkgroup, + Talkgroup: v.TGID, }, Notes: v.Notes, }) diff --git a/pkg/sinks/relay.go b/pkg/sinks/relay.go index 1c75db1..00b05a8 100644 --- a/pkg/sinks/relay.go +++ b/pkg/sinks/relay.go @@ -81,7 +81,7 @@ func (s *Relay) Call(ctx context.Context, call *calls.Call) error { var buf bytes.Buffer body := multipart.NewWriter(&buf) - err := forms.Marshal(call, body, forms.WithTag("json")) + err := forms.Marshal(call, body, forms.WithTag("relayOut")) if err != nil { return fmt.Errorf("relay form parse: %w", err) } diff --git a/sql/postgres/queries/incidents.sql b/sql/postgres/queries/incidents.sql index 16dde5c..f504919 100644 --- a/sql/postgres/queries/incidents.sql +++ b/sql/postgres/queries/incidents.sql @@ -94,7 +94,22 @@ CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN ; -- name: GetIncidentCalls :many -SELECT ic.call_id, ic.call_date, ic.notes, c.* +SELECT + ic.call_id, + ic.call_date, + c.duration, + c.system system_id, + c.talkgroup tgid, + ic.notes, + c.submitter, + c.audio_name, + c.audio_type, + c.audio_url, + c.frequency, + c.frequencies, + c.patches, + c.source, + c.transcript FROM incidents_calls ic, LATERAL ( SELECT ca.submitter, -- 2.48.1 From 8363b269220ae65541ac9b5355c55f99334a793a Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 11:31:08 -0500 Subject: [PATCH 16/27] Add sameSiteNoneForInsecure option --- config.sample.yaml | 3 +++ pkg/auth/jwt.go | 20 ++++++++++++++------ pkg/config/config.go | 5 +++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/config.sample.yaml b/config.sample.yaml index 11e365c..8a4ee2d 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -27,6 +27,9 @@ auth: # this allows the JWT cookie to be served over plain HTTP only for these Host: header values allowInsecureFor: "localhost": true + # this instead changes the meaning of allowInsecureFor to the cookie being marked + # Secure, but SameSite will be set to None + sameSiteNoneForInsecure: false listen: ':3050' public: true log: diff --git a/pkg/auth/jwt.go b/pkg/auth/jwt.go index 6cb64f3..b2eed4c 100644 --- a/pkg/auth/jwt.go +++ b/pkg/auth/jwt.go @@ -145,6 +145,16 @@ func (a *Auth) allowInsecureCookie(r *http.Request) bool { return has && v } +func (a *Auth) setInsecureCookie(cookie *http.Cookie) { + if a.cfg.SameSiteNoneWhenInsecure { + cookie.Secure = true + cookie.SameSite = http.SameSiteNoneMode + } else { + cookie.Secure = false + cookie.SameSite = http.SameSiteLaxMode + } +} + func (a *Auth) routeRefresh(w http.ResponseWriter, r *http.Request) { jwToken, _, err := jwtauth.FromContext(r.Context()) if err != nil { @@ -174,8 +184,7 @@ func (a *Auth) routeRefresh(w http.ResponseWriter, r *http.Request) { } if a.allowInsecureCookie(r) { - cookie.Secure = false - cookie.SameSite = http.SameSiteLaxMode + a.setInsecureCookie(cookie) } if cookie.Secure { @@ -236,8 +245,7 @@ func (a *Auth) routeAuth(w http.ResponseWriter, r *http.Request) { cookie.Domain = r.Host if a.allowInsecureCookie(r) { - cookie.Secure = false - cookie.SameSite = http.SameSiteLaxMode + a.setInsecureCookie(cookie) } http.SetCookie(w, cookie) @@ -263,8 +271,8 @@ func (a *Auth) routeLogout(w http.ResponseWriter, r *http.Request) { cookie.Domain = r.Host if a.allowInsecureCookie(r) { - cookie.Secure = false - cookie.SameSite = http.SameSiteLaxMode + cookie.Secure = true + cookie.SameSite = http.SameSiteNoneMode } http.SetCookie(w, cookie) diff --git a/pkg/config/config.go b/pkg/config/config.go index bbea959..61dc344 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -30,8 +30,9 @@ type Config struct { } type Auth struct { - JWTSecret string `yaml:"jwtsecret"` - AllowInsecure map[string]bool `yaml:"allowInsecureFor"` + JWTSecret string `yaml:"jwtsecret"` + AllowInsecure map[string]bool `yaml:"allowInsecureFor"` + SameSiteNoneWhenInsecure bool `yaml:"sameSiteNoneForInsecure"` } type CORS struct { -- 2.48.1 From 0821fc4422fc113e2e339791d909acb0d033c55f Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 11:42:29 -0500 Subject: [PATCH 17/27] wip --- .../call-player/call-player.component.ts | 6 +++++- client/stillbox/src/index.dev_nocsp.html | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 client/stillbox/src/index.dev_nocsp.html diff --git a/client/stillbox/src/app/calls/player/call-player/call-player.component.ts b/client/stillbox/src/app/calls/player/call-player/call-player.component.ts index 4a2eb86..0e7a974 100644 --- a/client/stillbox/src/app/calls/player/call-player/call-player.component.ts +++ b/client/stillbox/src/app/calls/player/call-player/call-player.component.ts @@ -31,7 +31,11 @@ export class CallPlayerComponent { }); this.playing = true; this.au.src = this.callsSvc.callAudioURL(this.call.id); + console.log(this.au.src); this.au.load(); - this.au.play(); + this.au.play().then(null, (reason) => { + this.playing = false; + alert(reason); + }); } } diff --git a/client/stillbox/src/index.dev_nocsp.html b/client/stillbox/src/index.dev_nocsp.html new file mode 100644 index 0000000..a20dc26 --- /dev/null +++ b/client/stillbox/src/index.dev_nocsp.html @@ -0,0 +1,19 @@ + + + + + Stillbox + + + + + + + + + + + + -- 2.48.1 From c30ea57bb8f25195b717291b405d6cac1591845d Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 11:47:26 -0500 Subject: [PATCH 18/27] Add ng serve proxy so we can play calls in devel --- client/stillbox/angular.json | 7 +++++++ .../stillbox/src/environments/environment.development.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/stillbox/angular.json b/client/stillbox/angular.json index e130a69..7807679 100644 --- a/client/stillbox/angular.json +++ b/client/stillbox/angular.json @@ -61,6 +61,10 @@ "optimization": false, "extractLicenses": false, "sourceMap": true, + "index": { + "input": "src/index.dev_nocsp.html", + "output": "index.html" + }, "fileReplacements": [ { "replace": "src/environments/environment.ts", @@ -73,6 +77,9 @@ }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "proxyConfig": "src/proxy.conf.json" + }, "configurations": { "production": { "buildTarget": "stillbox:build:production" diff --git a/client/stillbox/src/environments/environment.development.ts b/client/stillbox/src/environments/environment.development.ts index 247eb12..e67cb31 100644 --- a/client/stillbox/src/environments/environment.development.ts +++ b/client/stillbox/src/environments/environment.development.ts @@ -1,3 +1,3 @@ export const environment = { - baseUrl: 'http://xenon:3050', + baseUrl: '', }; -- 2.48.1 From 13a36461dd63d0a1efbdefa82115f6978bf3364d Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 11:48:08 -0500 Subject: [PATCH 19/27] remove log --- .../src/app/calls/player/call-player/call-player.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/client/stillbox/src/app/calls/player/call-player/call-player.component.ts b/client/stillbox/src/app/calls/player/call-player/call-player.component.ts index 0e7a974..2b528bc 100644 --- a/client/stillbox/src/app/calls/player/call-player/call-player.component.ts +++ b/client/stillbox/src/app/calls/player/call-player/call-player.component.ts @@ -31,7 +31,6 @@ export class CallPlayerComponent { }); this.playing = true; this.au.src = this.callsSvc.callAudioURL(this.call.id); - console.log(this.au.src); this.au.load(); this.au.play().then(null, (reason) => { this.playing = false; -- 2.48.1 From 2f8df69d1de10ec550ba056f5405d58db5cbe332 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 11:48:29 -0500 Subject: [PATCH 20/27] proxy config --- client/stillbox/src/proxy.conf.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 client/stillbox/src/proxy.conf.json diff --git a/client/stillbox/src/proxy.conf.json b/client/stillbox/src/proxy.conf.json new file mode 100644 index 0000000..96af567 --- /dev/null +++ b/client/stillbox/src/proxy.conf.json @@ -0,0 +1,6 @@ +{ + "/api": { + "target": "http://xenon:3050", + "secure": false + } +} -- 2.48.1 From 56279f45b3d2d31f4bca49749a88054557b0c6e9 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 14:36:37 -0500 Subject: [PATCH 21/27] WIP: incident record description is never being marked as dirty, needs fix --- .../src/app/calls/calls.component.html | 7 +++ .../stillbox/src/app/calls/calls.component.ts | 2 + .../incident-editor-dialog.component.html | 2 +- .../incidents/incident/incident.component.ts | 48 +++++++++++++++---- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/client/stillbox/src/app/calls/calls.component.html b/client/stillbox/src/app/calls/calls.component.html index a2b5857..8870ca3 100644 --- a/client/stillbox/src/app/calls/calls.component.html +++ b/client/stillbox/src/app/calls/calls.component.html @@ -83,6 +83,13 @@ + + + + +
diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index 9113069..0a3c077 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -30,6 +30,7 @@ import { debounceTime } from 'rxjs/operators'; import { ToolbarContextService } from '../navigation/toolbar-context.service'; import { MatSelectModule } from '@angular/material/select'; import { CallPlayerComponent } from './player/call-player/call-player.component'; +import {MatMenuModule} from '@angular/material/menu'; @Pipe({ name: 'grabDate', @@ -141,6 +142,7 @@ const reqPageSize = 200; MatProgressSpinnerModule, MatSelectModule, CallPlayerComponent, + MatMenuModule, ], templateUrl: './calls.component.html', styleUrl: './calls.component.scss', diff --git a/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html b/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html index 5ff7a2f..d939a2d 100644 --- a/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html +++ b/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html @@ -1,4 +1,4 @@ -

Edit Incident

+

{{ title }}

@let inc = inc$ | async; diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index a5278cb..5086abd 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -40,6 +40,11 @@ import { import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component'; import { FmtDatePipe } from '../incidents.component'; +export interface EditDialogData { + incID: string; + new: boolean; +} + @Component({ selector: 'app-incident-editor', imports: [ @@ -61,7 +66,8 @@ import { FmtDatePipe } from '../incidents.component'; }) export class IncidentEditDialogComponent { dialogRef = inject(MatDialogRef); - incID = inject(MAT_DIALOG_DATA); + data = inject(MAT_DIALOG_DATA); + title = this.data.new ? 'New Incident' : 'Edit Incident'; inc$!: Observable; form = new FormGroup({ name: new FormControl(''), @@ -73,14 +79,36 @@ export class IncidentEditDialogComponent { constructor(private incSvc: IncidentsService) {} ngOnInit() { - this.inc$ = this.incSvc.getIncident(this.incID).pipe( - tap((inc) => { - this.form.patchValue(inc); - }), - ); + if (!this.data.new) { + this.inc$ = this.incSvc.getIncident(this.data.incID).pipe( + tap((inc) => { + this.form.patchValue(inc, { + onlySelf: false, + emitEvent: false, + } + ); + this.form.markAsPristine(); + }), + ); + } } - save() {} + save() { + console.log(this.form.value); + this.incSvc.updateIncident(this.data.incID, { + name: this.form.controls['name'].dirty ? this.form.controls['name'].value : null, + startTime: this.form.controls['start'].dirty ? this.form.controls['start'].value : null, + endTime: this.form.controls['end'].dirty ? this.form.controls['end'].value : null, + description: this.form.controls['description'].dirty ? this.form.controls['description'].value : null, + }).subscribe({ + next: (ok) => { + this.dialogRef.close(ok); + }, + error: (er) => { + alert(er); + }, + }); + } cancel() { this.dialogRef.close(); @@ -145,7 +173,6 @@ export class IncidentComponent { .pipe( tap((inc) => { if (inc.calls) { - console.log(inc.calls); this.callsResult.data = inc.calls; } }), @@ -155,7 +182,10 @@ export class IncidentComponent { editIncident(incID: string) { const dialogRef = this.dialog.open(IncidentEditDialogComponent, { - data: incID, + data: { + incID: incID, + new: false, + }, }); dialogRef.afterClosed().subscribe((res) => { -- 2.48.1 From 38f68a39c35cfa2a026c760699c78a57d9d1aca8 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 16:05:10 -0500 Subject: [PATCH 22/27] form is fixed --- .../incident-editor-dialog.component.html | 1 + .../app/incidents/incident/incident.component.ts | 16 +++++----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html b/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html index d939a2d..f1f01d9 100644 --- a/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html +++ b/client/stillbox/src/app/incidents/incident/incident-editor-dialog.component.html @@ -35,6 +35,7 @@ matInput name="description" placeholder="Description" + formControlName="description" rows="8" cols="200" >{{ inc.description }} { - this.form.patchValue(inc, { - onlySelf: false, - emitEvent: false, - } - ); - this.form.markAsPristine(); + this.form.patchValue(inc); }), ); } } save() { - console.log(this.form.value); this.incSvc.updateIncident(this.data.incID, { name: this.form.controls['name'].dirty ? this.form.controls['name'].value : null, startTime: this.form.controls['start'].dirty ? this.form.controls['start'].value : null, @@ -167,7 +161,7 @@ export class IncidentComponent { ngOnInit() { this.incID = this.route.snapshot.paramMap.get('id')!; - this.inc$ = this.incPrime.pipe(map((inc) => inc)); + this.inc$ = this.incPrime.pipe(); this.incSvc .getIncident(this.incID) .pipe( @@ -176,8 +170,7 @@ export class IncidentComponent { this.callsResult.data = inc.calls; } }), - ) - .subscribe(this.incPrime); + ).subscribe(this.incPrime); } editIncident(incID: string) { @@ -190,6 +183,7 @@ export class IncidentComponent { dialogRef.afterClosed().subscribe((res) => { if (res !== undefined) { + console.log(res); this.incPrime.next(res); } }); -- 2.48.1 From 355ecc361e9812cac20422e6869ce4839e884265 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 16:23:30 -0500 Subject: [PATCH 23/27] Fix card update after edit --- .../src/app/calls/calls.component.html | 9 ++- .../stillbox/src/app/calls/calls.component.ts | 2 +- .../incidents/incident/incident.component.ts | 75 +++++++++++-------- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/client/stillbox/src/app/calls/calls.component.html b/client/stillbox/src/app/calls/calls.component.html index 8870ca3..4a599e0 100644 --- a/client/stillbox/src/app/calls/calls.component.html +++ b/client/stillbox/src/app/calls/calls.component.html @@ -83,12 +83,15 @@ - - - + +
diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index 0a3c077..4836f9a 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -30,7 +30,7 @@ import { debounceTime } from 'rxjs/operators'; import { ToolbarContextService } from '../navigation/toolbar-context.service'; import { MatSelectModule } from '@angular/material/select'; import { CallPlayerComponent } from './player/call-player/call-player.component'; -import {MatMenuModule} from '@angular/material/menu'; +import { MatMenuModule } from '@angular/material/menu'; @Pipe({ name: 'grabDate', diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 952efba..211c5ca 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,7 +1,14 @@ import { Component, computed, inject, ViewChild } from '@angular/core'; import { map, take, tap } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; -import { BehaviorSubject, Subject, Subscription } from 'rxjs'; +import { + BehaviorSubject, + forkJoin, + merge, + Subject, + Subscription, + zip, +} from 'rxjs'; import { Observable } from 'rxjs'; import { ReactiveFormsModule, @@ -82,26 +89,36 @@ export class IncidentEditDialogComponent { if (!this.data.new) { this.inc$ = this.incSvc.getIncident(this.data.incID).pipe( tap((inc) => { - this.form.patchValue(inc); + this.form.patchValue(inc); }), ); - } + } } save() { - this.incSvc.updateIncident(this.data.incID, { - name: this.form.controls['name'].dirty ? this.form.controls['name'].value : null, - startTime: this.form.controls['start'].dirty ? this.form.controls['start'].value : null, - endTime: this.form.controls['end'].dirty ? this.form.controls['end'].value : null, - description: this.form.controls['description'].dirty ? this.form.controls['description'].value : null, - }).subscribe({ - next: (ok) => { - this.dialogRef.close(ok); - }, - error: (er) => { - alert(er); - }, - }); + this.incSvc + .updateIncident(this.data.incID, { + name: this.form.controls['name'].dirty + ? this.form.controls['name'].value + : null, + startTime: this.form.controls['start'].dirty + ? this.form.controls['start'].value + : null, + endTime: this.form.controls['end'].dirty + ? this.form.controls['end'].value + : null, + description: this.form.controls['description'].dirty + ? this.form.controls['description'].value + : null, + }) + .subscribe({ + next: (ok) => { + this.dialogRef.close(ok); + }, + error: (er) => { + alert(er); + }, + }); } cancel() { @@ -133,7 +150,7 @@ export class IncidentEditDialogComponent { styleUrl: './incident.component.scss', }) export class IncidentComponent { - incPrime = new Subject(); + incPrime = new BehaviorSubject({}); inc$!: Observable; subscriptions: Subscription = new Subscription(); dialog = inject(MatDialog); @@ -161,16 +178,13 @@ export class IncidentComponent { ngOnInit() { this.incID = this.route.snapshot.paramMap.get('id')!; - this.inc$ = this.incPrime.pipe(); - this.incSvc - .getIncident(this.incID) - .pipe( - tap((inc) => { - if (inc.calls) { - this.callsResult.data = inc.calls; - } - }), - ).subscribe(this.incPrime); + this.inc$ = merge(this.incSvc.getIncident(this.incID), this.incPrime).pipe( + tap((inc) => { + if (inc.calls) { + this.callsResult.data = inc.calls; + } + }), + ); } editIncident(incID: string) { @@ -181,12 +195,7 @@ export class IncidentComponent { }, }); - dialogRef.afterClosed().subscribe((res) => { - if (res !== undefined) { - console.log(res); - this.incPrime.next(res); - } - }); + dialogRef.afterClosed().subscribe(this.incPrime); } ngOnDestroy() { -- 2.48.1 From e907195a0e1a7860979fe28935158efa825bf5c5 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 16:24:08 -0500 Subject: [PATCH 24/27] import clean --- .../src/app/incidents/incident/incident.component.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 211c5ca..3779d44 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,13 +1,10 @@ -import { Component, computed, inject, ViewChild } from '@angular/core'; -import { map, take, tap } from 'rxjs/operators'; +import { Component, inject } from '@angular/core'; +import { tap } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; import { BehaviorSubject, - forkJoin, merge, - Subject, Subscription, - zip, } from 'rxjs'; import { Observable } from 'rxjs'; import { @@ -34,7 +31,6 @@ import { } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatButtonModule } from '@angular/material/button'; -import { CallRecord } from '../../calls'; import { SelectionModel } from '@angular/cdk/collections'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { -- 2.48.1 From 3c133b092248560701b9f7659d4f191b4cbd9380 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 17:22:09 -0500 Subject: [PATCH 25/27] New incident --- .../src/app/calls/calls.component.html | 8 ++- .../stillbox/src/app/calls/calls.component.ts | 45 +++++++++++++- .../incidents/incident/incident.component.ts | 59 ++++++++++--------- 3 files changed, 81 insertions(+), 31 deletions(-) diff --git a/client/stillbox/src/app/calls/calls.component.html b/client/stillbox/src/app/calls/calls.component.html index 4a599e0..100cc73 100644 --- a/client/stillbox/src/app/calls/calls.component.html +++ b/client/stillbox/src/app/calls/calls.component.html @@ -90,8 +90,12 @@ playlist_add - - + +
diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index 4836f9a..ab7ac9b 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -1,4 +1,10 @@ -import { Component, Pipe, PipeTransform, ViewChild } from '@angular/core'; +import { + Component, + inject, + Pipe, + PipeTransform, + ViewChild, +} from '@angular/core'; import { CommonModule, AsyncPipe } from '@angular/common'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatTableModule } from '@angular/material/table'; @@ -31,6 +37,16 @@ import { ToolbarContextService } from '../navigation/toolbar-context.service'; import { MatSelectModule } from '@angular/material/select'; import { CallPlayerComponent } from './player/call-player/call-player.component'; import { MatMenuModule } from '@angular/material/menu'; +import { MatDialog } from '@angular/material/dialog'; +import { + EditDialogData, + IncidentEditDialogComponent, +} from '../incidents/incident/incident.component'; +import { + CallIncidentParams, + IncidentsService, +} from '../incidents/incidents.service'; +import { IncidentRecord } from '../incidents'; @Pipe({ name: 'grabDate', @@ -151,6 +167,7 @@ export class CallsComponent { callsResult = new BehaviorSubject(new Array(0)); @ViewChild('paginator') paginator!: MatPaginator; count = 0; + dialog = inject(MatDialog); page = 0; perPage = 25; pageSizeOptions = [25, 50, 75, 100, 200]; @@ -193,6 +210,7 @@ export class CallsComponent { private prefsSvc: PrefsService, public tcSvc: ToolbarContextService, public tgSvc: TalkgroupService, + public incSvc: IncidentsService, ) { this.tcSvc.showFilterButton(); } @@ -340,4 +358,29 @@ export class CallsComponent { this.form.controls['start'].setValue(this.lTime(new Date())); this.form.controls['duration'].setValue(0); } + + addToNewInc(ev: Event) { + const dialogRef = this.dialog.open(IncidentEditDialogComponent, { + data: { + incID: '', + new: true, + }, + }); + dialogRef.afterClosed().subscribe((res: IncidentRecord) => { + this.incSvc + .addRemoveCalls(res.id, { + add: this.selection.selected.map((s) => s.id), + }) + .subscribe({ + next: () => { + this.selection.clear(); + }, + error: (err) => { + alert(err); + }, + }); + }); + } + + addToExistingInc(ev: Event) {} } diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 3779d44..823fee8 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,11 +1,7 @@ import { Component, inject } from '@angular/core'; import { tap } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; -import { - BehaviorSubject, - merge, - Subscription, -} from 'rxjs'; +import { BehaviorSubject, merge, Subscription } from 'rxjs'; import { Observable } from 'rxjs'; import { ReactiveFormsModule, @@ -88,33 +84,40 @@ export class IncidentEditDialogComponent { this.form.patchValue(inc); }), ); + } else { + this.inc$ = new BehaviorSubject({}); } } save() { - this.incSvc - .updateIncident(this.data.incID, { - name: this.form.controls['name'].dirty - ? this.form.controls['name'].value - : null, - startTime: this.form.controls['start'].dirty - ? this.form.controls['start'].value - : null, - endTime: this.form.controls['end'].dirty - ? this.form.controls['end'].value - : null, - description: this.form.controls['description'].dirty - ? this.form.controls['description'].value - : null, - }) - .subscribe({ - next: (ok) => { - this.dialogRef.close(ok); - }, - error: (er) => { - alert(er); - }, - }); + let resObs: Observable; + let ir: IncidentRecord = { + name: this.form.controls['name'].dirty + ? this.form.controls['name'].value + : null, + startTime: this.form.controls['start'].dirty + ? this.form.controls['start'].value + : null, + endTime: this.form.controls['end'].dirty + ? this.form.controls['end'].value + : null, + description: this.form.controls['description'].dirty + ? this.form.controls['description'].value + : null, + }; + if (this.data.new) { + resObs = this.incSvc.createIncident(ir); + } else { + resObs = this.incSvc.updateIncident(this.data.incID, ir); + } + resObs.subscribe({ + next: (ok) => { + this.dialogRef.close(ok); + }, + error: (er) => { + alert(er); + }, + }); } cancel() { -- 2.48.1 From e72332904cbe43011d19dd34c882560675a95117 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 17:46:03 -0500 Subject: [PATCH 26/27] Delete incident --- .../incidents/incident/incident.component.html | 12 +++++++++++- .../incidents/incident/incident.component.scss | 2 ++ .../incidents/incident/incident.component.ts | 18 +++++++++++++++++- client/stillbox/src/styles.scss | 6 ++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/client/stillbox/src/app/incidents/incident/incident.component.html b/client/stillbox/src/app/incidents/incident/incident.component.html index 9c19fe7..b60261d 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.html +++ b/client/stillbox/src/app/incidents/incident/incident.component.html @@ -2,7 +2,17 @@

{{ inc?.name }}

- edit + + + + +
Start
diff --git a/client/stillbox/src/app/incidents/incident/incident.component.scss b/client/stillbox/src/app/incidents/incident/incident.component.scss index 7e8578f..08e0820 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.scss +++ b/client/stillbox/src/app/incidents/incident/incident.component.scss @@ -21,6 +21,7 @@ .cardHdr { display: flex; flex-flow: row wrap; + margin-bottom: 24px; } .field { @@ -37,6 +38,7 @@ .cardHdr h1 { flex: 1 1; + margin: 0; } .cardHdr a { diff --git a/client/stillbox/src/app/incidents/incident/incident.component.ts b/client/stillbox/src/app/incidents/incident/incident.component.ts index 823fee8..944fe04 100644 --- a/client/stillbox/src/app/incidents/incident/incident.component.ts +++ b/client/stillbox/src/app/incidents/incident/incident.component.ts @@ -1,6 +1,6 @@ import { Component, inject } from '@angular/core'; import { tap } from 'rxjs/operators'; -import { CommonModule } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; import { BehaviorSubject, merge, Subscription } from 'rxjs'; import { Observable } from 'rxjs'; import { @@ -38,6 +38,7 @@ import { } from '../../calls/calls.component'; import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component'; import { FmtDatePipe } from '../incidents.component'; +import { MatMenuModule } from '@angular/material/menu'; export interface EditDialogData { incID: string; @@ -144,6 +145,7 @@ export class IncidentEditDialogComponent { CallPlayerComponent, FmtDatePipe, MatTableModule, + MatMenuModule, ], templateUrl: './incident.component.html', styleUrl: './incident.component.scss', @@ -171,6 +173,7 @@ export class IncidentComponent { constructor( private route: ActivatedRoute, private incSvc: IncidentsService, + private location: Location, ) {} saveIncName(ev: Event) {} @@ -197,6 +200,19 @@ export class IncidentComponent { dialogRef.afterClosed().subscribe(this.incPrime); } + deleteIncident(incID: string) { + if (confirm('Are you sure you want to delete this incident?')) { + this.incSvc.deleteIncident(incID).subscribe({ + next: () => { + this.location.back(); + }, + error: (err) => { + alert(err); + }, + }); + } + } + ngOnDestroy() { this.subscriptions.unsubscribe(); } diff --git a/client/stillbox/src/styles.scss b/client/stillbox/src/styles.scss index 7d6fd0b..c0d1767 100644 --- a/client/stillbox/src/styles.scss +++ b/client/stillbox/src/styles.scss @@ -6,6 +6,7 @@ // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. // Be sure that you only ever include this mixin once! +@include mat.core(); @include mat.elevation-classes(); @include mat.app-background(); @@ -237,3 +238,8 @@ textarea { margin-bottom: 40px; justify-content: center; } + +.mat-mdc-menu-item.deleteItem { + color: white; + background-color: red; +} -- 2.48.1 From 8e4002d79a10ccca0e5a08d17c1f54eb38836dd9 Mon Sep 17 00:00:00 2001 From: Daniel Ponte Date: Sun, 5 Jan 2025 21:51:25 -0500 Subject: [PATCH 27/27] Add to existing incident --- .../stillbox/src/app/calls/calls.component.ts | 22 +++++- .../select-incident-dialog.component.html | 23 +++++++ .../select-incident-dialog.component.scss | 5 ++ .../select-incident-dialog.component.spec.ts | 22 ++++++ .../select-incident-dialog.component.ts | 68 +++++++++++++++++++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.html create mode 100644 client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.scss create mode 100644 client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.spec.ts create mode 100644 client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.ts diff --git a/client/stillbox/src/app/calls/calls.component.ts b/client/stillbox/src/app/calls/calls.component.ts index ab7ac9b..f6432c1 100644 --- a/client/stillbox/src/app/calls/calls.component.ts +++ b/client/stillbox/src/app/calls/calls.component.ts @@ -47,6 +47,7 @@ import { IncidentsService, } from '../incidents/incidents.service'; import { IncidentRecord } from '../incidents'; +import { SelectIncidentDialogComponent } from '../incidents/select-incident-dialog/select-incident-dialog.component'; @Pipe({ name: 'grabDate', @@ -382,5 +383,24 @@ export class CallsComponent { }); } - addToExistingInc(ev: Event) {} + addToExistingInc(ev: Event) { + const dialogRef = this.dialog.open(SelectIncidentDialogComponent); + dialogRef.afterClosed().subscribe((res: string) => { + if (!res) { + return; + } + this.incSvc + .addRemoveCalls(res, { + add: this.selection.selected.map((s) => s.id), + }) + .subscribe({ + next: () => { + this.selection.clear(); + }, + error: (err) => { + alert(err); + }, + }); + }); + } } diff --git a/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.html b/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.html new file mode 100644 index 0000000..a5c2b54 --- /dev/null +++ b/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.html @@ -0,0 +1,23 @@ +

Select Incident

+ + + + +
+ + @let incidents = incs$ | async; + @for (inc of incidents; track inc) { + {{ inc.name }} + } + +
+
+ + + + diff --git a/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.scss b/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.scss new file mode 100644 index 0000000..9174840 --- /dev/null +++ b/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.scss @@ -0,0 +1,5 @@ +.selList { + overflow: scroll; + width: 480px; + min-height: 200px; +} diff --git a/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.spec.ts b/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.spec.ts new file mode 100644 index 0000000..f11865b --- /dev/null +++ b/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectIncidentDialogComponent } from './select-incident-dialog.component'; + +describe('SelectIncidentDialogComponent', () => { + let component: SelectIncidentDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectIncidentDialogComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SelectIncidentDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.ts b/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.ts new file mode 100644 index 0000000..ecab136 --- /dev/null +++ b/client/stillbox/src/app/incidents/select-incident-dialog/select-incident-dialog.component.ts @@ -0,0 +1,68 @@ +import { AsyncPipe } from '@angular/common'; +import { Component, inject, ViewChild } from '@angular/core'; +import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule, MatSelectionList } from '@angular/material/list'; +import { + IncidentsListParams, + IncidentsPaginated, + IncidentsService, +} from '../incidents.service'; +import { + BehaviorSubject, + combineLatest, + debounceTime, + distinctUntilChanged, + map, + merge, + Observable, + switchMap, +} from 'rxjs'; +import { IncidentRecord } from '../../incidents'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; + +@Component({ + selector: 'app-select-incident-dialog', + imports: [ + MatListModule, + MatInputModule, + AsyncPipe, + MatDialogModule, + MatButtonModule, + FormsModule, + ], + templateUrl: './select-incident-dialog.component.html', + styleUrl: './select-incident-dialog.component.scss', +}) +export class SelectIncidentDialogComponent { + dialogRef = inject(MatDialogRef); + allIncs$!: Observable; + incs$!: Observable; + findStr = new BehaviorSubject(''); + sel!: string; + constructor(private incSvc: IncidentsService) {} + + ngOnInit() { + this.incs$ = this.findStr.pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap((sub) => + this.incSvc.getIncidents({ filter: sub }), + ), + map((incidents) => incidents.incidents), + ); + } + + search(term: string) { + this.findStr.next(term); + } + + cancel() { + this.dialogRef.close(null); + } + + save() { + this.dialogRef.close(this.sel); + } +} -- 2.48.1