Files
bizmatch-project/bizmatch/src/app/components/search-modal/search-modal.component.ts

446 lines
13 KiB
TypeScript

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<void>();
private searchDebounce$ = new Subject<void>();
// State
criteria: BusinessListingCriteria;
backupCriteria: any;
currentListingType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
// Geo search
counties$: Observable<CountyResult[]>;
countyLoading = false;
countyInput$ = new Subject<string>();
// 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<number>;
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();
}
}