global filter

This commit is contained in:
2026-03-11 17:56:17 +01:00
parent ce67fb4a34
commit c2bcac58b3
8 changed files with 119 additions and 79 deletions

View File

@@ -16,7 +16,7 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Ersteller</mat-label> <mat-label>Ersteller</mat-label>
<mat-select formControlName="owner"> <mat-select formControlName="owner">
<mat-option [value]="null">Alle</mat-option> <mat-option value="">Alle</mat-option>
@for (owner of owners; track owner) { @for (owner of owners; track owner) {
<mat-option [value]="owner.key">{{ <mat-option [value]="owner.key">{{
owner.value owner.value
@@ -29,7 +29,7 @@
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>Art der Veranstaltung</mat-label> <mat-label>Art der Veranstaltung</mat-label>
<mat-select formControlName="showType"> <mat-select formControlName="showType">
<mat-option [value]="null">Alle</mat-option> <mat-option value="">Alle</mat-option>
<mat-optgroup label="öffentlich"> <mat-optgroup label="öffentlich">
@for (key of showTypePublic; track key) { @for (key of showTypePublic; track key) {
<mat-option [value]="key">{{ <mat-option [value]="key">{{

View File

@@ -1,6 +1,5 @@
import {Component, Input, inject} from '@angular/core'; import {Component, Input, inject} from '@angular/core';
import {KeyValue} from '@angular/common'; import {KeyValue} from '@angular/common';
import {ActivatedRoute, Router} from '@angular/router';
import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms'; import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms';
import {FilterValues} from './filter-values'; import {FilterValues} from './filter-values';
import {Show} from '../../services/show'; import {Show} from '../../services/show';
@@ -9,6 +8,7 @@ import {distinctUntilChanged, map, switchMap} from 'rxjs/operators';
import {combineLatest, Observable, of} from 'rxjs'; import {combineLatest, Observable, of} from 'rxjs';
import {dynamicSort, onlyUnique} from '../../../../services/filter.helper'; import {dynamicSort, onlyUnique} from '../../../../services/filter.helper';
import {UserService} from '../../../../services/user/user.service'; import {UserService} from '../../../../services/user/user.service';
import {FilterStoreService} from '../../../../services/filter-store.service';
import {MatFormField, MatLabel} from '@angular/material/form-field'; import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatSelect} from '@angular/material/select'; import {MatSelect} from '@angular/material/select';
import {MatOptgroup, MatOption} from '@angular/material/core'; import {MatOptgroup, MatOption} from '@angular/material/core';
@@ -21,11 +21,10 @@ import {ShowTypePipe} from '../../../../widget-modules/pipes/show-type-translate
imports: [ReactiveFormsModule, MatFormField, MatLabel, MatSelect, MatOption, MatOptgroup, ShowTypePipe], imports: [ReactiveFormsModule, MatFormField, MatLabel, MatSelect, MatOption, MatOptgroup, ShowTypePipe],
}) })
export class FilterComponent { export class FilterComponent {
private router = inject(Router);
private showService = inject(ShowService); private showService = inject(ShowService);
private userService = inject(UserService); private userService = inject(UserService);
private filterStore = inject(FilterStoreService);
@Input() public route = '/shows/';
@Input() public shows: Show[] = []; @Input() public shows: Show[] = [];
public showTypePublic = ShowService.SHOW_TYPE_PUBLIC; public showTypePublic = ShowService.SHOW_TYPE_PUBLIC;
@@ -42,7 +41,6 @@ export class FilterComponent {
public owners: {key: string; value: string}[] = []; public owners: {key: string; value: string}[] = [];
public constructor() { public constructor() {
const activatedRoute = inject(ActivatedRoute);
const fb = inject(UntypedFormBuilder); const fb = inject(UntypedFormBuilder);
this.filterFormGroup = fb.group({ this.filterFormGroup = fb.group({
@@ -51,16 +49,20 @@ export class FilterComponent {
showType: null, showType: null,
}); });
activatedRoute.queryParams.subscribe(params => { this.filterStore.showFilter$.subscribe(filterValues => {
const filterValues = params as FilterValues; this.filterFormGroup.patchValue(
if (filterValues.time) this.filterFormGroup.controls.time.setValue(+filterValues.time); {
this.filterFormGroup.controls.owner.setValue(filterValues.owner ?? null, {emitEvent: false}); time: filterValues.time,
this.filterFormGroup.controls.showType.setValue(filterValues.showType ?? null, {emitEvent: false}); owner: filterValues.owner || null,
showType: filterValues.showType || null,
},
{emitEvent: false}
);
}); });
this.filterFormGroup.controls.time.valueChanges.subscribe(_ => void this.filerValueChanged('time', _ as number)); this.filterFormGroup.controls.time.valueChanges.subscribe(_ => this.filterValueChanged('time', (_ as number) ?? 1));
this.filterFormGroup.controls.owner.valueChanges.subscribe(_ => void this.filerValueChanged('owner', _ as string)); this.filterFormGroup.controls.owner.valueChanges.subscribe(_ => this.filterValueChanged('owner', (_ as string | null) ?? ''));
this.filterFormGroup.controls.showType.valueChanges.subscribe(_ => void this.filerValueChanged('showType', _ as string)); this.filterFormGroup.controls.showType.valueChanges.subscribe(_ => this.filterValueChanged('showType', (_ as string | null) ?? ''));
this.owners$().subscribe(owners => (this.owners = owners)); this.owners$().subscribe(owners => (this.owners = owners));
} }
@@ -97,12 +99,8 @@ export class FilterComponent {
); );
}; };
private async filerValueChanged<T>(key: string, value: T): Promise<void> { private filterValueChanged<T>(key: keyof FilterValues, value: T): void {
const route = this.router.createUrlTree([this.route], { this.filterStore.updateShowFilter({[key]: value} as Partial<FilterValues>);
queryParams: {[key]: value || null},
queryParamsHandling: 'merge',
});
await this.router.navigateByUrl(route);
} }
private sameOwners(left: {key: string; value: string}[], right: {key: string; value: string}[]): boolean { private sameOwners(left: {key: string; value: string}[], right: {key: string; value: string}[]): boolean {

View File

@@ -3,9 +3,9 @@ import {combineLatest} from 'rxjs';
import {Show} from '../services/show'; import {Show} from '../services/show';
import {fade} from '../../../animations'; import {fade} from '../../../animations';
import {ShowService} from '../services/show.service'; import {ShowService} from '../services/show.service';
import {FilterValues} from './filter/filter-values'; import {FilterValues} from './filter/filter-values'import {RouterLink} from '@angular/router';
import {ActivatedRoute, RouterLink} from '@angular/router';
import {map, switchMap} from 'rxjs/operators'; import {map, switchMap} from 'rxjs/operators';
import {FilterStoreService} from '../../../services/filter-store.service';
import {RoleDirective} from '../../../services/user/role.directive'; import {RoleDirective} from '../../../services/user/role.directive';
import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component'; import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component';
import {AsyncPipe} from '@angular/common'; import {AsyncPipe} from '@angular/common';
@@ -23,37 +23,20 @@ import {SortByPipe} from '../../../widget-modules/pipes/sort-by/sort-by.pipe';
}) })
export class ListComponent { export class ListComponent {
private showService = inject(ShowService); private showService = inject(ShowService);
private activatedRoute = inject(ActivatedRoute); private filterStore = inject(FilterStoreService);
public lastMonths$ = this.activatedRoute.queryParams.pipe( public filter$ = this.filterStore.showFilter$;
map(params => { public lastMonths$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.time || 1));
const filterValues = params as FilterValues; public owner$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.owner));
if (!filterValues?.time) return 1; public showType$ = this.filter$.pipe(map((filterValues: FilterValues) => filterValues.showType));
return +filterValues.time;
})
);
public owner$ = this.activatedRoute.queryParams.pipe(
map(params => {
const filterValues = params as FilterValues;
return filterValues?.owner;
})
);
public showType$ = this.activatedRoute.queryParams.pipe(
map(params => {
const filterValues = params as FilterValues;
return filterValues?.showType;
})
);
public shows$ = this.showService.list$(); public shows$ = this.showService.list$();
public privateShows$ = this.showService.list$().pipe(map(show => show.filter(_ => !_.published))); public privateShows$ = combineLatest([this.shows$, this.filter$]).pipe(
map(([shows, filter]) => shows.filter(show => !show.published).filter(show => this.matchesPrivateFilter(show, filter)))
);
public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths))); public queriedPublicShows$ = this.lastMonths$.pipe(switchMap(lastMonths => this.showService.listPublicSince$(lastMonths)));
public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe( public fallbackPublicShows$ = combineLatest([this.shows$, this.lastMonths$]).pipe(
map(([shows, lastMonths]) => { map(([shows, lastMonths]) => {
const startDate = new Date(); return shows.filter(show => show.published && !show.archived).filter(show => this.matchesTimeFilter(show, lastMonths));
startDate.setHours(0, 0, 0, 0);
startDate.setDate(startDate.getDate() - lastMonths * 30);
return shows.filter(show => show.published && !show.archived && show.date.toDate() >= startDate);
}) })
); );
public publicShows$ = combineLatest([this.queriedPublicShows$, this.fallbackPublicShows$, this.owner$, this.showType$]).pipe( public publicShows$ = combineLatest([this.queriedPublicShows$, this.fallbackPublicShows$, this.owner$, this.showType$]).pipe(
@@ -65,4 +48,19 @@ export class ListComponent {
); );
public trackBy = (index: number, show: unknown) => (show as Show).id; public trackBy = (index: number, show: unknown) => (show as Show).id;
private matchesFilter(show: Show, filter: FilterValues): boolean {
return this.matchesTimeFilter(show, filter.time || 1) && (!filter.owner || show.owner === filter.owner) && (!filter.showType || show.showType === filter.showType);
}
private matchesPrivateFilter(show: Show, filter: FilterValues): boolean {
return this.matchesTimeFilter(show, filter.time || 1) && (!filter.showType || show.showType === filter.showType);
}
private matchesTimeFilter(show: Show, lastMonths: number): boolean {
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
startDate.setDate(startDate.getDate() - lastMonths * 30);
return show.date.toDate() >= startDate;
}
} }

View File

@@ -24,12 +24,15 @@ export class ShowDataService {
public listRaw$ = () => this.dbService.col$<Show>(this.collection); public listRaw$ = () => this.dbService.col$<Show>(this.collection);
public listPublicSince$(lastMonths: number): Observable<Show[]> { public listPublicSince$(lastMonths: number): Observable<Show[]> {
const startDate = new Date(); const queryConstraints: QueryConstraint[] = [where('published', '==', true), orderBy('date', 'desc')];
startDate.setHours(0, 0, 0, 0);
startDate.setDate(startDate.getDate() - lastMonths * 30);
const startTimestamp = Timestamp.fromDate(startDate);
const queryConstraints: QueryConstraint[] = [where('published', '==', true), where('date', '>=', startTimestamp), orderBy('date', 'desc')]; if (lastMonths < 99999) {
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
startDate.setDate(startDate.getDate() - lastMonths * 30);
const startTimestamp = Timestamp.fromDate(startDate);
queryConstraints.splice(1, 0, where('date', '>=', startTimestamp));
}
return this.dbService.col$<Show>(this.collection, queryConstraints).pipe( return this.dbService.col$<Show>(this.collection, queryConstraints).pipe(
map(shows => shows.filter(show => !show.archived)), map(shows => shows.filter(show => !show.archived)),

View File

@@ -1,10 +1,10 @@
import {Component, Input, inject} from '@angular/core'; import {Component, Input, inject} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms'; import {ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup} from '@angular/forms';
import {SongService} from '../../services/song.service'; import {SongService} from '../../services/song.service';
import {FilterValues} from './filter-values'; import {FilterValues} from './filter-values';
import {Song} from '../../services/song'; import {Song} from '../../services/song';
import {KEYS} from '../../services/key.helper'; import {KEYS} from '../../services/key.helper';
import {FilterStoreService} from '../../../../services/filter-store.service';
import {MatFormField, MatLabel} from '@angular/material/form-field'; import {MatFormField, MatLabel} from '@angular/material/form-field';
import {MatInput} from '@angular/material/input'; import {MatInput} from '@angular/material/input';
import {MatSelect} from '@angular/material/select'; import {MatSelect} from '@angular/material/select';
@@ -21,17 +21,15 @@ import {SongTypePipe} from '../../../../widget-modules/pipes/song-type-translate
imports: [ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatSelect, MatOption, LegalTypePipe, KeyPipe, SongTypePipe], imports: [ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatSelect, MatOption, LegalTypePipe, KeyPipe, SongTypePipe],
}) })
export class FilterComponent { export class FilterComponent {
private router = inject(Router); private filterStore = inject(FilterStoreService);
public filterFormGroup: UntypedFormGroup; public filterFormGroup: UntypedFormGroup;
@Input() public route = '/';
@Input() public songs: Song[] = []; @Input() public songs: Song[] = [];
public types = SongService.TYPES; public types = SongService.TYPES;
public legalType = SongService.LEGAL_TYPE; public legalType = SongService.LEGAL_TYPE;
public keys = KEYS; public keys = KEYS;
public constructor() { public constructor() {
const activatedRoute = inject(ActivatedRoute);
const fb = inject(UntypedFormBuilder); const fb = inject(UntypedFormBuilder);
this.filterFormGroup = fb.group({ this.filterFormGroup = fb.group({
@@ -42,20 +40,15 @@ export class FilterComponent {
flag: '', flag: '',
}); });
activatedRoute.queryParams.subscribe(params => { this.filterStore.songFilter$.subscribe(filterValues => {
const filterValues = params as FilterValues; this.filterFormGroup.patchValue(filterValues, {emitEvent: false});
if (filterValues.q) this.filterFormGroup.controls.q.setValue(filterValues.q);
if (filterValues.type) this.filterFormGroup.controls.type.setValue(filterValues.type);
if (filterValues.key) this.filterFormGroup.controls.key.setValue(filterValues.key);
if (filterValues.legalType) this.filterFormGroup.controls.legalType.setValue(filterValues.legalType);
if (filterValues.flag) this.filterFormGroup.controls.flag.setValue(filterValues.flag);
}); });
this.filterFormGroup.controls.q.valueChanges.subscribe(_ => void this.filerValueChanged('q', _ as string)); this.filterFormGroup.controls.q.valueChanges.subscribe(_ => this.filterValueChanged('q', (_ as string) ?? ''));
this.filterFormGroup.controls.key.valueChanges.subscribe(_ => void this.filerValueChanged('key', _ as string)); this.filterFormGroup.controls.key.valueChanges.subscribe(_ => this.filterValueChanged('key', (_ as string) ?? ''));
this.filterFormGroup.controls.type.valueChanges.subscribe(_ => void this.filerValueChanged('type', _ as string)); this.filterFormGroup.controls.type.valueChanges.subscribe(_ => this.filterValueChanged('type', (_ as string) ?? ''));
this.filterFormGroup.controls.legalType.valueChanges.subscribe(_ => void this.filerValueChanged('legalType', _ as string)); this.filterFormGroup.controls.legalType.valueChanges.subscribe(_ => this.filterValueChanged('legalType', (_ as string) ?? ''));
this.filterFormGroup.controls.flag.valueChanges.subscribe(_ => void this.filerValueChanged('flag', _ as string)); this.filterFormGroup.controls.flag.valueChanges.subscribe(_ => this.filterValueChanged('flag', (_ as string) ?? ''));
} }
public getFlags(): string[] { public getFlags(): string[] {
@@ -69,11 +62,7 @@ export class FilterComponent {
return flags.filter((n, i) => flags.indexOf(n) === i); return flags.filter((n, i) => flags.indexOf(n) === i);
} }
private async filerValueChanged(key: string, value: string): Promise<void> { private filterValueChanged(key: keyof FilterValues, value: string): void {
const route = this.router.createUrlTree([this.route], { this.filterStore.updateSongFilter({[key]: value} as Partial<FilterValues>);
queryParams: {[key]: value},
queryParamsHandling: 'merge',
});
await this.router.navigateByUrl(route);
} }
} }

View File

@@ -1,7 +1,7 @@
@if (songs$ | async; as songs) { @if (songs$ | async; as songs) {
<div> <div>
<app-list-header [anyFilterActive]="anyFilterActive"> <app-list-header [anyFilterActive]="anyFilterActive">
<app-filter [songs]="songs" route="songs"></app-filter> <app-filter [songs]="songs"></app-filter>
</app-list-header> </app-list-header>
<app-card [padding]="false"> <app-card [padding]="false">
@for (song of songs; track trackBy($index, song)) { @for (song of songs; track trackBy($index, song)) {

View File

@@ -4,12 +4,13 @@ import {Song} from '../services/song';
import {map} from 'rxjs/operators'; import {map} from 'rxjs/operators';
import {combineLatest, Observable} from 'rxjs'; import {combineLatest, Observable} from 'rxjs';
import {fade} from '../../../animations'; import {fade} from '../../../animations';
import {ActivatedRoute, RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {filterSong} from '../../../services/filter.helper'; import {filterSong} from '../../../services/filter.helper';
import {FilterValues} from './filter/filter-values'; import {FilterValues} from './filter/filter-values';
import {ScrollService} from '../../../services/scroll.service'; import {ScrollService} from '../../../services/scroll.service';
import {faBalanceScaleRight, faCheck, faPencilRuler} from '@fortawesome/free-solid-svg-icons'; import {faBalanceScaleRight, faCheck, faPencilRuler} from '@fortawesome/free-solid-svg-icons';
import {TextRenderingService} from '../services/text-rendering.service'; import {TextRenderingService} from '../services/text-rendering.service';
import {FilterStoreService} from '../../../services/filter-store.service';
import {AsyncPipe} from '@angular/common'; import {AsyncPipe} from '@angular/common';
import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component'; import {ListHeaderComponent} from '../../../widget-modules/components/list-header/list-header.component';
import {FilterComponent} from './filter/filter.component'; import {FilterComponent} from './filter/filter.component';
@@ -31,13 +32,13 @@ interface SongListItem extends Song {
}) })
export class SongListComponent implements OnInit, OnDestroy { export class SongListComponent implements OnInit, OnDestroy {
private songService = inject(SongService); private songService = inject(SongService);
private activatedRoute = inject(ActivatedRoute);
private scrollService = inject(ScrollService); private scrollService = inject(ScrollService);
private textRenderingService = inject(TextRenderingService); private textRenderingService = inject(TextRenderingService);
private filterStore = inject(FilterStoreService);
public anyFilterActive = false; public anyFilterActive = false;
public songs$: Observable<SongListItem[]> = combineLatest([ public songs$: Observable<SongListItem[]> = combineLatest([
this.activatedRoute.queryParams.pipe(map(_ => _ as FilterValues)), this.filterStore.songFilter$,
this.songService.list$().pipe(map(songs => [...songs].sort((a, b) => a.number - b.number))), this.songService.list$().pipe(map(songs => [...songs].sort((a, b) => a.number - b.number))),
]).pipe( ]).pipe(
map(([filter, songs]) => { map(([filter, songs]) => {

View File

@@ -0,0 +1,51 @@
import {Injectable} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {FilterValues as SongFilterValues} from '../modules/songs/song-list/filter/filter-values';
import {FilterValues as ShowFilterValues} from '../modules/shows/list/filter/filter-values';
const DEFAULT_SONG_FILTER: SongFilterValues = {
q: '',
type: '',
key: '',
legalType: '',
flag: '',
};
const DEFAULT_SHOW_FILTER: ShowFilterValues = {
time: 1,
owner: '',
showType: '',
};
@Injectable({
providedIn: 'root',
})
export class FilterStoreService {
private readonly songFilterSubject = new BehaviorSubject<SongFilterValues>(DEFAULT_SONG_FILTER);
private readonly showFilterSubject = new BehaviorSubject<ShowFilterValues>(DEFAULT_SHOW_FILTER);
public readonly songFilter$: Observable<SongFilterValues> = this.songFilterSubject.asObservable();
public readonly showFilter$: Observable<ShowFilterValues> = this.showFilterSubject.asObservable();
public updateSongFilter(filter: Partial<SongFilterValues>): void {
this.songFilterSubject.next({
...this.songFilterSubject.value,
...filter,
});
}
public updateShowFilter(filter: Partial<ShowFilterValues>): void {
this.showFilterSubject.next({
...this.showFilterSubject.value,
...filter,
});
}
public resetSongFilter(): void {
this.songFilterSubject.next(DEFAULT_SONG_FILTER);
}
public resetShowFilter(): void {
this.showFilterSubject.next(DEFAULT_SHOW_FILTER);
}
}