refactoring Filter Handling

This commit is contained in:
Andreas Knuth 2025-08-08 18:10:04 -05:00
parent c5c210b616
commit 7b94785a30
10 changed files with 1353 additions and 758 deletions

View File

@ -1,25 +1,26 @@
import { BreakpointObserver } from '@angular/cdk/layout';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core'; import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons'; import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Collapse, Dropdown, initFlowbite } from 'flowbite'; import { Collapse, Dropdown, initFlowbite } from 'flowbite';
import { debounceTime, filter, Observable, Subject, Subscription } from 'rxjs'; import { filter, Observable, Subject, takeUntil } from 'rxjs';
import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model'; import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, KeyValueAsSortBy, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { emailToDirName, KeycloakUser, KeyValueAsSortBy, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { CriteriaChangeService } from '../../services/criteria-change.service'; import { FilterStateService } from '../../services/filter-state.service';
import { ListingsService } from '../../services/listings.service'; import { ListingsService } from '../../services/listings.service';
import { SearchService } from '../../services/search.service'; import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { SharedService } from '../../services/shared.service'; import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { assignProperties, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils'; import { map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component'; import { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service'; import { ModalService } from '../search-modal/modal.service';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
selector: 'header', selector: 'header',
@ -28,7 +29,7 @@ import { ModalService } from '../search-modal/modal.service';
templateUrl: './header.component.html', templateUrl: './header.component.html',
styleUrl: './header.component.scss', styleUrl: './header.component.scss',
}) })
export class HeaderComponent { export class HeaderComponent implements OnInit, OnDestroy {
public buildVersion = environment.buildVersion; public buildVersion = environment.buildVersion;
user$: Observable<KeycloakUser>; user$: Observable<KeycloakUser>;
keycloakUser: KeycloakUser; keycloakUser: KeycloakUser;
@ -41,27 +42,31 @@ export class HeaderComponent {
isMobile: boolean = false; isMobile: boolean = false;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
prompt: string; prompt: string;
private subscription: Subscription;
criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria; // Aktueller Listing-Typ basierend auf Route
private routerSubscription: Subscription | undefined; currentListingType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings' | null = null;
baseRoute: string;
sortDropdownVisible: boolean; // Sortierung
sortDropdownVisible: boolean = false;
sortByOptions: KeyValueAsSortBy[] = []; sortByOptions: KeyValueAsSortBy[] = [];
sortBy: SortByOptions = null;
// Observable für Anzahl der Listings
numberOfBroker$: Observable<number>; numberOfBroker$: Observable<number>;
numberOfCommercial$: Observable<number>; numberOfCommercial$: Observable<number>;
sortBy: SortByOptions = null; // Neu: Separate Property
constructor( constructor(
private router: Router, private router: Router,
private userService: UserService, private userService: UserService,
private sharedService: SharedService, private sharedService: SharedService,
private breakpointObserver: BreakpointObserver,
private modalService: ModalService, private modalService: ModalService,
private searchService: SearchService, private searchService: SearchService,
private criteriaChangeService: CriteriaChangeService, private filterStateService: FilterStateService,
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
public authService: AuthService, public authService: AuthService,
private listingService: ListingsService, private listingService: ListingsService,
) {} ) {}
@HostListener('document:click', ['$event']) @HostListener('document:click', ['$event'])
handleGlobalClick(event: Event) { handleGlobalClick(event: Event) {
const target = event.target as HTMLElement; const target = event.target as HTMLElement;
@ -69,91 +74,125 @@ export class HeaderComponent {
this.sortDropdownVisible = false; this.sortDropdownVisible = false;
} }
} }
async ngOnInit() { async ngOnInit() {
// User Setup
const token = await this.authService.getToken(); const token = await this.authService.getToken();
this.keycloakUser = map2User(token); this.keycloakUser = map2User(token);
if (this.keycloakUser) { if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser?.email); this.user = await this.userService.getByMail(this.keycloakUser?.email);
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
} }
this.numberOfBroker$ = this.userService.getNumberOfBroker(createEmptyUserListingCriteria());
// Lade Anzahl der Listings
this.numberOfBroker$ = this.userService.getNumberOfBroker(this.createEmptyUserListingCriteria());
this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty'); this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty');
// Flowbite initialisieren
setTimeout(() => { setTimeout(() => {
initFlowbite(); initFlowbite();
}, 10); }, 10);
this.sharedService.currentProfilePhoto.subscribe(photoUrl => { // Profile Photo Updates
this.sharedService.currentProfilePhoto.pipe(untilDestroyed(this)).subscribe(photoUrl => {
this.profileUrl = photoUrl; this.profileUrl = photoUrl;
}); });
this.checkCurrentRoute(this.router.url); // User Updates
this.setupSortByOptions();
this.loadSortBy(); // Neu
this.routerSubscription = this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: any) => {
this.checkCurrentRoute(event.urlAfterRedirects);
this.setupSortByOptions();
});
this.subscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => {
this.criteria = getCriteriaProxy(this.baseRoute, this);
});
this.userService.currentUser.pipe(untilDestroyed(this)).subscribe(u => { this.userService.currentUser.pipe(untilDestroyed(this)).subscribe(u => {
this.user = u; this.user = u;
}); });
}
private loadSortBy() { // Router Events
const storedSortBy = sessionStorage.getItem(this.getSortByKey()); this.router.events
this.sortBy = storedSortBy ? (storedSortBy as SortByOptions) : null; .pipe(
filter(event => event instanceof NavigationEnd),
untilDestroyed(this),
)
.subscribe((event: NavigationEnd) => {
this.checkCurrentRoute(event.urlAfterRedirects);
});
// Initial Route Check
this.checkCurrentRoute(this.router.url);
} }
private saveSortBy() {
sessionStorage.setItem(this.getSortByKey(), this.sortBy);
}
private getSortByKey(): string {
// Basierend auf Route (für Business/Commercial unterscheiden)
if (this.isBusinessListing()) return 'businessSortBy';
if (this.isCommercialPropertyListing()) return 'commercialSortBy';
if (this.isProfessionalListing()) return 'professionalsSortBy';
return 'defaultSortBy'; // Fallback
}
sortByFct(selectedSortBy: SortByOptions) {
this.sortBy = selectedSortBy;
this.saveSortBy(); // Speichere separat
this.sortDropdownVisible = false;
this.searchService.search(this.criteria.criteriaType); // Neu: Übergebe sortBy separat
}
private checkCurrentRoute(url: string): void { private checkCurrentRoute(url: string): void {
this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/' const baseRoute = url.split('/')[1];
const specialRoutes = [, '', ''];
this.criteria = getCriteriaProxy(this.baseRoute, this); // Bestimme den aktuellen Listing-Typ
if (baseRoute === 'businessListings') {
this.currentListingType = 'businessListings';
} else if (baseRoute === 'commercialPropertyListings') {
this.currentListingType = 'commercialPropertyListings';
} else if (baseRoute === 'brokerListings') {
this.currentListingType = 'brokerListings';
} else {
this.currentListingType = null;
return; // Keine relevante Route für Filter/Sort
}
// Setup für diese Route
this.setupSortByOptions();
this.subscribeToStateChanges();
} }
setupSortByOptions() {
private subscribeToStateChanges(): void {
if (!this.currentListingType) return;
// Abonniere State-Änderungen für den aktuellen Listing-Typ
this.filterStateService
.getState$(this.currentListingType)
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
this.sortBy = state.sortBy;
});
}
private setupSortByOptions(): void {
this.sortByOptions = []; this.sortByOptions = [];
let storedSortBy = null;
if (this.isProfessionalListing()) { if (!this.currentListingType) return;
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'professional')];
storedSortBy = sessionStorage.getItem('professionalsSortBy'); switch (this.currentListingType) {
} case 'brokerListings':
if (this.isBusinessListing()) { this.sortByOptions = [...this.selectOptions.sortByOptions.filter(s => s.type === 'professional')];
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'business' || s.type === 'listing')]; break;
storedSortBy = sessionStorage.getItem('businessSortBy'); case 'businessListings':
} this.sortByOptions = [...this.selectOptions.sortByOptions.filter(s => s.type === 'business' || s.type === 'listing')];
if (this.isCommercialPropertyListing()) { break;
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'commercial' || s.type === 'listing')]; case 'commercialPropertyListings':
storedSortBy = sessionStorage.getItem('commercialSortBy'); this.sortByOptions = [...this.selectOptions.sortByOptions.filter(s => s.type === 'commercial' || s.type === 'listing')];
break;
} }
// Füge generische Optionen hinzu (ohne type)
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => !s.type)]; this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => !s.type)];
this.sortBy = storedSortBy && storedSortBy !== 'null' ? (storedSortBy as SortByOptions) : null;
} }
ngAfterViewInit() {}
sortByFct(selectedSortBy: SortByOptions): void {
if (!this.currentListingType) return;
this.sortDropdownVisible = false;
// Update sortBy im State
this.filterStateService.updateSortBy(this.currentListingType, selectedSortBy);
// Trigger search
this.searchService.search(this.currentListingType);
}
async openModal() { async openModal() {
const modalResult = await this.modalService.showModal(this.criteria); if (!this.currentListingType) return;
const criteria = this.filterStateService.getCriteria(this.currentListingType);
const modalResult = await this.modalService.showModal(criteria);
if (modalResult.accepted) { if (modalResult.accepted) {
this.searchService.search(this.criteria.criteriaType); this.searchService.search(this.currentListingType);
} else {
this.criteria = assignProperties(this.criteria, modalResult.criteria);
} }
} }
navigateWithState(dest: string, state: any) { navigateWithState(dest: string, state: any) {
this.router.navigate([dest], { state: state }); this.router.navigate([dest], { state: state });
} }
@ -161,17 +200,21 @@ export class HeaderComponent {
isActive(route: string): boolean { isActive(route: string): boolean {
return this.router.url === route; return this.router.url === route;
} }
isFilterUrl(): boolean { isFilterUrl(): boolean {
return ['/businessListings', '/commercialPropertyListings', '/brokerListings'].includes(this.router.url); return ['/businessListings', '/commercialPropertyListings', '/brokerListings'].includes(this.router.url);
} }
isBusinessListing(): boolean { isBusinessListing(): boolean {
return ['/businessListings'].includes(this.router.url); return this.router.url === '/businessListings';
} }
isCommercialPropertyListing(): boolean { isCommercialPropertyListing(): boolean {
return ['/commercialPropertyListings'].includes(this.router.url); return this.router.url === '/commercialPropertyListings';
} }
isProfessionalListing(): boolean { isProfessionalListing(): boolean {
return ['/brokerListings'].includes(this.router.url); return this.router.url === '/brokerListings';
} }
closeDropdown() { closeDropdown() {
@ -183,6 +226,7 @@ export class HeaderComponent {
dropdown.hide(); dropdown.hide();
} }
} }
closeMobileMenu() { closeMobileMenu() {
const targetElement = document.getElementById('navbar-user'); const targetElement = document.getElementById('navbar-user');
const triggerElement = document.querySelector('[data-collapse-toggle="navbar-user"]'); const triggerElement = document.querySelector('[data-collapse-toggle="navbar-user"]');
@ -192,23 +236,60 @@ export class HeaderComponent {
collapse.collapse(); collapse.collapse();
} }
} }
closeMenusAndSetCriteria(path: string) { closeMenusAndSetCriteria(path: string) {
this.closeDropdown(); this.closeDropdown();
this.closeMobileMenu(); this.closeMobileMenu();
const criteria = getCriteriaProxy(path, this);
criteria.page = 1; // Bestimme Listing-Typ aus dem Pfad
criteria.start = 0; let listingType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings' | null = null;
if (path === 'businessListings') {
listingType = 'businessListings';
} else if (path === 'commercialPropertyListings') {
listingType = 'commercialPropertyListings';
} else if (path === 'brokerListings') {
listingType = 'brokerListings';
}
if (listingType) {
// Reset Pagination beim Wechsel zwischen Views
this.filterStateService.updateCriteria(listingType, {
page: 1,
start: 0,
});
}
}
toggleSortDropdown() {
this.sortDropdownVisible = !this.sortDropdownVisible;
}
get isProfessional() {
return this.user?.customerType === 'professional';
}
// Helper method für leere UserListingCriteria
private createEmptyUserListingCriteria(): UserListingCriteria {
return {
criteriaType: 'brokerListings',
types: [],
state: null,
city: null,
radius: null,
searchType: 'exact' as const,
brokerName: null,
companyName: null,
counties: [],
prompt: null,
page: 1,
start: 0,
length: 12,
};
} }
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }
toggleSortDropdown() {
this.sortDropdownVisible = !this.sortDropdownVisible;
}
get isProfessional() {
return this.user?.customerType === 'professional';
}
} }

View File

@ -1,17 +1,15 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { catchError, concat, debounceTime, distinctUntilChanged, map, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs'; import { catchError, concat, debounceTime, distinctUntilChanged, map, Observable, of, Subject, switchMap, takeUntil, tap } from 'rxjs';
import { CommercialPropertyListingCriteria, CountyResult, GeoResult } from '../../../../../bizmatch-server/src/models/main.model'; import { CommercialPropertyListingCriteria, CountyResult, GeoResult } from '../../../../../bizmatch-server/src/models/main.model';
import { CriteriaChangeService } from '../../services/criteria-change.service'; import { FilterStateService } from '../../services/filter-state.service';
import { GeoService } from '../../services/geo.service'; import { GeoService } from '../../services/geo.service';
import { ListingsService } from '../../services/listings.service'; import { ListingsService } from '../../services/listings.service';
import { SearchService } from '../../services/search.service'; import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { getCriteriaStateObject, resetCommercialPropertyListingCriteria } from '../../utils/utils';
import { ValidatedCityComponent } from '../validated-city/validated-city.component'; import { ValidatedCityComponent } from '../validated-city/validated-city.component';
import { ValidatedPriceComponent } from '../validated-price/validated-price.component'; import { ValidatedPriceComponent } from '../validated-price/validated-price.component';
import { ModalService } from './modal.service'; import { ModalService } from './modal.service';
@ -24,101 +22,85 @@ import { ModalService } from './modal.service';
templateUrl: './search-modal-commercial.component.html', templateUrl: './search-modal-commercial.component.html',
styleUrls: ['./search-modal.component.scss'], styleUrls: ['./search-modal.component.scss'],
}) })
export class SearchModalCommercialComponent { export class SearchModalCommercialComponent implements OnInit, OnDestroy {
@Input() @Input() isModal: boolean = true;
isModal: boolean = true;
// cities$: Observable<GeoResult[]>; private destroy$ = new Subject<void>();
private searchDebounce$ = new Subject<void>();
// State
criteria: CommercialPropertyListingCriteria;
backupCriteria: any;
// Geo search
counties$: Observable<CountyResult[]>; counties$: Observable<CountyResult[]>;
// cityLoading = false;
countyLoading = false; countyLoading = false;
// cityInput$ = new Subject<string>();
countyInput$ = new Subject<string>(); countyInput$ = new Subject<string>();
private criteriaChangeSubscription: Subscription;
public criteria: CommercialPropertyListingCriteria; // Results count
private debounceTimeout: any;
public backupCriteria: CommercialPropertyListingCriteria = getCriteriaStateObject('businessListings');
numberOfResults$: Observable<number>; numberOfResults$: Observable<number>;
cancelDisable = false; cancelDisable = false;
constructor( constructor(
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
public modalService: ModalService, public modalService: ModalService,
private geoService: GeoService, private geoService: GeoService,
private criteriaChangeService: CriteriaChangeService, private filterStateService: FilterStateService,
private listingService: ListingsService, private listingService: ListingsService,
private userService: UserService,
private searchService: SearchService, private searchService: SearchService,
) {} ) {}
// Define property type options
selectedPropertyType: string | null = null; ngOnInit(): void {
selectedPropertyTypeName: string | null = null; // Load counties
ngOnInit() {
this.setupCriteriaChangeListener();
this.modalService.message$.pipe(untilDestroyed(this)).subscribe(msg => {
this.criteria = msg as CommercialPropertyListingCriteria;
this.backupCriteria = JSON.parse(JSON.stringify(msg));
this.setTotalNumberOfResults();
});
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
if (val.visible) {
this.criteria.page = 1;
this.criteria.start = 0;
}
});
// this.loadCities();
this.loadCounties(); this.loadCounties();
this.modalService.sendCriteria(this.criteria);
} if (this.isModal) {
hasActiveFilters(): boolean { // Modal mode: Wait for messages from ModalService
return !!(this.criteria.state || this.criteria.city || this.criteria.minPrice || this.criteria.maxPrice || this.criteria.types.length || this.criteria.title); this.modalService.message$.pipe(untilDestroyed(this)).subscribe(criteria => {
} if (criteria?.criteriaType === 'commercialPropertyListings') {
removeFilter(filterType: string) { this.initializeWithCriteria(criteria);
switch (filterType) { }
case 'state': });
this.criteria.state = null;
this.setCity(null); this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
break; if (val.visible && val.type === 'commercialPropertyListings') {
case 'city': // Reset pagination when modal opens
this.criteria.city = null; if (this.criteria) {
this.criteria.radius = null; this.criteria.page = 1;
this.criteria.searchType = 'exact'; this.criteria.start = 0;
break; }
case 'price': }
this.criteria.minPrice = null; });
this.criteria.maxPrice = null; } else {
break; // Embedded mode: Subscribe to state changes
case 'types': this.subscribeToStateChanges();
this.criteria.types = [];
break;
case 'title':
this.criteria.title = null;
break;
} }
this.searchService.search(this.criteria.criteriaType);
} // Setup debounced search
clearFilter() { this.searchDebounce$.pipe(debounceTime(400), distinctUntilChanged(), takeUntil(this.destroy$)).subscribe(() => {
resetCommercialPropertyListingCriteria(this.criteria); this.triggerSearch();
this.searchService.search(this.criteria.criteriaType); });
}
// Handle category change
onCategoryChange(event: any[]) {
this.criteria.types = event;
this.onCriteriaChange();
} }
categoryClicked(checked: boolean, value: string) { private initializeWithCriteria(criteria: CommercialPropertyListingCriteria): void {
if (checked) { this.criteria = criteria;
this.criteria.types.push(value); this.backupCriteria = JSON.parse(JSON.stringify(criteria));
} else { this.setTotalNumberOfResults();
const index = this.criteria.types.findIndex(t => t === value);
if (index > -1) {
this.criteria.types.splice(index, 1);
}
}
this.searchService.search(this.criteria.criteriaType);
} }
private loadCounties() {
private subscribeToStateChanges(): void {
if (!this.isModal) {
this.filterStateService
.getState$('commercialPropertyListings')
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
this.criteria = { ...state.criteria };
this.setTotalNumberOfResults();
});
}
}
private loadCounties(): void {
this.counties$ = concat( this.counties$ = concat(
of([]), // default items of([]), // default items
this.countyInput$.pipe( this.countyInput$.pipe(
@ -126,83 +108,194 @@ export class SearchModalCommercialComponent {
tap(() => (this.countyLoading = true)), tap(() => (this.countyLoading = true)),
switchMap(term => switchMap(term =>
this.geoService.findCountiesStartingWith(term).pipe( this.geoService.findCountiesStartingWith(term).pipe(
catchError(() => of([])), // empty list on error catchError(() => of([])),
map(counties => counties.map(county => county.name)), // transform the list of objects to a list of city names map(counties => counties.map(county => county.name)),
tap(() => (this.countyLoading = false)), tap(() => (this.countyLoading = false)),
), ),
), ),
), ),
); );
} }
onCriteriaChange() {
this.searchService.search(this.criteria.criteriaType); // Filter removal methods
} removeFilter(filterType: string): void {
setCity(city) { const updates: any = {};
if (city) {
this.criteria.city = city; switch (filterType) {
this.criteria.state = city.state; case 'state':
} else { updates.state = null;
this.criteria.city = null; updates.city = null;
this.criteria.radius = null; updates.radius = null;
this.criteria.searchType = 'exact'; updates.searchType = 'exact';
break;
case 'city':
updates.city = null;
updates.radius = null;
updates.searchType = 'exact';
break;
case 'price':
updates.minPrice = null;
updates.maxPrice = null;
break;
case 'types':
updates.types = [];
break;
case 'title':
updates.title = null;
break;
} }
this.searchService.search(this.criteria.criteriaType);
this.updateCriteria(updates);
} }
setState(state: string) {
if (state) { // Category handling
this.criteria.state = state; onCategoryChange(selectedCategories: string[]): void {
this.updateCriteria({ types: selectedCategories });
}
categoryClicked(checked: boolean, value: string): void {
const types = [...(this.criteria.types || [])];
if (checked) {
if (!types.includes(value)) {
types.push(value);
}
} else { } else {
this.criteria.state = null; const index = types.indexOf(value);
this.setCity(null); if (index > -1) {
} types.splice(index, 1);
this.searchService.search(this.criteria.criteriaType);
}
setRadius(radius: number) {
this.criteria.radius = radius;
this.searchService.search(this.criteria.criteriaType);
}
private setupCriteriaChangeListener() {
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => {
this.setTotalNumberOfResults();
this.cancelDisable = true;
});
}
trackByFn(item: GeoResult) {
return item.id;
}
search() {
console.log('Search criteria:', this.criteria);
}
getCounties() {
this.geoService.findCountiesStartingWith('');
}
closeModal() {
console.log('Closing modal');
}
closeAndSearch() {
this.modalService.accept();
this.searchService.search(this.criteria.criteriaType);
this.close();
}
setTotalNumberOfResults() {
if (this.criteria) {
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
if (this.criteria.criteriaType === 'commercialPropertyListings') {
this.numberOfResults$ = this.listingService.getNumberOfListings('commercialProperty');
} else {
this.numberOfResults$ = of();
} }
} }
this.updateCriteria({ types });
} }
close() { // Location handling
this.modalService.reject(this.backupCriteria); setState(state: string): void {
const updates: any = { state };
if (!state) {
updates.city = null;
updates.radius = null;
updates.searchType = 'exact';
}
this.updateCriteria(updates);
} }
debouncedSearch() { setCity(city: any): void {
clearTimeout(this.debounceTimeout); const updates: any = {};
this.debounceTimeout = setTimeout(() => { if (city) {
this.searchService.search(this.criteria.criteriaType); updates.city = city;
}, 1000); updates.state = city.state;
} else {
updates.city = null;
updates.radius = null;
updates.searchType = 'exact';
}
this.updateCriteria(updates);
}
setRadius(radius: number): void {
this.updateCriteria({ radius });
}
onCriteriaChange(): void {
this.triggerSearch();
}
// Debounced search for text inputs
debouncedSearch(): void {
this.searchDebounce$.next();
}
// Clear all filters
clearFilter(): void {
if (this.isModal) {
// In modal: Reset locally
const defaultCriteria = this.getDefaultCriteria();
this.criteria = defaultCriteria;
this.setTotalNumberOfResults();
} else {
// Embedded: Use state service
this.filterStateService.clearFilters('commercialPropertyListings');
}
}
// Modal-specific methods
closeAndSearch(): void {
if (this.isModal) {
// Save changes to state
this.filterStateService.setCriteria('commercialPropertyListings', this.criteria);
this.modalService.accept();
this.searchService.search('commercialPropertyListings');
}
}
close(): void {
if (this.isModal) {
// Discard changes
this.modalService.reject(this.backupCriteria);
}
}
// Helper methods
private updateCriteria(updates: any): void {
if (this.isModal) {
// In modal: Update locally only
this.criteria = { ...this.criteria, ...updates };
this.setTotalNumberOfResults();
} else {
// Embedded: Update through state service
this.filterStateService.updateCriteria('commercialPropertyListings', updates);
}
// Trigger search after update
this.debouncedSearch();
}
private triggerSearch(): void {
if (this.isModal) {
// In modal: Only update count
this.setTotalNumberOfResults();
this.cancelDisable = true;
} else {
// Embedded: Full search
this.searchService.search('commercialPropertyListings');
}
}
private setTotalNumberOfResults(): void {
this.numberOfResults$ = this.listingService.getNumberOfListings('commercialProperty');
}
private getDefaultCriteria(): CommercialPropertyListingCriteria {
// Access the private method through a workaround or create it here
return {
criteriaType: 'commercialPropertyListings',
types: [],
state: null,
city: null,
radius: null,
searchType: 'exact' as const,
minPrice: null,
maxPrice: null,
title: null,
prompt: null,
page: 1,
start: 0,
length: 12,
};
}
hasActiveFilters(): boolean {
if (!this.criteria) return false;
return !!(this.criteria.state || this.criteria.city || this.criteria.minPrice || this.criteria.maxPrice || this.criteria.types?.length || this.criteria.title);
}
trackByFn(item: GeoResult): any {
return item.id;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
} }

View File

@ -1,20 +1,20 @@
import { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe, NgIf } from '@angular/common';
import { Component, Input, Output } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { catchError, concat, debounceTime, distinctUntilChanged, map, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs'; import { catchError, concat, debounceTime, distinctUntilChanged, map, Observable, of, Subject, switchMap, takeUntil, tap } from 'rxjs';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, CountyResult, GeoResult, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { BusinessListingCriteria, CountyResult, GeoResult, KeyValue, KeyValueStyle } from '../../../../../bizmatch-server/src/models/main.model';
import { CriteriaChangeService } from '../../services/criteria-change.service'; import { FilterStateService } from '../../services/filter-state.service';
import { GeoService } from '../../services/geo.service'; import { GeoService } from '../../services/geo.service';
import { ListingsService } from '../../services/listings.service'; import { ListingsService } from '../../services/listings.service';
import { SearchService } from '../../services/search.service'; import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { SharedModule } from '../../shared/shared/shared.module'; import { SharedModule } from '../../shared/shared/shared.module';
import { getCriteriaStateObject, resetBusinessListingCriteria } from '../../utils/utils';
import { ValidatedCityComponent } from '../validated-city/validated-city.component'; import { ValidatedCityComponent } from '../validated-city/validated-city.component';
import { ValidatedPriceComponent } from '../validated-price/validated-price.component'; import { ValidatedPriceComponent } from '../validated-price/validated-price.component';
import { ModalService } from './modal.service'; import { ModalService } from './modal.service';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
selector: 'app-search-modal', selector: 'app-search-modal',
@ -23,167 +23,107 @@ import { ModalService } from './modal.service';
templateUrl: './search-modal.component.html', templateUrl: './search-modal.component.html',
styleUrl: './search-modal.component.scss', styleUrl: './search-modal.component.scss',
}) })
export class SearchModalComponent { export class SearchModalComponent implements OnInit, OnDestroy {
@Output() @Input() isModal: boolean = true;
@Input()
isModal: boolean = true; private destroy$ = new Subject<void>();
// cities$: Observable<GeoResult[]>; private searchDebounce$ = new Subject<void>();
// State
criteria: BusinessListingCriteria;
backupCriteria: any;
currentListingType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
// Geo search
counties$: Observable<CountyResult[]>; counties$: Observable<CountyResult[]>;
// cityLoading = false;
countyLoading = false; countyLoading = false;
// cityInput$ = new Subject<string>();
countyInput$ = new Subject<string>(); countyInput$ = new Subject<string>();
private criteriaChangeSubscription: Subscription;
public criteria: BusinessListingCriteria; // Property type for business listings
private debounceTimeout: any; selectedPropertyType: string | null = null;
public backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria = getCriteriaStateObject('businessListings'); propertyTypeOptions = [
numberOfResults$: Observable<number>;
cancelDisable = false;
constructor(
public selectOptions: SelectOptionsService,
public modalService: ModalService,
private geoService: GeoService,
private criteriaChangeService: CriteriaChangeService,
private listingService: ListingsService,
private userService: UserService,
private searchService: SearchService,
) {}
// Define property type options
public propertyTypeOptions = [
{ name: 'Real Estate', value: 'realEstateChecked' }, { name: 'Real Estate', value: 'realEstateChecked' },
{ name: 'Leased Location', value: 'leasedLocation' }, { name: 'Leased Location', value: 'leasedLocation' },
{ name: 'Franchise', value: 'franchiseResale' }, { name: 'Franchise', value: 'franchiseResale' },
]; ];
selectedPropertyType: string | null = null;
selectedPropertyTypeName: string | null = null;
ngOnInit() {
this.setupCriteriaChangeListener();
this.modalService.message$.pipe(untilDestroyed(this)).subscribe(msg => { // Results count
this.criteria = msg as BusinessListingCriteria; numberOfResults$: Observable<number>;
this.backupCriteria = JSON.parse(JSON.stringify(msg));
this.setTotalNumberOfResults(); constructor(
}); public selectOptions: SelectOptionsService,
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => { public modalService: ModalService,
if (val.visible) { private geoService: GeoService,
this.criteria.page = 1; private filterStateService: FilterStateService,
this.criteria.start = 0; private listingService: ListingsService,
} private userService: UserService,
}); private searchService: SearchService,
// this.loadCities(); ) {}
ngOnInit(): void {
// Load counties
this.loadCounties(); this.loadCounties();
this.updateSelectedPropertyType();
this.modalService.sendCriteria(this.criteria);
}
hasActiveFilters(): boolean {
return !!(
this.criteria.state ||
this.criteria.city ||
this.criteria.minPrice ||
this.criteria.maxPrice ||
this.criteria.minRevenue ||
this.criteria.maxRevenue ||
this.criteria.minCashFlow ||
this.criteria.maxCashFlow ||
this.criteria.types.length ||
this.selectedPropertyType ||
this.criteria.minNumberEmployees ||
this.criteria.maxNumberEmployees ||
this.criteria.establishedMin ||
this.criteria.brokerName ||
this.criteria.title
);
}
removeFilter(filterType: string) {
switch (filterType) {
case 'state':
this.criteria.state = null;
this.setCity(null);
break;
case 'city':
this.criteria.city = null;
this.criteria.radius = null;
this.criteria.searchType = 'exact';
break;
case 'price':
this.criteria.minPrice = null;
this.criteria.maxPrice = null;
break;
case 'revenue':
this.criteria.minRevenue = null;
this.criteria.maxRevenue = null;
break;
case 'cashflow':
this.criteria.minCashFlow = null;
this.criteria.maxCashFlow = null;
break;
case 'types':
this.criteria.types = [];
break;
case 'propertyType':
this.criteria.realEstateChecked = false;
this.criteria.leasedLocation = false;
this.criteria.franchiseResale = false;
this.selectedPropertyType = null;
break;
case 'employees':
this.criteria.minNumberEmployees = null;
this.criteria.maxNumberEmployees = null;
break;
case 'established':
this.criteria.establishedMin = null;
break;
case 'brokerName':
this.criteria.brokerName = null;
break;
case 'title':
this.criteria.title = null;
break;
}
this.searchService.search(this.criteria.criteriaType);
}
// Handle category change
onCategoryChange(selectedCategories: string[]) {
this.criteria.types = selectedCategories;
this.searchService.search(this.criteria.criteriaType);
}
// Handle property type change if (this.isModal) {
onPropertyTypeChange(value: string) { // Modal mode: Wait for messages from ModalService
// Reset all property type flags this.modalService.message$.pipe(untilDestroyed(this)).subscribe(criteria => {
(<BusinessListingCriteria>this.criteria).realEstateChecked = false; this.initializeWithCriteria(criteria);
(<BusinessListingCriteria>this.criteria).leasedLocation = false; });
(<BusinessListingCriteria>this.criteria).franchiseResale = false;
// Set the selected property type
if (value) {
this.criteria[value] = true;
}
this.selectedPropertyType = value;
this.searchService.search(this.criteria.criteriaType);
}
// Update selected property type based on current criteria this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
updateSelectedPropertyType() { if (val.visible) {
if ((<BusinessListingCriteria>this.criteria).realEstateChecked) this.selectedPropertyType = 'realEstateChecked'; // Reset pagination when modal opens
else if ((<BusinessListingCriteria>this.criteria).leasedLocation) this.selectedPropertyType = 'leasedLocation'; if (this.criteria) {
else if ((<BusinessListingCriteria>this.criteria).franchiseResale) this.selectedPropertyType = 'franchiseResale'; this.criteria.page = 1;
else this.selectedPropertyType = null; this.criteria.start = 0;
} }
getSelectedPropertyTypeName() { }
return this.selectedPropertyType ? this.propertyTypeOptions.find(opt => opt.value === this.selectedPropertyType)?.name : null; });
}
categoryClicked(checked: boolean, value: string) {
if (checked) {
this.criteria.types.push(value);
} else { } else {
const index = this.criteria.types.findIndex(t => t === value); // Embedded mode: Determine type from route and subscribe to state
if (index > -1) { this.determineListingType();
this.criteria.types.splice(index, 1); this.subscribeToStateChanges();
}
} }
this.searchService.search(this.criteria.criteriaType);
// Setup debounced search
this.searchDebounce$.pipe(debounceTime(400), distinctUntilChanged(), takeUntil(this.destroy$)).subscribe(() => {
this.triggerSearch();
});
} }
private loadCounties() {
private initializeWithCriteria(criteria: any): void {
this.criteria = criteria;
this.currentListingType = criteria.criteriaType;
this.backupCriteria = JSON.parse(JSON.stringify(criteria));
this.updateSelectedPropertyType();
this.setTotalNumberOfResults();
}
private determineListingType(): void {
const url = window.location.pathname;
if (url.includes('businessListings')) {
this.currentListingType = 'businessListings';
} else if (url.includes('commercialPropertyListings')) {
this.currentListingType = 'commercialPropertyListings';
} else if (url.includes('brokerListings')) {
this.currentListingType = 'brokerListings';
}
}
private subscribeToStateChanges(): void {
if (!this.isModal && this.currentListingType) {
this.filterStateService
.getState$(this.currentListingType)
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
this.criteria = { ...state.criteria };
this.updateSelectedPropertyType();
this.setTotalNumberOfResults();
});
}
}
private loadCounties(): void {
this.counties$ = concat( this.counties$ = concat(
of([]), // default items of([]), // default items
this.countyInput$.pipe( this.countyInput$.pipe(
@ -191,103 +131,314 @@ export class SearchModalComponent {
tap(() => (this.countyLoading = true)), tap(() => (this.countyLoading = true)),
switchMap(term => switchMap(term =>
this.geoService.findCountiesStartingWith(term).pipe( this.geoService.findCountiesStartingWith(term).pipe(
catchError(() => of([])), // empty list on error catchError(() => of([])),
map(counties => counties.map(county => county.name)), // transform the list of objects to a list of city names map(counties => counties.map(county => county.name)),
tap(() => (this.countyLoading = false)), tap(() => (this.countyLoading = false)),
), ),
), ),
), ),
); );
} }
onCriteriaChange() {
this.searchService.search(this.criteria.criteriaType); // Filter removal methods
removeFilter(filterType: string): void {
const updates: any = {};
switch (filterType) {
case 'state':
updates.state = null;
updates.city = null;
updates.radius = null;
updates.searchType = 'exact';
break;
case 'city':
updates.city = null;
updates.radius = null;
updates.searchType = 'exact';
break;
case 'price':
updates.minPrice = null;
updates.maxPrice = null;
break;
case 'revenue':
updates.minRevenue = null;
updates.maxRevenue = null;
break;
case 'cashflow':
updates.minCashFlow = null;
updates.maxCashFlow = null;
break;
case 'types':
updates.types = [];
break;
case 'propertyType':
updates.realEstateChecked = false;
updates.leasedLocation = false;
updates.franchiseResale = false;
this.selectedPropertyType = null;
break;
case 'employees':
updates.minNumberEmployees = null;
updates.maxNumberEmployees = null;
break;
case 'established':
updates.establishedMin = null;
break;
case 'brokerName':
updates.brokerName = null;
break;
case 'title':
updates.title = null;
break;
}
this.updateCriteria(updates);
} }
setCity(city) {
// Category handling
onCategoryChange(selectedCategories: string[]): void {
this.updateCriteria({ types: selectedCategories });
}
categoryClicked(checked: boolean, value: string): void {
const types = [...(this.criteria.types || [])];
if (checked) {
if (!types.includes(value)) {
types.push(value);
}
} else {
const index = types.indexOf(value);
if (index > -1) {
types.splice(index, 1);
}
}
this.updateCriteria({ types });
}
// Property type handling (Business listings only)
onPropertyTypeChange(value: string): void {
const updates: any = {
realEstateChecked: false,
leasedLocation: false,
franchiseResale: false,
};
if (value) {
updates[value] = true;
}
this.selectedPropertyType = value;
this.updateCriteria(updates);
}
onCheckboxChange(checkbox: string, value: boolean): void {
const updates: any = {
realEstateChecked: false,
leasedLocation: false,
franchiseResale: false,
};
updates[checkbox] = value;
this.selectedPropertyType = value ? checkbox : null;
this.updateCriteria(updates);
}
// Location handling
setState(state: string): void {
const updates: any = { state };
if (!state) {
updates.city = null;
updates.radius = null;
updates.searchType = 'exact';
}
this.updateCriteria(updates);
}
setCity(city: any): void {
const updates: any = {};
if (city) { if (city) {
this.criteria.city = city; updates.city = city;
this.criteria.state = city.state; updates.state = city.state;
} else { } else {
this.criteria.city = null; updates.city = null;
this.criteria.radius = null; updates.radius = null;
this.criteria.searchType = 'exact'; updates.searchType = 'exact';
} }
this.searchService.search(this.criteria.criteriaType); this.updateCriteria(updates);
} }
setState(state: string) {
if (state) { setRadius(radius: number): void {
this.criteria.state = state; this.updateCriteria({ radius });
} else {
this.criteria.state = null;
this.setCity(null);
}
this.searchService.search(this.criteria.criteriaType);
} }
setRadius(radius: number) {
this.criteria.radius = radius; onCriteriaChange(): void {
this.searchService.search(this.criteria.criteriaType); this.triggerSearch();
} }
private setupCriteriaChangeListener() {
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => { // Debounced search for text inputs
debouncedSearch(): void {
this.searchDebounce$.next();
}
// Clear all filters
clearFilter(): void {
if (this.isModal) {
// In modal: Reset locally
const defaultCriteria = this.getDefaultCriteria();
this.criteria = defaultCriteria;
this.updateSelectedPropertyType();
this.setTotalNumberOfResults(); this.setTotalNumberOfResults();
this.cancelDisable = true; } else {
}); // Embedded: Use state service
this.filterStateService.clearFilters(this.currentListingType);
}
} }
trackByFn(item: GeoResult) {
return item.id; // Modal-specific methods
closeAndSearch(): void {
if (this.isModal) {
// Save changes to state
this.filterStateService.setCriteria(this.currentListingType, this.criteria);
this.modalService.accept();
this.searchService.search(this.currentListingType);
}
} }
search() {
console.log('Search criteria:', this.criteria); close(): void {
if (this.isModal) {
// Discard changes
this.modalService.reject(this.backupCriteria);
}
} }
getCounties() {
this.geoService.findCountiesStartingWith(''); // Helper methods
private updateCriteria(updates: any): void {
if (this.isModal) {
// In modal: Update locally only
this.criteria = { ...this.criteria, ...updates };
this.setTotalNumberOfResults();
} else {
// Embedded: Update through state service
this.filterStateService.updateCriteria(this.currentListingType, updates);
}
// Trigger search after update
this.debouncedSearch();
} }
closeModal() {
console.log('Closing modal'); private triggerSearch(): void {
if (this.isModal) {
// In modal: Only update count
this.setTotalNumberOfResults();
} else {
// Embedded: Full search
this.searchService.search(this.currentListingType);
}
} }
closeAndSearch() {
this.modalService.accept(); private updateSelectedPropertyType(): void {
this.searchService.search(this.criteria.criteriaType); if (this.currentListingType === 'businessListings') {
this.close(); const businessCriteria = this.criteria as BusinessListingCriteria;
} if (businessCriteria.realEstateChecked) {
isTypeOfBusinessClicked(v: KeyValueStyle) { this.selectedPropertyType = 'realEstateChecked';
return this.criteria.types.find(t => t === v.value); } else if (businessCriteria.leasedLocation) {
} this.selectedPropertyType = 'leasedLocation';
isTypeOfProfessionalClicked(v: KeyValue) { } else if (businessCriteria.franchiseResale) {
return this.criteria.types.find(t => t === v.value); this.selectedPropertyType = 'franchiseResale';
}
setTotalNumberOfResults() {
if (this.criteria) {
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') {
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria.criteriaType === 'businessListings' ? 'business' : 'commercialProperty');
} else if (this.criteria.criteriaType === 'brokerListings') {
//this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria);
} else { } else {
this.numberOfResults$ = of(); this.selectedPropertyType = null;
} }
} }
} }
clearFilter() {
resetBusinessListingCriteria(this.criteria);
this.searchService.search(this.criteria.criteriaType); private setTotalNumberOfResults(): void {
} if (!this.criteria) return;
close() {
this.modalService.reject(this.backupCriteria);
}
onCheckboxChange(checkbox: string, value: boolean) {
(<BusinessListingCriteria>this.criteria).realEstateChecked = false;
(<BusinessListingCriteria>this.criteria).leasedLocation = false;
(<BusinessListingCriteria>this.criteria).franchiseResale = false;
// Aktivieren Sie nur die aktuell ausgewählte Checkbox switch (this.currentListingType) {
this.criteria[checkbox] = value; case 'businessListings':
this.searchService.search(this.criteria.criteriaType); this.numberOfResults$ = this.listingService.getNumberOfListings('business');
break;
case 'commercialPropertyListings':
this.numberOfResults$ = this.listingService.getNumberOfListings('commercialProperty');
break;
case 'brokerListings':
this.numberOfResults$ = this.userService.getNumberOfBroker();
break;
}
} }
debouncedSearch() {
clearTimeout(this.debounceTimeout); private getDefaultCriteria(): any {
this.debounceTimeout = setTimeout(() => { switch (this.currentListingType) {
this.searchService.search(this.criteria.criteriaType); case 'businessListings':
}, 1000); return this.filterStateService['createEmptyBusinessListingCriteria']();
case 'commercialPropertyListings':
return this.filterStateService['createEmptyCommercialPropertyListingCriteria']();
case 'brokerListings':
return this.filterStateService['createEmptyUserListingCriteria']();
}
}
hasActiveFilters(): boolean {
if (!this.criteria) return false;
// Check all possible filter properties
const hasBasicFilters = !!(this.criteria.state || this.criteria.city || this.criteria.types?.length);
// Check business-specific filters
if (this.currentListingType === 'businessListings') {
const bc = this.criteria as BusinessListingCriteria;
return (
hasBasicFilters ||
!!(
bc.minPrice ||
bc.maxPrice ||
bc.minRevenue ||
bc.maxRevenue ||
bc.minCashFlow ||
bc.maxCashFlow ||
bc.minNumberEmployees ||
bc.maxNumberEmployees ||
bc.establishedMin ||
bc.brokerName ||
bc.title ||
this.selectedPropertyType
)
);
}
// Check commercial property filters
// if (this.currentListingType === 'commercialPropertyListings') {
// const cc = this.criteria as CommercialPropertyListingCriteria;
// return hasBasicFilters || !!(cc.minPrice || cc.maxPrice || cc.title);
// }
// Check user/broker filters
// if (this.currentListingType === 'brokerListings') {
// const uc = this.criteria as UserListingCriteria;
// return hasBasicFilters || !!(uc.brokerName || uc.companyName || uc.counties?.length);
// }
return hasBasicFilters;
}
getSelectedPropertyTypeName(): string | null {
return this.selectedPropertyType ? this.propertyTypeOptions.find(opt => opt.value === this.selectedPropertyType)?.name || null : null;
}
isTypeOfBusinessClicked(v: KeyValueStyle): boolean {
return !!this.criteria.types?.find(t => t === v.value);
}
isTypeOfProfessionalClicked(v: KeyValue): boolean {
return !!this.criteria.types?.find(t => t === v.value);
}
trackByFn(item: GeoResult): any {
return item.id;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
} }

View File

@ -155,7 +155,7 @@
</div> </div>
} }
<div class="bg-blue-600 hover:bg-blue-500 transition-colors duration-200 max-sm:rounded-md"> <div class="bg-blue-600 hover:bg-blue-500 transition-colors duration-200 max-sm:rounded-md">
@if(getNumberOfFiltersSet()>0 && numberOfResults$){ @if( numberOfResults$){
<button class="w-full h-full text-white font-semibold py-2 px-4 md:py-3 md:px-6 focus:outline-none rounded-md md:rounded-none min-h-[48px]" (click)="search()"> <button class="w-full h-full text-white font-semibold py-2 px-4 md:py-3 md:px-6 focus:outline-none rounded-md md:rounded-none min-h-[48px]" (click)="search()">
Search ({{ numberOfResults$ | async }}) Search ({{ numberOfResults$ | async }})
</button> </button>
@ -165,16 +165,6 @@
</div> </div>
</div> </div>
} }
<!-- <div class="mt-4 flex items-center justify-center text-gray-700">
<span class="mr-2">AI-Search</span>
<span [attr.data-tooltip-target]="tooltipTargetBeta" class="bg-sky-300 text-teal-800 text-xs font-semibold px-2 py-1 rounded">BETA</span>
<app-tooltip [id]="tooltipTargetBeta" text="AI will convert your input into filter criteria. Please check them in the filter menu after search"></app-tooltip>
<span class="ml-2">- Try now</span>
<div class="ml-4 relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input (click)="toggleAiSearch()" type="checkbox" name="toggle" id="toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 border-gray-300 appearance-none cursor-pointer" />
<label for="toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div> -->
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,30 +3,22 @@ import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/co
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy } from '@ngneat/until-destroy';
import { initFlowbite } from 'flowbite'; import { initFlowbite } from 'flowbite';
import { catchError, concat, debounceTime, distinctUntilChanged, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs'; import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { ModalService } from '../../components/search-modal/modal.service'; import { ModalService } from '../../components/search-modal/modal.service';
import { TooltipComponent } from '../../components/tooltip/tooltip.component'; import { TooltipComponent } from '../../components/tooltip/tooltip.component';
import { AiService } from '../../services/ai.service'; import { AiService } from '../../services/ai.service';
import { AuthService } from '../../services/auth.service'; import { AuthService } from '../../services/auth.service';
import { CriteriaChangeService } from '../../services/criteria-change.service'; import { FilterStateService } from '../../services/filter-state.service';
import { GeoService } from '../../services/geo.service'; import { GeoService } from '../../services/geo.service';
import { ListingsService } from '../../services/listings.service'; import { ListingsService } from '../../services/listings.service';
import { SearchService } from '../../services/search.service'; import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { import { map2User } from '../../utils/utils';
compareObjects,
createEmptyBusinessListingCriteria,
createEmptyCommercialPropertyListingCriteria,
createEmptyUserListingCriteria,
createEnhancedProxy,
getCriteriaStateObject,
map2User,
removeSortByStorage,
} from '../../utils/utils';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -50,7 +42,6 @@ export class HomeComponent {
cityLoading = false; cityLoading = false;
cityInput$ = new Subject<string>(); cityInput$ = new Subject<string>();
cityOrState = undefined; cityOrState = undefined;
private criteriaChangeSubscription: Subscription;
numberOfResults$: Observable<number>; numberOfResults$: Observable<number>;
numberOfBroker$: Observable<number>; numberOfBroker$: Observable<number>;
numberOfCommercial$: Observable<number>; numberOfCommercial$: Observable<number>;
@ -59,127 +50,156 @@ export class HomeComponent {
aiSearchFailed = false; aiSearchFailed = false;
loadingAi = false; loadingAi = false;
@ViewChild('aiSearchInput', { static: false }) searchInput!: ElementRef; @ViewChild('aiSearchInput', { static: false }) searchInput!: ElementRef;
typingSpeed: number = 100; // Geschwindigkeit des Tippens (ms) typingSpeed: number = 100;
pauseTime: number = 2000; // Pausezeit, bevor der Text verschwindet (ms) pauseTime: number = 2000;
index: number = 0; index: number = 0;
charIndex: number = 0; charIndex: number = 0;
typingInterval: any; typingInterval: any;
showInput: boolean = true; // Steuerung der Anzeige des Eingabefelds showInput: boolean = true;
tooltipTargetBeta = 'tooltipTargetBeta'; tooltipTargetBeta = 'tooltipTargetBeta';
public constructor(
constructor(
private router: Router, private router: Router,
private modalService: ModalService, private modalService: ModalService,
private searchService: SearchService, private searchService: SearchService,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private criteriaChangeService: CriteriaChangeService,
private geoService: GeoService, private geoService: GeoService,
public cdRef: ChangeDetectorRef, public cdRef: ChangeDetectorRef,
private listingService: ListingsService, private listingService: ListingsService,
private userService: UserService, private userService: UserService,
private aiService: AiService, private aiService: AiService,
private authService: AuthService, private authService: AuthService,
private filterStateService: FilterStateService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
setTimeout(() => { setTimeout(() => {
initFlowbite(); initFlowbite();
}, 0); }, 0);
this.numberOfBroker$ = this.userService.getNumberOfBroker(createEmptyUserListingCriteria());
// Clear all filters and sort options on initial load
this.filterStateService.resetCriteria('businessListings');
this.filterStateService.resetCriteria('commercialPropertyListings');
this.filterStateService.resetCriteria('brokerListings');
this.filterStateService.updateSortBy('businessListings', null);
this.filterStateService.updateSortBy('commercialPropertyListings', null);
this.filterStateService.updateSortBy('brokerListings', null);
// Initialize criteria for the default tab
this.criteria = this.filterStateService.getCriteria('businessListings');
this.numberOfBroker$ = this.userService.getNumberOfBroker(this.filterStateService.getCriteria('brokerListings') as UserListingCriteria);
this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty'); this.numberOfCommercial$ = this.listingService.getNumberOfListings('commercialProperty');
const token = await this.authService.getToken(); const token = await this.authService.getToken();
sessionStorage.removeItem('businessListings');
sessionStorage.removeItem('commercialPropertyListings');
sessionStorage.removeItem('brokerListings');
this.criteria = createEnhancedProxy(getCriteriaStateObject('businessListings'), this);
removeSortByStorage();
this.user = map2User(token); this.user = map2User(token);
this.loadCities(); this.loadCities();
this.setupCriteriaChangeListener(); this.setTotalNumberOfResults();
} }
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
this.activeTabAction = tabname; this.activeTabAction = tabname;
this.cityOrState = null; this.cityOrState = null;
if ('business' === tabname) { const tabToListingType = {
this.criteria = createEnhancedProxy(getCriteriaStateObject('businessListings'), this); business: 'businessListings',
} else if ('commercialProperty' === tabname) { commercialProperty: 'commercialPropertyListings',
this.criteria = createEnhancedProxy(getCriteriaStateObject('commercialPropertyListings'), this); broker: 'brokerListings',
} else if ('broker' === tabname) { };
this.criteria = createEnhancedProxy(getCriteriaStateObject('brokerListings'), this); this.criteria = this.filterStateService.getCriteria(tabToListingType[tabname] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings');
} else { this.setTotalNumberOfResults();
this.criteria = undefined;
}
} }
search() { search() {
this.router.navigate([`${this.activeTabAction}Listings`]); this.router.navigate([`${this.activeTabAction}Listings`]);
} }
private setupCriteriaChangeListener() {
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(untilDestroyed(this), debounceTime(400)).subscribe(() => this.setTotalNumberOfResults());
}
toggleMenu() { toggleMenu() {
this.isMenuOpen = !this.isMenuOpen; this.isMenuOpen = !this.isMenuOpen;
} }
onTypesChange(value) { onTypesChange(value) {
if (value === '') { const tabToListingType = {
// Wenn keine Option ausgewählt ist, setzen Sie types zurück auf ein leeres Array business: 'businessListings',
this.criteria.types = []; commercialProperty: 'commercialPropertyListings',
} else { broker: 'brokerListings',
this.criteria.types = [value]; };
} const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
this.filterStateService.updateCriteria(listingType, { types: value === '' ? [] : [value] });
this.criteria = this.filterStateService.getCriteria(listingType);
this.setTotalNumberOfResults();
} }
onRadiusChange(value) { onRadiusChange(value) {
if (value === 'null') { const tabToListingType = {
// Wenn keine Option ausgewählt ist, setzen Sie types zurück auf ein leeres Array business: 'businessListings',
this.criteria.radius = null; commercialProperty: 'commercialPropertyListings',
} else { broker: 'brokerListings',
this.criteria.radius = parseInt(value); };
} const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
this.filterStateService.updateCriteria(listingType, { radius: value === 'null' ? null : parseInt(value) });
this.criteria = this.filterStateService.getCriteria(listingType);
this.setTotalNumberOfResults();
} }
async openModal() { async openModal() {
const tabToListingType = {
business: 'businessListings',
commercialProperty: 'commercialPropertyListings',
broker: 'brokerListings',
};
const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
const accepted = await this.modalService.showModal(this.criteria); const accepted = await this.modalService.showModal(this.criteria);
if (accepted) { if (accepted) {
this.router.navigate([`${this.activeTabAction}Listings`]); this.router.navigate([`${this.activeTabAction}Listings`]);
} }
} }
private loadCities() { private loadCities() {
this.cities$ = concat( this.cities$ = concat(
of([]), // default items of([]),
this.cityInput$.pipe( this.cityInput$.pipe(
distinctUntilChanged(), distinctUntilChanged(),
tap(() => (this.cityLoading = true)), tap(() => (this.cityLoading = true)),
switchMap(term => switchMap(term =>
//this.geoService.findCitiesStartingWith(term).pipe(
this.geoService.findCitiesAndStatesStartingWith(term).pipe( this.geoService.findCitiesAndStatesStartingWith(term).pipe(
catchError(() => of([])), // empty list on error catchError(() => of([])),
// map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names
tap(() => (this.cityLoading = false)), tap(() => (this.cityLoading = false)),
), ),
), ),
), ),
); );
} }
trackByFn(item: GeoResult) { trackByFn(item: GeoResult) {
return item.id; return item.id;
} }
setCityOrState(cityOrState: CityAndStateResult) { setCityOrState(cityOrState: CityAndStateResult) {
const tabToListingType = {
business: 'businessListings',
commercialProperty: 'commercialPropertyListings',
broker: 'brokerListings',
};
const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
if (cityOrState) { if (cityOrState) {
if (cityOrState.type === 'state') { if (cityOrState.type === 'state') {
this.criteria.state = cityOrState.content.state_code; this.filterStateService.updateCriteria(listingType, { state: cityOrState.content.state_code, city: null, radius: null, searchType: 'exact' });
} else { } else {
this.criteria.city = cityOrState.content as GeoResult; this.filterStateService.updateCriteria(listingType, {
this.criteria.state = cityOrState.content.state; city: cityOrState.content as GeoResult,
this.criteria.searchType = 'radius'; state: cityOrState.content.state,
this.criteria.radius = 20; searchType: 'radius',
radius: 20,
});
} }
} else { } else {
this.criteria.state = null; this.filterStateService.updateCriteria(listingType, { state: null, city: null, radius: null, searchType: 'exact' });
this.criteria.city = null;
this.criteria.radius = null;
this.criteria.searchType = 'exact';
} }
this.criteria = this.filterStateService.getCriteria(listingType);
this.setTotalNumberOfResults();
} }
getTypes() { getTypes() {
if (this.criteria.criteriaType === 'businessListings') { if (this.criteria.criteriaType === 'businessListings') {
return this.selectOptions.typesOfBusiness; return this.selectOptions.typesOfBusiness;
@ -189,6 +209,7 @@ export class HomeComponent {
return this.selectOptions.customerSubTypes; return this.selectOptions.customerSubTypes;
} }
} }
getPlaceholderLabel() { getPlaceholderLabel() {
if (this.criteria.criteriaType === 'businessListings') { if (this.criteria.criteriaType === 'businessListings') {
return 'Business Type'; return 'Business Type';
@ -198,80 +219,28 @@ export class HomeComponent {
return 'Professional Type'; return 'Professional Type';
} }
} }
setTotalNumberOfResults() { setTotalNumberOfResults() {
if (this.criteria) { if (this.criteria) {
console.log(`Getting total number of results for ${this.criteria.criteriaType}`); console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
const tabToListingType = {
business: 'businessListings',
commercialProperty: 'commercialPropertyListings',
broker: 'brokerListings',
};
const listingType = tabToListingType[this.activeTabAction] as 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') { if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') {
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria.criteriaType === 'businessListings' ? 'business' : 'commercialProperty'); this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria.criteriaType === 'businessListings' ? 'business' : 'commercialProperty');
} else if (this.criteria.criteriaType === 'brokerListings') { } else if (this.criteria.criteriaType === 'brokerListings') {
this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria); this.numberOfResults$ = this.userService.getNumberOfBroker(this.filterStateService.getCriteria('brokerListings') as UserListingCriteria);
} else { } else {
this.numberOfResults$ = of(); this.numberOfResults$ = of();
} }
} }
} }
getNumberOfFiltersSet() {
if (this.criteria?.criteriaType === 'brokerListings') {
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else if (this.criteria?.criteriaType === 'businessListings') {
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else if (this.criteria?.criteriaType === 'commercialPropertyListings') {
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else {
return 0;
}
}
toggleAiSearch() {
this.aiSearch = !this.aiSearch;
this.aiSearchFailed = false;
if (!this.aiSearch) {
this.aiSearchText = '';
this.stopTypingEffect();
} else {
setTimeout(() => this.startTypingEffect(), 0);
}
}
ngOnDestroy(): void { ngOnDestroy(): void {
clearTimeout(this.typingInterval); // Stelle sicher, dass das Intervall gestoppt wird, wenn die Komponente zerstört wird
}
startTypingEffect(): void {
if (!this.aiSearchText) {
this.typePlaceholder();
}
}
stopTypingEffect(): void {
clearTimeout(this.typingInterval); clearTimeout(this.typingInterval);
} }
typePlaceholder(): void {
if (!this.searchInput || !this.searchInput.nativeElement) {
return; // Falls das Eingabefeld nicht verfügbar ist (z.B. durch ngIf)
}
if (this.aiSearchText) {
return; // Stoppe, wenn der Benutzer Text eingegeben hat
}
const inputField = this.searchInput.nativeElement as HTMLInputElement;
if (document.activeElement === inputField) {
this.stopTypingEffect();
return;
}
inputField.placeholder = this.placeholders[this.index].substring(0, this.charIndex);
if (this.charIndex < this.placeholders[this.index].length) {
this.charIndex++;
this.typingInterval = setTimeout(() => this.typePlaceholder(), this.typingSpeed);
} else {
// Nach dem vollständigen Tippen eine Pause einlegen
this.typingInterval = setTimeout(() => {
inputField.placeholder = ''; // Schlagartiges Löschen des Platzhalters
this.charIndex = 0;
this.index = (this.index + 1) % this.placeholders.length;
this.typingInterval = setTimeout(() => this.typePlaceholder(), this.typingSpeed);
}, this.pauseTime);
}
}
} }

View File

@ -2,7 +2,7 @@ import { CommonModule, NgOptimizedImage } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy } from '@ngneat/until-destroy';
import { BusinessListing, SortByOptions, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, SortByOptions, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { LISTINGS_PER_PAGE, ListingType, UserListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model'; import { LISTINGS_PER_PAGE, ListingType, UserListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
@ -62,11 +62,11 @@ export class BrokerListingsComponent {
this.criteria = getCriteriaProxy('brokerListings', this) as UserListingCriteria; this.criteria = getCriteriaProxy('brokerListings', this) as UserListingCriteria;
this.init(); this.init();
this.loadSortBy(); this.loadSortBy();
this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(({ criteria }) => { // this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(({ criteria }) => {
if (criteria.criteriaType === 'brokerListings') { // if (criteria.criteriaType === 'brokerListings') {
this.search(); // this.search();
} // }
}); // });
} }
private loadSortBy() { private loadSortBy() {
const storedSortBy = sessionStorage.getItem('professionalsSortBy'); const storedSortBy = sessionStorage.getItem('professionalsSortBy');

View File

@ -1,8 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy } from '@ngneat/until-destroy';
import { Subject, takeUntil } from 'rxjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { BusinessListing, SortByOptions } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, SortByOptions } from '../../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, LISTINGS_PER_PAGE, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model'; import { BusinessListingCriteria, LISTINGS_PER_PAGE, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
@ -10,12 +12,12 @@ import { environment } from '../../../../environments/environment';
import { PaginatorComponent } from '../../../components/paginator/paginator.component'; import { PaginatorComponent } from '../../../components/paginator/paginator.component';
import { ModalService } from '../../../components/search-modal/modal.service'; import { ModalService } from '../../../components/search-modal/modal.service';
import { SearchModalComponent } from '../../../components/search-modal/search-modal.component'; import { SearchModalComponent } from '../../../components/search-modal/search-modal.component';
import { CriteriaChangeService } from '../../../services/criteria-change.service'; import { FilterStateService } from '../../../services/filter-state.service';
import { ImageService } from '../../../services/image.service'; import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
import { SearchService } from '../../../services/search.service'; import { SearchService } from '../../../services/search.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { assignProperties, getCriteriaProxy, resetBusinessListingCriteria } from '../../../utils/utils';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
selector: 'app-business-listings', selector: 'app-business-listings',
@ -24,102 +26,137 @@ import { assignProperties, getCriteriaProxy, resetBusinessListingCriteria } from
templateUrl: './business-listings.component.html', templateUrl: './business-listings.component.html',
styleUrls: ['./business-listings.component.scss', '../../pages.scss'], styleUrls: ['./business-listings.component.scss', '../../pages.scss'],
}) })
export class BusinessListingsComponent { export class BusinessListingsComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
// Component properties
environment = environment; environment = environment;
listings: Array<BusinessListing>;
filteredListings: Array<BusinessListing>;
criteria: BusinessListingCriteria;
realEstateChecked: boolean;
maxPrice: string;
minPrice: string;
type: string;
state: string;
totalRecords: number = 0;
ts = new Date().getTime();
first: number = 0;
rows: number = 12;
env = environment; env = environment;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined; listings: Array<BusinessListing> = [];
filteredListings: Array<ListingType> = [];
criteria: BusinessListingCriteria;
sortBy: SortByOptions | null = null;
// Pagination
totalRecords = 0;
page = 1; page = 1;
pageCount = 1; pageCount = 1;
first = 0;
rows = LISTINGS_PER_PAGE;
// UI state
ts = new Date().getTime();
emailToDirName = emailToDirName; emailToDirName = emailToDirName;
sortBy: SortByOptions = null; // Neu: Separate Property
constructor( constructor(
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private listingsService: ListingsService, private listingsService: ListingsService,
private activatedRoute: ActivatedRoute,
private router: Router, private router: Router,
private cdRef: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private imageService: ImageService, private imageService: ImageService,
private route: ActivatedRoute,
private searchService: SearchService, private searchService: SearchService,
private modalService: ModalService, private modalService: ModalService,
private criteriaChangeService: CriteriaChangeService, private filterStateService: FilterStateService,
) { private route: ActivatedRoute,
this.criteria = getCriteriaProxy('businessListings', this) as BusinessListingCriteria; ) {}
this.modalService.sendCriteria(this.criteria);
this.init(); ngOnInit(): void {
this.loadSortBy(); // Subscribe to state changes
this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(({ criteria }) => { this.filterStateService
if (criteria.criteriaType === 'businessListings') { .getState$('businessListings')
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
this.criteria = state.criteria;
this.sortBy = state.sortBy;
// Automatically search when state changes
this.search();
});
// Subscribe to search triggers (if triggered from other components)
this.searchService.searchTrigger$.pipe(takeUntil(this.destroy$)).subscribe(type => {
if (type === 'businessListings') {
this.search(); this.search();
} }
}); });
} }
private loadSortBy() {
const storedSortBy = sessionStorage.getItem('businessSortBy'); async search(): Promise<void> {
this.sortBy = storedSortBy && storedSortBy !== 'null' ? (storedSortBy as SortByOptions) : null; try {
} // Get current criteria from service
async ngOnInit() { this.criteria = this.filterStateService.getCriteria('businessListings') as BusinessListingCriteria;
this.search();
} // Add sortBy if available
async init() { const searchCriteria = {
this.reset(); ...this.criteria,
sortBy: this.sortBy,
};
// Perform search
const listingsResponse = await this.listingsService.getListings('business');
this.listings = listingsResponse.results;
this.totalRecords = listingsResponse.totalCount;
this.pageCount = Math.ceil(this.totalRecords / LISTINGS_PER_PAGE);
this.page = this.criteria.page || 1;
// Update view
this.cdRef.markForCheck();
this.cdRef.detectChanges();
} catch (error) {
console.error('Search error:', error);
// Handle error appropriately
this.listings = [];
this.totalRecords = 0;
this.cdRef.markForCheck();
}
} }
async search() { onPageChange(page: number): void {
const listingReponse = await this.listingsService.getListings('business'); // Update only pagination properties
this.listings = listingReponse.results; this.filterStateService.updateCriteria('businessListings', {
this.totalRecords = listingReponse.totalCount; page: page,
this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1; start: (page - 1) * LISTINGS_PER_PAGE,
this.page = this.criteria.page ? this.criteria.page : 1; length: LISTINGS_PER_PAGE,
this.cdRef.markForCheck(); });
this.cdRef.detectChanges(); // Search will be triggered automatically through state subscription
} }
onPageChange(page: any) {
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE; clearAllFilters(): void {
this.criteria.length = LISTINGS_PER_PAGE; // Reset criteria but keep sortBy
this.criteria.page = page; this.filterStateService.clearFilters('businessListings');
this.search(); // Search will be triggered automatically through state subscription
} }
imageErrorHandler(listing: ListingType) {}
reset() { async openFilterModal(): Promise<void> {
this.criteria.title = null; // Open modal with current criteria
const currentCriteria = this.filterStateService.getCriteria('businessListings');
const modalResult = await this.modalService.showModal(currentCriteria);
if (modalResult.accepted) {
// Modal accepted changes - state is updated by modal
// Search will be triggered automatically through state subscription
} else {
// Modal was cancelled - no action needed
}
}
getListingPrice(listing: BusinessListing): string {
if (!listing.price) return 'Price on Request';
return `$${listing.price.toLocaleString()}`;
}
getListingLocation(listing: BusinessListing): string {
if (!listing.location) return 'Location not specified';
return `${listing.location.name}, ${listing.location.state}`;
}
navigateToDetails(listingId: string): void {
this.router.navigate(['/details-business', listingId]);
} }
getDaysListed(listing: BusinessListing) { getDaysListed(listing: BusinessListing) {
return dayjs().diff(listing.created, 'day'); return dayjs().diff(listing.created, 'day');
} }
// New methods for filter actions ngOnDestroy(): void {
clearAllFilters() { this.destroy$.next();
// Reset criteria to default values this.destroy$.complete();
resetBusinessListingCriteria(this.criteria);
// Reset pagination
this.criteria.page = 1;
this.criteria.start = 0;
this.criteriaChangeService.notifyCriteriaChange();
// Search with cleared filters
this.searchService.search('businessListings');
}
async openFilterModal() {
// Open the search modal with current criteria
const modalResult = await this.modalService.showModal(this.criteria);
if (modalResult.accepted) {
this.criteria = assignProperties(this.criteria, modalResult.criteria); // Update criteria with modal result
this.searchService.search('businessListings'); // Trigger search with updated criteria
}
} }
} }

