import { AsyncPipe, NgIf } from '@angular/common'; import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { NgSelectModule } from '@ng-select/ng-select'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { catchError, concat, debounceTime, distinctUntilChanged, map, Observable, of, Subject, switchMap, takeUntil, tap } from 'rxjs'; import { BusinessListingCriteria, CountyResult, GeoResult, KeyValue, KeyValueStyle } from '../../../../../bizmatch-server/src/models/main.model'; import { FilterStateService } from '../../services/filter-state.service'; import { GeoService } from '../../services/geo.service'; import { ListingsService } from '../../services/listings.service'; import { SearchService } from '../../services/search.service'; import { SelectOptionsService } from '../../services/select-options.service'; import { UserService } from '../../services/user.service'; import { SharedModule } from '../../shared/shared/shared.module'; import { ValidatedCityComponent } from '../validated-city/validated-city.component'; import { ValidatedPriceComponent } from '../validated-price/validated-price.component'; import { ModalService } from './modal.service'; @UntilDestroy() @Component({ selector: 'app-search-modal', standalone: true, imports: [SharedModule, AsyncPipe, NgIf, NgSelectModule, ValidatedCityComponent, ValidatedPriceComponent], templateUrl: './search-modal.component.html', styleUrl: './search-modal.component.scss', }) export class SearchModalComponent implements OnInit, OnDestroy { @Input() isModal: boolean = true; private destroy$ = new Subject(); private searchDebounce$ = new Subject(); // State criteria: BusinessListingCriteria; backupCriteria: any; currentListingType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings'; // Geo search counties$: Observable; countyLoading = false; countyInput$ = new Subject(); // Property type for business listings selectedPropertyType: string | null = null; propertyTypeOptions = [ { name: 'Real Estate', value: 'realEstateChecked' }, { name: 'Leased Location', value: 'leasedLocation' }, { name: 'Franchise', value: 'franchiseResale' }, ]; // Results count numberOfResults$: Observable; constructor( public selectOptions: SelectOptionsService, public modalService: ModalService, private geoService: GeoService, private filterStateService: FilterStateService, private listingService: ListingsService, private userService: UserService, private searchService: SearchService, ) {} ngOnInit(): void { // Load counties this.loadCounties(); if (this.isModal) { // Modal mode: Wait for messages from ModalService this.modalService.message$.pipe(untilDestroyed(this)).subscribe(criteria => { this.initializeWithCriteria(criteria); }); this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => { if (val.visible) { // Reset pagination when modal opens if (this.criteria) { this.criteria.page = 1; this.criteria.start = 0; } } }); } else { // Embedded mode: Determine type from route and subscribe to state this.determineListingType(); this.subscribeToStateChanges(); } // Setup debounced search this.searchDebounce$.pipe(debounceTime(400), takeUntil(this.destroy$)).subscribe(() => this.triggerSearch()); } 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( of([]), // default items this.countyInput$.pipe( distinctUntilChanged(), tap(() => (this.countyLoading = true)), switchMap(term => this.geoService.findCountiesStartingWith(term).pipe( catchError(() => of([])), map(counties => counties.map(county => county.name)), tap(() => (this.countyLoading = false)), ), ), ), ); } // 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); } // 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) { updates.city = city; updates.state = city.state; // Automatically set radius to 50 miles and enable radius search updates.searchType = 'radius'; updates.radius = 50; } 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.updateSelectedPropertyType(); this.setTotalNumberOfResults(); } else { // Embedded: Use state service this.filterStateService.clearFilters(this.currentListingType); } } // 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); } } close(): void { if (this.isModal) { // Discard changes this.modalService.reject(this.backupCriteria); } } // Helper methods public 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(); } private triggerSearch(): void { if (this.isModal) { // In modal: Only update count this.setTotalNumberOfResults(); } else { // Embedded: Full search this.searchService.search(this.currentListingType); } } private updateSelectedPropertyType(): void { if (this.currentListingType === 'businessListings') { const businessCriteria = this.criteria as BusinessListingCriteria; if (businessCriteria.realEstateChecked) { this.selectedPropertyType = 'realEstateChecked'; } else if (businessCriteria.leasedLocation) { this.selectedPropertyType = 'leasedLocation'; } else if (businessCriteria.franchiseResale) { this.selectedPropertyType = 'franchiseResale'; } else { this.selectedPropertyType = null; } } } private setTotalNumberOfResults(): void { if (!this.criteria) return; switch (this.currentListingType) { case 'businessListings': this.numberOfResults$ = this.listingService.getNumberOfListings('business', this.criteria); break; case 'commercialPropertyListings': this.numberOfResults$ = this.listingService.getNumberOfListings('commercialProperty', this.criteria); break; case 'brokerListings': this.numberOfResults$ = this.userService.getNumberOfBroker(); break; } } private getDefaultCriteria(): any { switch (this.currentListingType) { case 'businessListings': 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(); } }