317 lines
8.9 KiB
TypeScript
317 lines
8.9 KiB
TypeScript
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<void>();
|
|
private searchDebounce$ = new Subject<void>();
|
|
|
|
// State
|
|
criteria: CommercialPropertyListingCriteria;
|
|
backupCriteria: any;
|
|
|
|
// Geo search
|
|
counties$: Observable<CountyResult[]>;
|
|
countyLoading = false;
|
|
countyInput$ = new Subject<string>();
|
|
|
|
// Results count
|
|
numberOfResults$: Observable<number>;
|
|
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();
|
|
}
|
|
}
|