View File

@ -1,21 +1,21 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy } from '@ngneat/until-destroy';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Subject, takeUntil } from 'rxjs';
import { CommercialPropertyListing, SortByOptions } from '../../../../../../bizmatch-server/src/models/db.model'; import { CommercialPropertyListing, SortByOptions } from '../../../../../../bizmatch-server/src/models/db.model';
import { CommercialPropertyListingCriteria, LISTINGS_PER_PAGE, ResponseCommercialPropertyListingArray } from '../../../../../../bizmatch-server/src/models/main.model'; import { CommercialPropertyListingCriteria, LISTINGS_PER_PAGE, ResponseCommercialPropertyListingArray } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { PaginatorComponent } from '../../../components/paginator/paginator.component'; import { PaginatorComponent } from '../../../components/paginator/paginator.component';
import { ModalService } from '../../../components/search-modal/modal.service'; import { ModalService } from '../../../components/search-modal/modal.service';
import { SearchModalCommercialComponent } from '../../../components/search-modal/search-modal-commercial.component'; import { SearchModalCommercialComponent } from '../../../components/search-modal/search-modal-commercial.component';
import { CriteriaChangeService } from '../../../services/criteria-change.service'; import { FilterStateService } from '../../../services/filter-state.service';
import { ImageService } from '../../../services/image.service'; import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
import { SearchService } from '../../../services/search.service'; import { SearchService } from '../../../services/search.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { assignProperties, getCriteriaProxy, resetCommercialPropertyListingCriteria } from '../../../utils/utils';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
@ -25,103 +25,141 @@ import { assignProperties, getCriteriaProxy, resetCommercialPropertyListingCrite
templateUrl: './commercial-property-listings.component.html', templateUrl: './commercial-property-listings.component.html',
styleUrls: ['./commercial-property-listings.component.scss', '../../pages.scss'], styleUrls: ['./commercial-property-listings.component.scss', '../../pages.scss'],
}) })
export class CommercialPropertyListingsComponent { export class CommercialPropertyListingsComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
// Component properties
environment = environment; environment = environment;
listings: Array<CommercialPropertyListing>;
filteredListings: Array<CommercialPropertyListing>;
criteria: CommercialPropertyListingCriteria;
realEstateChecked: boolean;
first: number = 0;
rows: number = 12;
maxPrice: string;
minPrice: string;
type: string;
statesSet = new Set();
state: string;
totalRecords: number = 0;
env = environment; env = environment;
listings: Array<CommercialPropertyListing> = [];
filteredListings: Array<CommercialPropertyListing> = [];
criteria: CommercialPropertyListingCriteria;
sortBy: SortByOptions | null = null;
// Pagination
totalRecords = 0;
page = 1; page = 1;
pageCount = 1; pageCount = 1;
first = 0;
rows = LISTINGS_PER_PAGE;
// UI state
ts = new Date().getTime(); ts = new Date().getTime();
sortBy: SortByOptions = null; // Neu: Separate Property
constructor( constructor(
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private listingsService: ListingsService, private listingsService: ListingsService,
private activatedRoute: ActivatedRoute,
private router: Router, private router: Router,
private cdRef: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private imageService: ImageService, private imageService: ImageService,
private route: ActivatedRoute,
private searchService: SearchService, private searchService: SearchService,
private modalService: ModalService, private modalService: ModalService,
private criteriaChangeService: CriteriaChangeService, private filterStateService: FilterStateService,
) { private route: ActivatedRoute,
this.criteria = getCriteriaProxy('commercialPropertyListings', this) as CommercialPropertyListingCriteria; ) {}
this.modalService.sendCriteria(this.criteria);
this.loadSortBy(); ngOnInit(): void {
this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(({ criteria }) => { // Subscribe to state changes
if (criteria.criteriaType === 'commercialPropertyListings') { this.filterStateService
.getState$('commercialPropertyListings')
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
this.criteria = state.criteria;
this.sortBy = state.sortBy;
// Automatically search when state changes
this.search();
});
// Subscribe to search triggers (if triggered from other components)
this.searchService.searchTrigger$.pipe(takeUntil(this.destroy$)).subscribe(type => {
if (type === 'commercialPropertyListings') {
this.search(); this.search();
} }
}); });
} }
private loadSortBy() {
const storedSortBy = sessionStorage.getItem('commercialSortBy'); async search(): Promise<void> {
this.sortBy = storedSortBy && storedSortBy !== 'null' ? (storedSortBy as SortByOptions) : null; try {
} // Perform search
async ngOnInit() { const listingResponse = await this.listingsService.getListings('commercialProperty');
this.search(); this.listings = (listingResponse as ResponseCommercialPropertyListingArray).results;
this.totalRecords = (listingResponse as ResponseCommercialPropertyListingArray).totalCount;
this.pageCount = Math.ceil(this.totalRecords / LISTINGS_PER_PAGE);
this.page = this.criteria.page || 1;
// Update view
this.cdRef.markForCheck();
this.cdRef.detectChanges();
} catch (error) {
console.error('Search error:', error);
// Handle error appropriately
this.listings = [];
this.totalRecords = 0;
this.cdRef.markForCheck();
}
} }
async search() { onPageChange(page: number): void {
const listingReponse = await this.listingsService.getListings('commercialProperty'); // Update only pagination properties
this.listings = (<ResponseCommercialPropertyListingArray>listingReponse).results; this.filterStateService.updateCriteria('commercialPropertyListings', {
this.totalRecords = (<ResponseCommercialPropertyListingArray>listingReponse).totalCount; page: page,
this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1; start: (page - 1) * LISTINGS_PER_PAGE,
this.page = this.criteria.page ? this.criteria.page : 1; length: LISTINGS_PER_PAGE,
this.cdRef.markForCheck(); });
this.cdRef.detectChanges(); // Search will be triggered automatically through state subscription
} }
onPageChange(page: any) { clearAllFilters(): void {
this.criteria.start = (page - 1) * LISTINGS_PER_PAGE; // Reset criteria but keep sortBy
this.criteria.length = LISTINGS_PER_PAGE; this.filterStateService.clearFilters('commercialPropertyListings');
this.criteria.page = page; // Search will be triggered automatically through state subscription
this.search();
} }
reset() { async openFilterModal(): Promise<void> {
this.criteria.title = null; // Open modal with current criteria
const currentCriteria = this.filterStateService.getCriteria('commercialPropertyListings');
const modalResult = await this.modalService.showModal(currentCriteria);
if (modalResult.accepted) {
// Modal accepted changes - state is updated by modal
// Search will be triggered automatically through state subscription
} else {
// Modal was cancelled - no action needed
}
} }
getTS() { // Helper methods for template
getTS(): number {
return new Date().getTime(); return new Date().getTime();
} }
getDaysListed(listing: CommercialPropertyListing) { getDaysListed(listing: CommercialPropertyListing): number {
return dayjs().diff(listing.created, 'day'); return dayjs().diff(listing.created, 'day');
} }
// New methods for filter actions getListingImage(listing: CommercialPropertyListing): string {
clearAllFilters() { if (listing.imageOrder?.length > 0) {
// Reset criteria to default values return `${this.env.imageBaseUrl}/pictures/property/${listing.imagePath}/${listing.serialId}/${listing.imageOrder[0]}`;
resetCommercialPropertyListingCriteria(this.criteria); }
return 'assets/images/placeholder_properties.jpg';
// Reset pagination
this.criteria.page = 1;
this.criteria.start = 0;
this.criteriaChangeService.notifyCriteriaChange();
// Search with cleared filters
this.searchService.search('commercialPropertyListings');
} }
async openFilterModal() { getListingPrice(listing: CommercialPropertyListing): string {
const modalResult = await this.modalService.showModal(this.criteria); if (!listing.price) return 'Price on Request';
if (modalResult.accepted) { return `$${listing.price.toLocaleString()}`;
this.criteria = assignProperties(this.criteria, modalResult.criteria); // Update criteria with modal result }
this.searchService.search('commercialPropertyListings'); // Trigger search with updated criteria
} getListingLocation(listing: CommercialPropertyListing): string {
if (!listing.location) return 'Location not specified';
return listing.location.name || listing.location.county || 'Location not specified';
}
navigateToDetails(listingId: string): void {
this.router.navigate(['/details-commercial-property-listing', listingId]);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
} }
} }

