import { CommonModule } from '@angular/common'; import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; 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 { CommercialPropertyListingCriteria, CountyResult, GeoResult } 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 { 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-commercial', standalone: true, imports: [CommonModule, FormsModule, NgSelectModule, ValidatedCityComponent, ValidatedPriceComponent], templateUrl: './search-modal-commercial.component.html', styleUrls: ['./search-modal.component.scss'], }) export class SearchModalCommercialComponent implements OnInit, OnDestroy { @Input() isModal: boolean = true; private destroy$ = new Subject(); private searchDebounce$ = new Subject(); // State criteria: CommercialPropertyListingCriteria; backupCriteria: any; // Geo search counties$: Observable; countyLoading = false; countyInput$ = new Subject(); // Results count numberOfResults$: Observable; cancelDisable = false; constructor( public selectOptions: SelectOptionsService, public modalService: ModalService, private geoService: GeoService, private filterStateService: FilterStateService, private listingService: ListingsService, 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 => { if (criteria?.criteriaType === 'commercialPropertyListings') { this.initializeWithCriteria(criteria); } }); this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => { if (val.visible && val.type === 'commercialPropertyListings') { // Reset pagination when modal opens if (this.criteria) { this.criteria.page = 1; this.criteria.start = 0; } } }); } else { // Embedded mode: Subscribe to state changes this.subscribeToStateChanges(); } // Setup debounced search this.searchDebounce$.pipe(debounceTime(400), takeUntil(this.destroy$)).subscribe(() => { this.triggerSearch(); }); } private initializeWithCriteria(criteria: CommercialPropertyListingCriteria): void { this.criteria = criteria; this.backupCriteria = JSON.parse(JSON.stringify(criteria)); this.setTotalNumberOfResults(); } 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( 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 'types': updates.types = []; break; case 'title': updates.title = null; break; case 'brokerName': updates.brokerName = 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 }); } // 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.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 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('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', this.criteria); } 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, brokerName: 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 || this.criteria.brokerName ); } trackByFn(item: GeoResult): any { return item.id; } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } }