From c62af8746f765af5bf187e943b690f26b3b1f9d8 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Fri, 25 Jul 2025 17:51:43 -0500 Subject: [PATCH] Filter for commercial properties --- bizmatch/src/app/app.component.html | 1 + bizmatch/src/app/app.component.ts | 3 +- .../components/search-modal/modal.service.ts | 11 +- .../search-modal-commercial.component.html | 220 ++++++++++++++++++ .../search-modal-commercial.component.ts | 208 +++++++++++++++++ .../search-modal/search-modal.component.html | 11 +- .../search-modal/search-modal.component.ts | 2 +- ...ommercial-property-listings.component.html | 189 ++++++++------- .../commercial-property-listings.component.ts | 20 +- 9 files changed, 557 insertions(+), 108 deletions(-) create mode 100644 bizmatch/src/app/components/search-modal/search-modal-commercial.component.html create mode 100644 bizmatch/src/app/components/search-modal/search-modal-commercial.component.ts diff --git a/bizmatch/src/app/app.component.html b/bizmatch/src/app/app.component.html index 868b99b..a0a401e 100644 --- a/bizmatch/src/app/app.component.html +++ b/bizmatch/src/app/app.component.html @@ -41,5 +41,6 @@ + diff --git a/bizmatch/src/app/app.component.ts b/bizmatch/src/app/app.component.ts index d56b340..47c3b42 100644 --- a/bizmatch/src/app/app.component.ts +++ b/bizmatch/src/app/app.component.ts @@ -10,6 +10,7 @@ import { EMailComponent } from './components/email/email.component'; import { FooterComponent } from './components/footer/footer.component'; import { HeaderComponent } from './components/header/header.component'; import { MessageContainerComponent } from './components/message/message-container.component'; +import { SearchModalCommercialComponent } from './components/search-modal/search-modal-commercial.component'; import { SearchModalComponent } from './components/search-modal/search-modal.component'; import { AuditService } from './services/audit.service'; import { GeoService } from './services/geo.service'; @@ -19,7 +20,7 @@ import { UserService } from './services/user.service'; @Component({ selector: 'app-root', standalone: true, - imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent, EMailComponent], + imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, SearchModalCommercialComponent, ConfirmationComponent, EMailComponent], providers: [], templateUrl: './app.component.html', styleUrl: './app.component.scss', diff --git a/bizmatch/src/app/components/search-modal/modal.service.ts b/bizmatch/src/app/components/search-modal/modal.service.ts index e72dd40..1a6394d 100644 --- a/bizmatch/src/app/components/search-modal/modal.service.ts +++ b/bizmatch/src/app/components/search-modal/modal.service.ts @@ -1,4 +1,3 @@ -// 1. Shared Service (modal.service.ts) import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { BusinessListingCriteria, CommercialPropertyListingCriteria, ModalResult, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; @@ -7,16 +6,16 @@ import { BusinessListingCriteria, CommercialPropertyListingCriteria, ModalResult providedIn: 'root', }) export class ModalService { - private modalVisibleSubject = new BehaviorSubject(false); + private modalVisibleSubject = new BehaviorSubject<{ visible: boolean; type?: string }>({ visible: false }); private messageSubject = new BehaviorSubject(null); private resolvePromise!: (value: ModalResult) => void; - modalVisible$: Observable = this.modalVisibleSubject.asObservable(); + modalVisible$: Observable<{ visible: boolean; type?: string }> = this.modalVisibleSubject.asObservable(); message$: Observable = this.messageSubject.asObservable(); showModal(message: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): Promise { this.messageSubject.next(message); - this.modalVisibleSubject.next(true); + this.modalVisibleSubject.next({ visible: true, type: message.criteriaType }); return new Promise(resolve => { this.resolvePromise = resolve; }); @@ -28,12 +27,12 @@ export class ModalService { }); } accept(): void { - this.modalVisibleSubject.next(false); + this.modalVisibleSubject.next({ visible: false }); this.resolvePromise({ accepted: true }); } reject(backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): void { - this.modalVisibleSubject.next(false); + this.modalVisibleSubject.next({ visible: false }); this.resolvePromise({ accepted: false, criteria: backupCriteria }); } } diff --git a/bizmatch/src/app/components/search-modal/search-modal-commercial.component.html b/bizmatch/src/app/components/search-modal/search-modal-commercial.component.html new file mode 100644 index 0000000..bd54207 --- /dev/null +++ b/bizmatch/src/app/components/search-modal/search-modal-commercial.component.html @@ -0,0 +1,220 @@ +
+
+
+
+

Commercial Property Listing Search

+ +
+
+
+ + + +
+ +
+ + State: {{ criteria.state }} + + + City: {{ criteria.city.name }} + + + Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} + + + Categories: {{ criteria.types.join(', ') }} + + + Title: {{ criteria.title }} + +
+ @if(criteria.criteriaType==='commercialPropertyListings') { +
+
+
+ + +
+
+ +
+
+ +
+ + +
+
+
+ +
+ @for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) { + + } +
+
+
+ +
+ + - + +
+
+
+ + +
+
+ + +
+
+
+ } +
+
+
+
+
+
+

Filter ({{ numberOfResults$ | async }})

+ + +
+ +
+ + State: {{ criteria.state }} + + + City: {{ criteria.city.name }} + + + Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} + + + Categories: {{ criteria.types.join(', ') }} + + + Title: {{ criteria.title }} + +
+ @if(criteria.criteriaType==='commercialPropertyListings') { +
+
+ + +
+
+ +
+
+ +
+ + +
+
+
+ +
+ @for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) { + + } +
+
+
+ + +
+
+ +
+ + - + +
+
+
+ + +
+
+ } +
diff --git a/bizmatch/src/app/components/search-modal/search-modal-commercial.component.ts b/bizmatch/src/app/components/search-modal/search-modal-commercial.component.ts new file mode 100644 index 0000000..9ccc2b9 --- /dev/null +++ b/bizmatch/src/app/components/search-modal/search-modal-commercial.component.ts @@ -0,0 +1,208 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } 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, Subscription, switchMap, tap } from 'rxjs'; +import { CommercialPropertyListingCriteria, CountyResult, GeoResult } from '../../../../../bizmatch-server/src/models/main.model'; +import { CriteriaChangeService } from '../../services/criteria-change.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 { getCriteriaStateObject, resetCommercialPropertyListingCriteria } from '../../utils/utils'; +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 { + @Input() + isModal: boolean = true; + // cities$: Observable; + counties$: Observable; + // cityLoading = false; + countyLoading = false; + // cityInput$ = new Subject(); + countyInput$ = new Subject(); + private criteriaChangeSubscription: Subscription; + public criteria: CommercialPropertyListingCriteria; + private debounceTimeout: any; + public backupCriteria: CommercialPropertyListingCriteria = getCriteriaStateObject('businessListings'); + numberOfResults$: Observable; + 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 + + selectedPropertyType: string | null = null; + selectedPropertyTypeName: string | null = null; + 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.modalService.sendCriteria(this.criteria); + } + hasActiveFilters(): boolean { + return !!(this.criteria.state || this.criteria.city || this.criteria.minPrice || this.criteria.maxPrice || this.criteria.types.length || 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 'types': + this.criteria.types = []; + break; + case 'title': + this.criteria.title = null; + break; + } + this.searchService.search(this.criteria); + } + clearFilter() { + resetCommercialPropertyListingCriteria(this.criteria); + this.searchService.search(this.criteria); + } + // Handle category change + onCategoryChange(event: any[]) { + this.criteria.types = event; + this.onCriteriaChange(); + } + + categoryClicked(checked: boolean, value: string) { + if (checked) { + this.criteria.types.push(value); + } else { + const index = this.criteria.types.findIndex(t => t === value); + if (index > -1) { + this.criteria.types.splice(index, 1); + } + } + this.searchService.search(this.criteria); + } + private loadCounties() { + this.counties$ = concat( + of([]), // default items + this.countyInput$.pipe( + distinctUntilChanged(), + tap(() => (this.countyLoading = true)), + switchMap(term => + this.geoService.findCountiesStartingWith(term).pipe( + catchError(() => of([])), // empty list on error + map(counties => counties.map(county => county.name)), // transform the list of objects to a list of city names + tap(() => (this.countyLoading = false)), + ), + ), + ), + ); + } + onCriteriaChange() { + this.searchService.search(this.criteria); + } + setCity(city) { + if (city) { + this.criteria.city = city; + this.criteria.state = city.state; + } else { + this.criteria.city = null; + this.criteria.radius = null; + this.criteria.searchType = 'exact'; + } + this.searchService.search(this.criteria); + } + setState(state: string) { + if (state) { + this.criteria.state = state; + } else { + this.criteria.state = null; + this.setCity(null); + } + this.searchService.search(this.criteria); + } + setRadius(radius: number) { + this.criteria.radius = radius; + this.searchService.search(this.criteria); + } + 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); + 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(this.criteria, 'commercialProperty'); + } else { + this.numberOfResults$ = of(); + } + } + } + + close() { + this.modalService.reject(this.backupCriteria); + } + + debouncedSearch() { + clearTimeout(this.debounceTimeout); + this.debounceTimeout = setTimeout(() => { + this.searchService.search(this.criteria); + }, 1000); + } +} diff --git a/bizmatch/src/app/components/search-modal/search-modal.component.html b/bizmatch/src/app/components/search-modal/search-modal.component.html index cec1fb2..c110041 100644 --- a/bizmatch/src/app/components/search-modal/search-modal.component.html +++ b/bizmatch/src/app/components/search-modal/search-modal.component.html @@ -1,14 +1,11 @@ -
+
- @if(criteria.criteriaType==='businessListings') {

Business Listing Search

- } @else if (criteria.criteriaType==='commercialPropertyListings') { -

Property Listing Search

- } @else { -

Professional Listing Search

- } +
+ {{ selectOptions.getCommercialProperty(listing.type) }} +
+ {{ selectOptions.getState(listing.location.state) }} +

+ {{ getDaysListed(listing) }} days listed +

+
+

+ {{ listing.title }} + @if(listing.draft){ + Draft + } +

+

{{ listing.location.name ? listing.location.name : listing.location.county }}

+

{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}

+
+ +
+
+ }
+ } @else if (listings?.length === 0){ +
+
+ + + + + + + + + + + + + + + + + +
+

There’s no listing here

+

Try changing your filters to
see listings

+
+ + +
+
+
+
+ }
+ @if(pageCount > 1) { + }
- } @else if (listings?.length===0){ -
-
- - - - - - - - - - - - - - - - - -
-

There’s no listing here

-

Try changing your filters to
see listings

-
- - -
-
-
-
- } + + +
-@if(pageCount>1){ - -} diff --git a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.ts b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.ts index e49304b..c974f5f 100644 --- a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.ts +++ b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.ts @@ -9,17 +9,19 @@ import { CommercialPropertyListingCriteria, LISTINGS_PER_PAGE, ResponseCommercia import { environment } from '../../../../environments/environment'; import { PaginatorComponent } from '../../../components/paginator/paginator.component'; import { ModalService } from '../../../components/search-modal/modal.service'; +import { SearchModalCommercialComponent } from '../../../components/search-modal/search-modal-commercial.component'; import { CriteriaChangeService } from '../../../services/criteria-change.service'; import { ImageService } from '../../../services/image.service'; import { ListingsService } from '../../../services/listings.service'; import { SearchService } from '../../../services/search.service'; import { SelectOptionsService } from '../../../services/select-options.service'; import { assignProperties, getCriteriaProxy, resetCommercialPropertyListingCriteria } from '../../../utils/utils'; + @UntilDestroy() @Component({ selector: 'app-commercial-property-listings', standalone: true, - imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent], + imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, SearchModalCommercialComponent], templateUrl: './commercial-property-listings.component.html', styleUrls: ['./commercial-property-listings.component.scss', '../../pages.scss'], }) @@ -41,6 +43,7 @@ export class CommercialPropertyListingsComponent { page = 1; pageCount = 1; ts = new Date().getTime(); + constructor( public selectOptions: SelectOptionsService, private listingsService: ListingsService, @@ -54,6 +57,7 @@ export class CommercialPropertyListingsComponent { private criteriaChangeService: CriteriaChangeService, ) { this.criteria = getCriteriaProxy('commercialPropertyListings', this) as CommercialPropertyListingCriteria; + this.modalService.sendCriteria(this.criteria); this.init(); this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(criteria => { if (criteria && criteria.criteriaType === 'commercialPropertyListings') { @@ -62,10 +66,13 @@ export class CommercialPropertyListingsComponent { } }); } + async ngOnInit() {} + async init() { this.search(); } + async search() { const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty'); this.listings = (listingReponse).results; @@ -75,21 +82,26 @@ export class CommercialPropertyListingsComponent { this.cdRef.markForCheck(); this.cdRef.detectChanges(); } + onPageChange(page: any) { this.criteria.start = (page - 1) * LISTINGS_PER_PAGE; this.criteria.length = LISTINGS_PER_PAGE; this.criteria.page = page; this.search(); } + reset() { this.criteria.title = null; } + getTS() { return new Date().getTime(); } + getDaysListed(listing: CommercialPropertyListing) { return dayjs().diff(listing.created, 'day'); } + // New methods for filter actions clearAllFilters() { // Reset criteria to default values @@ -106,12 +118,10 @@ export class CommercialPropertyListingsComponent { } async openFilterModal() { - // Open the search modal with current criteria const modalResult = await this.modalService.showModal(this.criteria); if (modalResult.accepted) { - this.searchService.search(this.criteria); - } else { - this.criteria = assignProperties(this.criteria, modalResult.criteria); + this.criteria = assignProperties(this.criteria, modalResult.criteria); // Update criteria with modal result + this.searchService.search(this.criteria); // Trigger search with updated criteria } } }