Merge pull request 'Filter call source' (#122) from filterSource into trunk

Reviewed-on: #122
This commit is contained in:
Daniel Ponte 2025-02-22 19:28:31 -05:00
commit 2cf124cfc3
9 changed files with 129 additions and 51 deletions

View file

@ -49,6 +49,25 @@
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<mat-form-field class="filterBox" subscriptSizing="dynamic">
<mat-label>Source Filter</mat-label>
<input
matInput
name="sourceFilter"
type="text"
autocomplete="off"
formControlName="sourceFilter"
/>
<button
class="clearBtn"
*ngIf="form.controls['sourceFilter'].value"
matSuffix
mat-icon-button
(click)="form.controls['sourceFilter'].setValue('')"
>
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<mat-form-field class="tagSelect" subscriptSizing="dynamic">
<mat-label>Any Tags</mat-label>
<mat-select
@ -163,14 +182,26 @@
<a
href="javascript:void(0)"
class="tgFilter"
(click)="searchFilter(tgAlpha)"
(click)="searchTGFilter(tgAlpha)"
>{{ tgAlpha }}</a
>
</td>
</ng-container>
<ng-container matColumnDef="talker">
<th mat-header-cell *matHeaderCellDef>Source</th>
<td mat-cell *matCellDef="let call" [innerHTML]="call | talker"></td>
<td mat-cell *matCellDef="let call">
@let tlkAlias = call | talker;
@if (tlkAlias) {
<a
href="javascript:void(0)"
class="srcFilter"
(click)="searchSrcFilter(tlkAlias)"
>{{ tlkAlias }}</a
>
} @else {
&mdash;
}
</td>
</ng-container>
<ng-container matColumnDef="duration">
<th mat-header-cell *matHeaderCellDef class="durationHdr">Duration</th>

View file

@ -86,7 +86,7 @@ form {
}
.filterBox {
flex: 1 1 300px;
flex: 1 2 150px;
}
.durationFilter {
@ -94,13 +94,14 @@ form {
}
.tagSelect {
flex: 1 1 250px;
flex: 1 1 220px;
}
.in-incident {
background-color: rgb(59, 0, 59);
}
a.tgFilter:hover {
a.tgFilter:hover,
a.srcFilter:hover {
text-decoration: underline;
}

View file

@ -112,6 +112,7 @@ export class CallsComponent {
start: new FormControl(this.lTime(new Date())),
end: new FormControl(null),
filter: new FormControl(''),
sourceFilter: new FormControl(''),
duration: new FormControl(0),
tagsAny: new FormControl<string[]>([]),
tagsNot: new FormControl<string[]>([]),
@ -139,12 +140,18 @@ export class CallsComponent {
return numSelected === numRows;
}
searchFilter(filt: string | null) {
searchTGFilter(filt: string | null) {
if (filt) {
this.form.controls['filter'].setValue(filt);
}
}
searchSrcFilter(filt: string | null) {
if (filt) {
this.form.controls['sourceFilter'].setValue(filt);
}
}
buildParams(p: PageEvent, serverPage: number): CallsListParams {
const par: CallsListParams = {
start: new Date(this.form.controls['start'].value!),
@ -167,6 +174,10 @@ export class CallsComponent {
this.form.controls['filter'].value != ''
? this.form.controls['filter'].value
: null,
sourceFilter:
this.form.controls['sourceFilter'].value != ''
? this.form.controls['sourceFilter'].value
: null,
atLeastSeconds:
this.form.controls['duration'].value != null &&
this.form.controls['duration'].value > 0

View file

@ -33,12 +33,12 @@ export class DatePipe implements PipeTransform {
pure: true,
})
export class TalkerPipe implements PipeTransform {
transform(call: CallRecord, args?: any): string {
transform(call: CallRecord, args?: any): string | null {
if (call.talkerAlias != null) {
return call.talkerAlias;
}
return '&mdash;';
return null;
}
}
@ -175,6 +175,7 @@ export interface CallsListParams {
page: number;
perPage: number;
tgFilter: string | null;
sourceFilter: string | null;
atLeastSeconds: number | null;
}

View file

@ -102,6 +102,18 @@
{{ call | talkgroup: "alpha" | async }}
</td>
</ng-container>
<ng-container matColumnDef="talker">
<th mat-header-cell *matHeaderCellDef>Source</th>
<td mat-cell *matCellDef="let call">
@let tlkAlias = call | talker;
@if (tlkAlias) {
{{ tlkAlias }}
} @else {
&mdash;
}
</td>
</ng-container>
<ng-container matColumnDef="duration">
<th mat-header-cell *matHeaderCellDef class="durationHdr">Duration</th>
<td mat-cell *matCellDef="let call" class="duration">

View file

@ -35,6 +35,7 @@ import {
TimePipe,
DatePipe,
DownloadURLPipe,
TalkerPipe,
} from '../../calls/calls.service';
import { CallPlayerComponent } from '../../calls/player/call-player/call-player.component';
import { FmtDatePipe } from '../incidents.component';
@ -141,6 +142,7 @@ export class IncidentEditDialogComponent {
MatIconModule,
MatCardModule,
FixedPointPipe,
TalkerPipe,
TimePipe,
DatePipe,
TalkgroupPipe,
@ -169,6 +171,7 @@ export class IncidentComponent {
'system',
'group',
'talkgroup',
'talker',
'duration',
];
callsResult = new MatTableDataSource<IncidentCall>();

View file

@ -203,6 +203,7 @@ type CallsParams struct {
TagsAny []string `json:"tagsAny"`
TagsNot []string `json:"tagsNot"`
TGFilter *string `json:"tgFilter"`
SourceFilter *string `json:"sourceFilter"`
AtLeastSeconds *float32 `json:"atLeastSeconds"`
UnknownTG bool `json:"unknownTG"`
}
@ -217,15 +218,16 @@ func (s *postgresStore) Calls(ctx context.Context, p CallsParams) (rows []databa
offset, perPage := p.Pagination.OffsetPerPage(100)
par := database.ListCallsPParams{
Start: p.Start.PGTypeTSTZ(),
End: p.End.PGTypeTSTZ(),
TagsAny: p.TagsAny,
TagsNot: p.TagsNot,
Offset: offset,
PerPage: perPage,
Direction: p.Direction.DirString(common.DirAsc),
TGFilter: p.TGFilter,
UnknownTG: p.UnknownTG,
Start: p.Start.PGTypeTSTZ(),
End: p.End.PGTypeTSTZ(),
TagsAny: p.TagsAny,
TagsNot: p.TagsNot,
Offset: offset,
PerPage: perPage,
Direction: p.Direction.DirString(common.DirAsc),
TGFilter: p.TGFilter,
SourceFilter: p.SourceFilter,
UnknownTG: p.UnknownTG,
}
if p.AtLeastSeconds != nil {
@ -241,13 +243,14 @@ func (s *postgresStore) Calls(ctx context.Context, p CallsParams) (rows []databa
txErr := db.InTx(ctx, func(db database.Store) error {
var err error
count, err = db.ListCallsCount(ctx, database.ListCallsCountParams{
Start: par.Start,
End: par.End,
TagsAny: par.TagsAny,
TagsNot: par.TagsNot,
TGFilter: par.TGFilter,
LongerThan: par.LongerThan,
UnknownTG: par.UnknownTG,
Start: par.Start,
End: par.End,
TagsAny: par.TagsAny,
TagsNot: par.TagsNot,
TGFilter: par.TGFilter,
SourceFilter: p.SourceFilter,
LongerThan: par.LongerThan,
UnknownTG: par.UnknownTG,
})
if err != nil {
return err

View file

@ -317,22 +317,26 @@ CASE WHEN $4::TEXT[] IS NOT NULL THEN
tgs.name ILIKE '%' || $5 || '%' OR
tgs.alpha_tag ILIKE '%' || $5 || '%'
) ELSE TRUE END) AND
(CASE WHEN $6::NUMERIC IS NOT NULL THEN (
c.duration > $6
(CASE WHEN $6::TEXT IS NOT NULL THEN (
c.talker_alias ILIKE '%' || $6 || '%'
) ELSE TRUE END) AND
(CASE WHEN $7::BOOLEAN = TRUE THEN (
(CASE WHEN $7::NUMERIC IS NOT NULL THEN (
c.duration > $7
) ELSE TRUE END) AND
(CASE WHEN $8::BOOLEAN = TRUE THEN (
tgs.tgid IS NULL
) ELSE TRUE END)
`
type ListCallsCountParams struct {
Start pgtype.Timestamptz `json:"start"`
End pgtype.Timestamptz `json:"end"`
TagsAny []string `json:"tagsAny"`
TagsNot []string `json:"tagsNot"`
TGFilter *string `json:"tgFilter"`
LongerThan pgtype.Numeric `json:"longerThan"`
UnknownTG bool `json:"unknownTg"`
Start pgtype.Timestamptz `json:"start"`
End pgtype.Timestamptz `json:"end"`
TagsAny []string `json:"tagsAny"`
TagsNot []string `json:"tagsNot"`
TGFilter *string `json:"tgFilter"`
SourceFilter *string `json:"sourceFilter"`
LongerThan pgtype.Numeric `json:"longerThan"`
UnknownTG bool `json:"unknownTg"`
}
func (q *Queries) ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error) {
@ -342,6 +346,7 @@ func (q *Queries) ListCallsCount(ctx context.Context, arg ListCallsCountParams)
arg.TagsAny,
arg.TagsNot,
arg.TGFilter,
arg.SourceFilter,
arg.LongerThan,
arg.UnknownTG,
)
@ -377,31 +382,35 @@ CASE WHEN $4::TEXT[] IS NOT NULL THEN
tgs.name ILIKE '%' || $5 || '%' OR
tgs.alpha_tag ILIKE '%' || $5 || '%'
) ELSE TRUE END) AND
(CASE WHEN $6::NUMERIC IS NOT NULL THEN (
c.duration > $6
(CASE WHEN $6::TEXT IS NOT NULL THEN (
c.talker_alias ILIKE '%' || $6 || '%'
) ELSE TRUE END) AND
(CASE WHEN $7::BOOLEAN = TRUE THEN (
(CASE WHEN $7::NUMERIC IS NOT NULL THEN (
c.duration > $7
) ELSE TRUE END) AND
(CASE WHEN $8::BOOLEAN = TRUE THEN (
tgs.tgid IS NULL
) ELSE TRUE END)
GROUP BY c.id, c.call_date
ORDER BY
CASE WHEN $8::TEXT = 'asc' THEN c.call_date END ASC,
CASE WHEN $8 = 'desc' THEN c.call_date END DESC
OFFSET $9 ROWS
FETCH NEXT $10 ROWS ONLY
CASE WHEN $9::TEXT = 'asc' THEN c.call_date END ASC,
CASE WHEN $9 = 'desc' THEN c.call_date END DESC
OFFSET $10 ROWS
FETCH NEXT $11 ROWS ONLY
`
type ListCallsPParams struct {
Start pgtype.Timestamptz `json:"start"`
End pgtype.Timestamptz `json:"end"`
TagsAny []string `json:"tagsAny"`
TagsNot []string `json:"tagsNot"`
TGFilter *string `json:"tgFilter"`
LongerThan pgtype.Numeric `json:"longerThan"`
UnknownTG bool `json:"unknownTg"`
Direction string `json:"direction"`
Offset int32 `json:"offset"`
PerPage int32 `json:"perPage"`
Start pgtype.Timestamptz `json:"start"`
End pgtype.Timestamptz `json:"end"`
TagsAny []string `json:"tagsAny"`
TagsNot []string `json:"tagsNot"`
TGFilter *string `json:"tgFilter"`
SourceFilter *string `json:"sourceFilter"`
LongerThan pgtype.Numeric `json:"longerThan"`
UnknownTG bool `json:"unknownTg"`
Direction string `json:"direction"`
Offset int32 `json:"offset"`
PerPage int32 `json:"perPage"`
}
type ListCallsPRow struct {
@ -421,6 +430,7 @@ func (q *Queries) ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListC
arg.TagsAny,
arg.TagsNot,
arg.TGFilter,
arg.SourceFilter,
arg.LongerThan,
arg.UnknownTG,
arg.Direction,

View file

@ -125,6 +125,9 @@ CASE WHEN sqlc.narg('tags_not')::TEXT[] IS NOT NULL THEN
tgs.name ILIKE '%' || @tg_filter || '%' OR
tgs.alpha_tag ILIKE '%' || @tg_filter || '%'
) ELSE TRUE END) AND
(CASE WHEN sqlc.narg('source_filter')::TEXT IS NOT NULL THEN (
c.talker_alias ILIKE '%' || @source_filter || '%'
) ELSE TRUE END) AND
(CASE WHEN sqlc.narg('longer_than')::NUMERIC IS NOT NULL THEN (
c.duration > @longer_than
) ELSE TRUE END) AND
@ -158,6 +161,9 @@ CASE WHEN sqlc.narg('tags_not')::TEXT[] IS NOT NULL THEN
tgs.name ILIKE '%' || @tg_filter || '%' OR
tgs.alpha_tag ILIKE '%' || @tg_filter || '%'
) ELSE TRUE END) AND
(CASE WHEN sqlc.narg('source_filter')::TEXT IS NOT NULL THEN (
c.talker_alias ILIKE '%' || @source_filter || '%'
) ELSE TRUE END) AND
(CASE WHEN sqlc.narg('longer_than')::NUMERIC IS NOT NULL THEN (
c.duration > @longer_than
) ELSE TRUE END) AND