View File

@ -0,0 +1,245 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { SortByOptions } from '../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
type CriteriaType = BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
type ListingType = 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
interface FilterState {
businessListings: {
criteria: BusinessListingCriteria;
sortBy: SortByOptions | null;
};
commercialPropertyListings: {
criteria: CommercialPropertyListingCriteria;
sortBy: SortByOptions | null;
};
brokerListings: {
criteria: UserListingCriteria;
sortBy: SortByOptions | null;
};
}
@Injectable({
providedIn: 'root',
})
export class FilterStateService {
private state: FilterState;
private stateSubjects: Map<ListingType, BehaviorSubject<any>> = new Map();
constructor() {
// Initialize state from sessionStorage or with defaults
this.state = this.loadStateFromStorage();
// Create BehaviorSubjects for each listing type
this.stateSubjects.set('businessListings', new BehaviorSubject(this.state.businessListings));
this.stateSubjects.set('commercialPropertyListings', new BehaviorSubject(this.state.commercialPropertyListings));
this.stateSubjects.set('brokerListings', new BehaviorSubject(this.state.brokerListings));
}
// Get observable for specific listing type
getState$(type: ListingType): Observable<any> {
return this.stateSubjects.get(type)!.asObservable();
}
// Get current criteria
getCriteria(type: ListingType): CriteriaType {
return { ...this.state[type].criteria };
}
// Update criteria
updateCriteria(type: ListingType, criteria: Partial<CriteriaType>): void {
// Type-safe update basierend auf dem Listing-Typ
if (type === 'businessListings') {
this.state.businessListings.criteria = {
...this.state.businessListings.criteria,
...criteria,
} as BusinessListingCriteria;
} else if (type === 'commercialPropertyListings') {
this.state.commercialPropertyListings.criteria = {
...this.state.commercialPropertyListings.criteria,
...criteria,
} as CommercialPropertyListingCriteria;
} else if (type === 'brokerListings') {
this.state.brokerListings.criteria = {
...this.state.brokerListings.criteria,
...criteria,
} as UserListingCriteria;
}
this.saveToStorage(type);
this.emitState(type);
}
// Set complete criteria (for reset operations)
setCriteria(type: ListingType, criteria: CriteriaType): void {
if (type === 'businessListings') {
this.state.businessListings.criteria = criteria as BusinessListingCriteria;
} else if (type === 'commercialPropertyListings') {
this.state.commercialPropertyListings.criteria = criteria as CommercialPropertyListingCriteria;
} else if (type === 'brokerListings') {
this.state.brokerListings.criteria = criteria as UserListingCriteria;
}
this.saveToStorage(type);
this.emitState(type);
}
// Get current sortBy
getSortBy(type: ListingType): SortByOptions | null {
return this.state[type].sortBy;
}
// Update sortBy
updateSortBy(type: ListingType, sortBy: SortByOptions | null): void {
this.state[type].sortBy = sortBy;
this.saveSortByToStorage(type, sortBy);
this.emitState(type);
}
// Reset criteria to defaults
resetCriteria(type: ListingType): void {
if (type === 'businessListings') {
this.state.businessListings.criteria = this.createEmptyBusinessListingCriteria();
} else if (type === 'commercialPropertyListings') {
this.state.commercialPropertyListings.criteria = this.createEmptyCommercialPropertyListingCriteria();
} else if (type === 'brokerListings') {
this.state.brokerListings.criteria = this.createEmptyUserListingCriteria();
}
this.saveToStorage(type);
this.emitState(type);
}
// Clear all filters but keep sortBy
clearFilters(type: ListingType): void {
const sortBy = this.state[type].sortBy;
this.resetCriteria(type);
this.state[type].sortBy = sortBy;
this.emitState(type);
}
private emitState(type: ListingType): void {
this.stateSubjects.get(type)?.next({ ...this.state[type] });
}
private saveToStorage(type: ListingType): void {
sessionStorage.setItem(type, JSON.stringify(this.state[type].criteria));
}
private saveSortByToStorage(type: ListingType, sortBy: SortByOptions | null): void {
const sortByKey = type === 'businessListings' ? 'businessSortBy' : type === 'commercialPropertyListings' ? 'commercialSortBy' : 'professionalsSortBy';
if (sortBy) {
sessionStorage.setItem(sortByKey, sortBy);
} else {
sessionStorage.removeItem(sortByKey);
}
}
private loadStateFromStorage(): FilterState {
return {
businessListings: {
criteria: this.loadCriteriaFromStorage('businessListings') as BusinessListingCriteria,
sortBy: this.loadSortByFromStorage('businessSortBy'),
},
commercialPropertyListings: {
criteria: this.loadCriteriaFromStorage('commercialPropertyListings') as CommercialPropertyListingCriteria,
sortBy: this.loadSortByFromStorage('commercialSortBy'),
},
brokerListings: {
criteria: this.loadCriteriaFromStorage('brokerListings') as UserListingCriteria,
sortBy: this.loadSortByFromStorage('professionalsSortBy'),
},
};
}
private loadCriteriaFromStorage(key: ListingType): CriteriaType {
const stored = sessionStorage.getItem(key);
if (stored) {
return JSON.parse(stored);
}
switch (key) {
case 'businessListings':
return this.createEmptyBusinessListingCriteria();
case 'commercialPropertyListings':
return this.createEmptyCommercialPropertyListingCriteria();
case 'brokerListings':
return this.createEmptyUserListingCriteria();
}
}
private loadSortByFromStorage(key: string): SortByOptions | null {
const stored = sessionStorage.getItem(key);
return stored && stored !== 'null' ? (stored as SortByOptions) : null;
}
// Helper methods to create empty criteria
private createEmptyBusinessListingCriteria(): BusinessListingCriteria {
return {
criteriaType: 'businessListings',
types: [],
state: null,
city: null,
radius: null,
searchType: 'exact' as const,
minPrice: null,
maxPrice: null,
minRevenue: null,
maxRevenue: null,
minCashFlow: null,
maxCashFlow: null,
minNumberEmployees: null,
maxNumberEmployees: null,
establishedMin: null,
brokerName: null,
title: null,
realEstateChecked: false,
leasedLocation: false,
franchiseResale: false,
email: null,
prompt: null,
page: 1,
start: 0,
length: 12,
};
}
private createEmptyCommercialPropertyListingCriteria(): CommercialPropertyListingCriteria {
return {
criteriaType: 'commercialPropertyListings',
types: [],
state: null,
city: null,
radius: null,
searchType: 'exact' as const,
minPrice: null,
maxPrice: null,
title: null,
prompt: null,
page: 1,
start: 0,
length: 12,
};
}
private createEmptyUserListingCriteria(): UserListingCriteria {
return {
criteriaType: 'brokerListings',
types: [],
state: null,
city: null,
radius: null,
searchType: 'exact' as const,
brokerName: null,
companyName: null,
counties: [],
prompt: null,
page: 1,
start: 0,
length: 12,
};
}
}

View File

@ -1,30 +1,21 @@
// Vereinfachter search.service.ts
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { SortByOptions } from '../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
import { getCriteriaProxy } from '../utils/utils';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class SearchService { export class SearchService {
private criteriaSource = new Subject<{ private searchTriggerSubject = new Subject<string>();
criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
sortBy?: SortByOptions; // Observable für Komponenten zum Abonnieren
}>(); searchTrigger$ = this.searchTriggerSubject.asObservable();
currentCriteria = this.criteriaSource.asObservable();
constructor() {} constructor() {}
search(criteriaType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings'): void { // Trigger eine Suche für einen bestimmten Listing-Typ
const criteria = getCriteriaProxy(criteriaType, this); search(listingType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings'): void {
const storedSortBy = console.log(`Triggering search for: ${listingType}`);
criteriaType === 'businessListings' this.searchTriggerSubject.next(listingType);
? sessionStorage.getItem('businessSortBy')
: criteriaType === 'commercialPropertyListings'
? sessionStorage.getItem('commercialSortBy')
: sessionStorage.getItem('professionalsSortBy');
const sortBy = storedSortBy && storedSortBy !== 'null' ? (storedSortBy as SortByOptions) : null;
this.criteriaSource.next({ criteria, sortBy });
} }
} }