SEO
This commit is contained in:
@@ -1,260 +1,260 @@
|
||||
<div
|
||||
*ngIf="isModal && (modalService.modalVisible$ | async)?.visible && (modalService.modalVisible$ | async)?.type === 'brokerListings'"
|
||||
class="fixed inset-0 bg-neutral-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="relative w-full h-screen max-h-screen">
|
||||
<div class="relative bg-white rounded-lg shadow h-full">
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t bg-primary-600">
|
||||
<h3 class="text-xl font-semibold text-white p-2 rounded">Professional Search</h3>
|
||||
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-gray-200 hover:text-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close Modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Types: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Professional Name: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.companyName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Company: {{ criteria.companyName }} <button (click)="removeFilter('companyName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.counties?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Areas Served: {{ criteria.counties.join(', ') }} <button (click)="removeFilter('counties')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='brokerListings') {
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-gray-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Professional Type</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.customerSubTypes"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select professional types"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokerName" class="block mb-2 text-sm font-medium text-neutral-900">Professional Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokerName"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. John Smith"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="companyName" class="block mb-2 text-sm font-medium text-neutral-900">Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyName"
|
||||
[ngModel]="criteria.companyName"
|
||||
(ngModelChange)="updateCriteria({ companyName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. ABC Brokers"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Counties / Areas Served</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="counties$ | async"
|
||||
[multiple]="true"
|
||||
[loading]="countyLoading"
|
||||
[typeahead]="countyInput$"
|
||||
[ngModel]="criteria.counties"
|
||||
(ngModelChange)="onCountiesChange($event)"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Type to search counties"
|
||||
></ng-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!isModal" class="space-y-6 pb-10">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Types: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Professional Name: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.companyName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Company: {{ criteria.companyName }} <button (click)="removeFilter('companyName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.counties?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Areas Served: {{ criteria.counties.join(', ') }} <button (click)="removeFilter('counties')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='brokerListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-gray-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Professional Type</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.customerSubTypes"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select professional types"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokerName" class="block mb-2 text-sm font-medium text-neutral-900">Professional Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokerName"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. John Smith"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="companyName" class="block mb-2 text-sm font-medium text-neutral-900">Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyName"
|
||||
[ngModel]="criteria.companyName"
|
||||
(ngModelChange)="updateCriteria({ companyName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. ABC Brokers"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Counties / Areas Served</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="counties$ | async"
|
||||
[multiple]="true"
|
||||
[loading]="countyLoading"
|
||||
[typeahead]="countyInput$"
|
||||
[ngModel]="criteria.counties"
|
||||
(ngModelChange)="onCountiesChange($event)"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Type to search counties"
|
||||
></ng-select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="isModal && (modalService.modalVisible$ | async)?.visible && (modalService.modalVisible$ | async)?.type === 'brokerListings'"
|
||||
class="fixed inset-0 bg-neutral-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="relative w-full h-screen max-h-screen">
|
||||
<div class="relative bg-white rounded-lg shadow h-full">
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t bg-primary-600">
|
||||
<h3 class="text-xl font-semibold text-white p-2 rounded">Professional Search</h3>
|
||||
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-gray-200 hover:text-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close Modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Types: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Professional Name: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.companyName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Company: {{ criteria.companyName }} <button (click)="removeFilter('companyName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.counties?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Areas Served: {{ criteria.counties.join(', ') }} <button (click)="removeFilter('counties')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='brokerListings') {
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-gray-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Professional Type</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.customerSubTypes"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select professional types"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokerName" class="block mb-2 text-sm font-medium text-neutral-900">Professional Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokerName"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. John Smith"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="companyName" class="block mb-2 text-sm font-medium text-neutral-900">Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyName"
|
||||
[ngModel]="criteria.companyName"
|
||||
(ngModelChange)="updateCriteria({ companyName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. ABC Brokers"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Counties / Areas Served</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="counties$ | async"
|
||||
[multiple]="true"
|
||||
[loading]="countyLoading"
|
||||
[typeahead]="countyInput$"
|
||||
[ngModel]="criteria.counties"
|
||||
(ngModelChange)="onCountiesChange($event)"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Type to search counties"
|
||||
></ng-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!isModal" class="space-y-6 pb-10">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Types: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Professional Name: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.companyName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Company: {{ criteria.companyName }} <button (click)="removeFilter('companyName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.counties?.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Areas Served: {{ criteria.counties.join(', ') }} <button (click)="removeFilter('counties')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='brokerListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-gray-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Professional Type</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.customerSubTypes"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select professional types"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokerName" class="block mb-2 text-sm font-medium text-neutral-900">Professional Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokerName"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. John Smith"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="companyName" class="block mb-2 text-sm font-medium text-neutral-900">Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="companyName"
|
||||
[ngModel]="criteria.companyName"
|
||||
(ngModelChange)="updateCriteria({ companyName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. ABC Brokers"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Counties / Areas Served</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="counties$ | async"
|
||||
[multiple]="true"
|
||||
[loading]="countyLoading"
|
||||
[typeahead]="countyInput$"
|
||||
[ngModel]="criteria.counties"
|
||||
(ngModelChange)="onCountiesChange($event)"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Type to search counties"
|
||||
></ng-select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,316 +1,316 @@
|
||||
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 { CountyResult, GeoResult, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { FilterStateService } from '../../services/filter-state.service';
|
||||
import { GeoService } from '../../services/geo.service';
|
||||
import { SearchService } from '../../services/search.service';
|
||||
import { SelectOptionsService } from '../../services/select-options.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { ValidatedCityComponent } from '../validated-city/validated-city.component';
|
||||
import { ModalService } from './modal.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-search-modal-broker',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, NgSelectModule, ValidatedCityComponent],
|
||||
templateUrl: './search-modal-broker.component.html',
|
||||
styleUrls: ['./search-modal.component.scss'],
|
||||
})
|
||||
export class SearchModalBrokerComponent implements OnInit, OnDestroy {
|
||||
@Input() isModal: boolean = true;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private searchDebounce$ = new Subject<void>();
|
||||
|
||||
// State
|
||||
criteria: UserListingCriteria;
|
||||
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 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 => {
|
||||
if (criteria?.criteriaType === 'brokerListings') {
|
||||
this.initializeWithCriteria(criteria);
|
||||
}
|
||||
});
|
||||
|
||||
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
|
||||
if (val.visible && val.type === 'brokerListings') {
|
||||
// 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: UserListingCriteria): void {
|
||||
this.criteria = criteria;
|
||||
this.backupCriteria = JSON.parse(JSON.stringify(criteria));
|
||||
this.setTotalNumberOfResults();
|
||||
}
|
||||
|
||||
private subscribeToStateChanges(): void {
|
||||
if (!this.isModal) {
|
||||
this.filterStateService
|
||||
.getState$('brokerListings')
|
||||
.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 'types':
|
||||
updates.types = [];
|
||||
break;
|
||||
case 'brokerName':
|
||||
updates.brokerName = null;
|
||||
break;
|
||||
case 'companyName':
|
||||
updates.companyName = null;
|
||||
break;
|
||||
case 'counties':
|
||||
updates.counties = [];
|
||||
break;
|
||||
}
|
||||
|
||||
this.updateCriteria(updates);
|
||||
}
|
||||
|
||||
// Professional type 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 });
|
||||
}
|
||||
|
||||
// Counties handling
|
||||
onCountiesChange(selectedCounties: string[]): void {
|
||||
this.updateCriteria({ counties: selectedCounties });
|
||||
}
|
||||
|
||||
// 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('brokerListings');
|
||||
}
|
||||
}
|
||||
|
||||
// Modal-specific methods
|
||||
closeAndSearch(): void {
|
||||
if (this.isModal) {
|
||||
// Save changes to state
|
||||
this.filterStateService.setCriteria('brokerListings', this.criteria);
|
||||
this.modalService.accept();
|
||||
this.searchService.search('brokerListings');
|
||||
}
|
||||
}
|
||||
|
||||
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('brokerListings', 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('brokerListings');
|
||||
}
|
||||
}
|
||||
|
||||
private setTotalNumberOfResults(): void {
|
||||
this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria);
|
||||
}
|
||||
|
||||
private getDefaultCriteria(): UserListingCriteria {
|
||||
return {
|
||||
criteriaType: 'brokerListings',
|
||||
types: [],
|
||||
state: null,
|
||||
city: null,
|
||||
radius: null,
|
||||
searchType: 'exact' as const,
|
||||
brokerName: null,
|
||||
companyName: null,
|
||||
counties: [],
|
||||
prompt: null,
|
||||
page: 1,
|
||||
start: 0,
|
||||
length: 12,
|
||||
};
|
||||
}
|
||||
|
||||
hasActiveFilters(): boolean {
|
||||
if (!this.criteria) return false;
|
||||
|
||||
return !!(
|
||||
this.criteria.state ||
|
||||
this.criteria.city ||
|
||||
this.criteria.types?.length ||
|
||||
this.criteria.brokerName ||
|
||||
this.criteria.companyName ||
|
||||
this.criteria.counties?.length
|
||||
);
|
||||
}
|
||||
|
||||
trackByFn(item: GeoResult): any {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
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 { CountyResult, GeoResult, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { FilterStateService } from '../../services/filter-state.service';
|
||||
import { GeoService } from '../../services/geo.service';
|
||||
import { SearchService } from '../../services/search.service';
|
||||
import { SelectOptionsService } from '../../services/select-options.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { ValidatedCityComponent } from '../validated-city/validated-city.component';
|
||||
import { ModalService } from './modal.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-search-modal-broker',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, NgSelectModule, ValidatedCityComponent],
|
||||
templateUrl: './search-modal-broker.component.html',
|
||||
styleUrls: ['./search-modal.component.scss'],
|
||||
})
|
||||
export class SearchModalBrokerComponent implements OnInit, OnDestroy {
|
||||
@Input() isModal: boolean = true;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
private searchDebounce$ = new Subject<void>();
|
||||
|
||||
// State
|
||||
criteria: UserListingCriteria;
|
||||
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 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 => {
|
||||
if (criteria?.criteriaType === 'brokerListings') {
|
||||
this.initializeWithCriteria(criteria);
|
||||
}
|
||||
});
|
||||
|
||||
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
|
||||
if (val.visible && val.type === 'brokerListings') {
|
||||
// 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: UserListingCriteria): void {
|
||||
this.criteria = criteria;
|
||||
this.backupCriteria = JSON.parse(JSON.stringify(criteria));
|
||||
this.setTotalNumberOfResults();
|
||||
}
|
||||
|
||||
private subscribeToStateChanges(): void {
|
||||
if (!this.isModal) {
|
||||
this.filterStateService
|
||||
.getState$('brokerListings')
|
||||
.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 'types':
|
||||
updates.types = [];
|
||||
break;
|
||||
case 'brokerName':
|
||||
updates.brokerName = null;
|
||||
break;
|
||||
case 'companyName':
|
||||
updates.companyName = null;
|
||||
break;
|
||||
case 'counties':
|
||||
updates.counties = [];
|
||||
break;
|
||||
}
|
||||
|
||||
this.updateCriteria(updates);
|
||||
}
|
||||
|
||||
// Professional type 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 });
|
||||
}
|
||||
|
||||
// Counties handling
|
||||
onCountiesChange(selectedCounties: string[]): void {
|
||||
this.updateCriteria({ counties: selectedCounties });
|
||||
}
|
||||
|
||||
// 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('brokerListings');
|
||||
}
|
||||
}
|
||||
|
||||
// Modal-specific methods
|
||||
closeAndSearch(): void {
|
||||
if (this.isModal) {
|
||||
// Save changes to state
|
||||
this.filterStateService.setCriteria('brokerListings', this.criteria);
|
||||
this.modalService.accept();
|
||||
this.searchService.search('brokerListings');
|
||||
}
|
||||
}
|
||||
|
||||
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('brokerListings', 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('brokerListings');
|
||||
}
|
||||
}
|
||||
|
||||
private setTotalNumberOfResults(): void {
|
||||
this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria);
|
||||
}
|
||||
|
||||
private getDefaultCriteria(): UserListingCriteria {
|
||||
return {
|
||||
criteriaType: 'brokerListings',
|
||||
types: [],
|
||||
state: null,
|
||||
city: null,
|
||||
radius: null,
|
||||
searchType: 'exact' as const,
|
||||
brokerName: null,
|
||||
companyName: null,
|
||||
counties: [],
|
||||
prompt: null,
|
||||
page: 1,
|
||||
start: 0,
|
||||
length: 12,
|
||||
};
|
||||
}
|
||||
|
||||
hasActiveFilters(): boolean {
|
||||
if (!this.criteria) return false;
|
||||
|
||||
return !!(
|
||||
this.criteria.state ||
|
||||
this.criteria.city ||
|
||||
this.criteria.types?.length ||
|
||||
this.criteria.brokerName ||
|
||||
this.criteria.companyName ||
|
||||
this.criteria.counties?.length
|
||||
);
|
||||
}
|
||||
|
||||
trackByFn(item: GeoResult): any {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,250 +1,250 @@
|
||||
<div
|
||||
*ngIf="isModal && (modalService.modalVisible$ | async)?.visible && (modalService.modalVisible$ | async)?.type === 'commercialPropertyListings'"
|
||||
class="fixed inset-0 bg-neutral-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="relative w-full h-screen max-h-screen">
|
||||
<div class="relative bg-white rounded-lg shadow h-full">
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t bg-primary-600">
|
||||
<h3 class="text-xl font-semibold text-white p-2 rounded">Commercial Property Listing Search</h3>
|
||||
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-gray-200 hover:text-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close Modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-gray-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-neutral-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="price-from" [ngModel]="criteria.minPrice" (ngModelChange)="updateCriteria({ minPrice: $event })" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="price-to" [ngModel]="criteria.maxPrice" (ngModelChange)="updateCriteria({ maxPrice: $event })" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="block mb-2 text-sm font-medium text-neutral-900">Title / Description (Free Search)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[ngModel]="criteria.title"
|
||||
(ngModelChange)="updateCriteria({ title: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Office Space"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Category</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.typesOfCommercialProperty"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername" class="block mb-2 text-sm font-medium text-neutral-900">Broker Name / Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Brokers Invest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!isModal" class="space-y-6 pb-10">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-gray-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Category</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.typesOfCommercialProperty"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-neutral-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="price-from" [ngModel]="criteria.minPrice" (ngModelChange)="updateCriteria({ minPrice: $event })" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"> </app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="price-to" [ngModel]="criteria.maxPrice" (ngModelChange)="updateCriteria({ maxPrice: $event })" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"> </app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="block mb-2 text-sm font-medium text-neutral-900">Title / Description (Free Search)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[ngModel]="criteria.title"
|
||||
(ngModelChange)="updateCriteria({ title: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Office Space"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername-embedded" class="block mb-2 text-sm font-medium text-neutral-900">Broker Name / Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername-embedded"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Brokers Invest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="isModal && (modalService.modalVisible$ | async)?.visible && (modalService.modalVisible$ | async)?.type === 'commercialPropertyListings'"
|
||||
class="fixed inset-0 bg-neutral-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="relative w-full h-screen max-h-screen">
|
||||
<div class="relative bg-white rounded-lg shadow h-full">
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t bg-primary-600">
|
||||
<h3 class="text-xl font-semibold text-white p-2 rounded">Commercial Property Listing Search</h3>
|
||||
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-gray-200 hover:text-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close Modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-gray-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-neutral-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="price-from" [ngModel]="criteria.minPrice" (ngModelChange)="updateCriteria({ minPrice: $event })" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="price-to" [ngModel]="criteria.maxPrice" (ngModelChange)="updateCriteria({ maxPrice: $event })" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="block mb-2 text-sm font-medium text-neutral-900">Title / Description (Free Search)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[ngModel]="criteria.title"
|
||||
(ngModelChange)="updateCriteria({ title: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Office Space"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Category</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.typesOfCommercialProperty"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername" class="block mb-2 text-sm font-medium text-neutral-900">Broker Name / Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Brokers Invest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="!isModal" class="space-y-6 pb-10">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='commercialPropertyListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-gray-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Category</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.typesOfCommercialProperty"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-neutral-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="price-from" [ngModel]="criteria.minPrice" (ngModelChange)="updateCriteria({ minPrice: $event })" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"> </app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="price-to" [ngModel]="criteria.maxPrice" (ngModelChange)="updateCriteria({ maxPrice: $event })" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"> </app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="block mb-2 text-sm font-medium text-neutral-900">Title / Description (Free Search)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[ngModel]="criteria.title"
|
||||
(ngModelChange)="updateCriteria({ title: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Office Space"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername-embedded" class="block mb-2 text-sm font-medium text-neutral-900">Broker Name / Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername-embedded"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-gray-50 border border-gray-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Brokers Invest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,316 +1,316 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,415 +1,415 @@
|
||||
<div
|
||||
*ngIf="isModal && (modalService.modalVisible$ | async)?.visible && (modalService.modalVisible$ | async)?.type === 'businessListings'"
|
||||
class="fixed inset-0 bg-neutral-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="relative w-full max-h-full">
|
||||
<div class="relative bg-white rounded-lg shadow">
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t bg-primary-600">
|
||||
<h3 class="text-xl font-semibold text-white p-2 rounded">Business Listing Search</h3>
|
||||
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-neutral-200 hover:text-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close Modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minRevenue || criteria.maxRevenue" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Revenue: {{ criteria.minRevenue || 'Any' }} - {{ criteria.maxRevenue || 'Any' }} <button (click)="removeFilter('revenue')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minCashFlow || criteria.maxCashFlow" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Cashflow: {{ criteria.minCashFlow || 'Any' }} - {{ criteria.maxCashFlow || 'Any' }} <button (click)="removeFilter('cashflow')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="selectedPropertyType" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Property Type: {{ getSelectedPropertyTypeName() }} <button (click)="removeFilter('propertyType')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minNumberEmployees || criteria.maxNumberEmployees" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Employees: {{ criteria.minNumberEmployees || 'Any' }} - {{ criteria.maxNumberEmployees || 'Any' }} <button (click)="removeFilter('employees')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.establishedMin" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Established: {{ criteria.establishedMin || 'Any' }} <button (click)="removeFilter('established')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-neutral-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-neutral-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-neutral-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="price-from" [ngModel]="criteria.minPrice" (ngModelChange)="updateCriteria({ minPrice: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="price-to" [ngModel]="criteria.maxPrice" (ngModelChange)="updateCriteria({ maxPrice: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="salesRevenue" class="block mb-2 text-sm font-medium text-neutral-900">Sales Revenue</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="salesRevenue-from" [ngModel]="criteria.minRevenue" (ngModelChange)="updateCriteria({ minRevenue: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="salesRevenue-to" [ngModel]="criteria.maxRevenue" (ngModelChange)="updateCriteria({ maxRevenue: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p.2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cashflow" class="block mb-2 text-sm font-medium text-neutral-900">Cashflow</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="cashflow-from" [ngModel]="criteria.minCashFlow" (ngModelChange)="updateCriteria({ minCashFlow: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="cashflow-to" [ngModel]="criteria.maxCashFlow" (ngModelChange)="updateCriteria({ maxCashFlow: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="block mb-2 text-sm font-medium text-neutral-900">Title / Description (Free Search)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[ngModel]="criteria.title"
|
||||
(ngModelChange)="updateCriteria({ title: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Restaurant"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Category</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.typesOfBusiness"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Type of Property</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="propertyTypeOptions"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="selectedPropertyType"
|
||||
(ngModelChange)="onPropertyTypeChange($event)"
|
||||
placeholder="Select property type"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="numberEmployees" class="block mb-2 text-sm font-medium text-neutral-900">Number of Employees</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
id="numberEmployees-from"
|
||||
[ngModel]="criteria.minNumberEmployees"
|
||||
(ngModelChange)="updateCriteria({ minNumberEmployees: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="From"
|
||||
/>
|
||||
<span>-</span>
|
||||
<input
|
||||
type="number"
|
||||
id="numberEmployees-to"
|
||||
[ngModel]="criteria.maxNumberEmployees"
|
||||
(ngModelChange)="updateCriteria({ maxNumberEmployees: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="To"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="establishedMin" class="block mb-2 text-sm font-medium text-neutral-900">Minimum years established</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
id="establishedMin"
|
||||
[ngModel]="criteria.establishedMin"
|
||||
(ngModelChange)="updateCriteria({ establishedMin: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="YY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername" class="block mb-2 text-sm font-medium text-neutral-900">Broker Name / Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Brokers Invest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ################################################################################## -->
|
||||
<!-- ################################################################################## -->
|
||||
<!-- ################################################################################## -->
|
||||
<div *ngIf="!isModal" class="space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minRevenue || criteria.maxRevenue" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Revenue: {{ criteria.minRevenue || 'Any' }} - {{ criteria.maxRevenue || 'Any' }} <button (click)="removeFilter('revenue')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minCashFlow || criteria.maxCashFlow" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Cashflow: {{ criteria.minCashFlow || 'Any' }} - {{ criteria.maxCashFlow || 'Any' }} <button (click)="removeFilter('cashflow')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="selectedPropertyType" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Property Type: {{ getSelectedPropertyTypeName() }} <button (click)="removeFilter('propertyType')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minNumberEmployees || criteria.maxNumberEmployees" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Employees: {{ criteria.minNumberEmployees || 'Any' }} - {{ criteria.maxNumberEmployees || 'Any' }} <button (click)="removeFilter('employees')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.establishedMin" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Years established: {{ criteria.establishedMin || 'Any' }} <button (click)="removeFilter('established')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='businessListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-neutral-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-neutral-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-neutral-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="price-from" [ngModel]="criteria.minPrice" (ngModelChange)="updateCriteria({ minPrice: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5"> </app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="price-to" [ngModel]="criteria.maxPrice" (ngModelChange)="updateCriteria({ maxPrice: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5"> </app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="salesRevenue" class="block mb-2 text-sm font-medium text-neutral-900">Sales Revenue</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="salesRevenue-from" [ngModel]="criteria.minRevenue" (ngModelChange)="updateCriteria({ minRevenue: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="salesRevenue-to" [ngModel]="criteria.maxRevenue" (ngModelChange)="updateCriteria({ maxRevenue: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p.2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cashflow" class="block mb-2 text-sm font-medium text-neutral-900">Cashflow</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="cashflow-from" [ngModel]="criteria.minCashFlow" (ngModelChange)="updateCriteria({ minCashFlow: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="cashflow-to" [ngModel]="criteria.maxCashFlow" (ngModelChange)="updateCriteria({ maxCashFlow: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="block mb-2 text-sm font-medium text-neutral-900">Title / Description (Free Search)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[ngModel]="criteria.title"
|
||||
(ngModelChange)="updateCriteria({ title: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Restaurant"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Category</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.typesOfBusiness"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Type of Property</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="propertyTypeOptions"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="selectedPropertyType"
|
||||
(ngModelChange)="onPropertyTypeChange($event)"
|
||||
placeholder="Select property type"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="numberEmployees" class="block mb-2 text-sm font-medium text-neutral-900">Number of Employees</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
id="numberEmployees-from"
|
||||
[ngModel]="criteria.minNumberEmployees"
|
||||
(ngModelChange)="updateCriteria({ minNumberEmployees: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="From"
|
||||
/>
|
||||
<span>-</span>
|
||||
<input
|
||||
type="number"
|
||||
id="numberEmployees-to"
|
||||
[ngModel]="criteria.maxNumberEmployees"
|
||||
(ngModelChange)="updateCriteria({ maxNumberEmployees: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="To"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="establishedMin" class="block mb-2 text-sm font-medium text-neutral-900">Minimum years established</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
id="establishedMin"
|
||||
[ngModel]="criteria.establishedMin"
|
||||
(ngModelChange)="updateCriteria({ establishedMin: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="YY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername" class="block mb-2 text-sm font-medium text-neutral-900">Broker Name / Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Brokers Invest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
*ngIf="isModal && (modalService.modalVisible$ | async)?.visible && (modalService.modalVisible$ | async)?.type === 'businessListings'"
|
||||
class="fixed inset-0 bg-neutral-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="relative w-full max-h-full">
|
||||
<div class="relative bg-white rounded-lg shadow">
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t bg-primary-600">
|
||||
<h3 class="text-xl font-semibold text-white p-2 rounded">Business Listing Search</h3>
|
||||
<button (click)="closeAndSearch()" type="button" class="text-white bg-transparent hover:bg-neutral-200 hover:text-neutral-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close Modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<button class="text-primary-600 font-medium border-b-2 border-primary-600 pb-2">Filter ({{ numberOfResults$ | async }})</button>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minRevenue || criteria.maxRevenue" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Revenue: {{ criteria.minRevenue || 'Any' }} - {{ criteria.maxRevenue || 'Any' }} <button (click)="removeFilter('revenue')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minCashFlow || criteria.maxCashFlow" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Cashflow: {{ criteria.minCashFlow || 'Any' }} - {{ criteria.maxCashFlow || 'Any' }} <button (click)="removeFilter('cashflow')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="selectedPropertyType" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Property Type: {{ getSelectedPropertyTypeName() }} <button (click)="removeFilter('propertyType')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minNumberEmployees || criteria.maxNumberEmployees" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Employees: {{ criteria.minNumberEmployees || 'Any' }} - {{ criteria.maxNumberEmployees || 'Any' }} <button (click)="removeFilter('employees')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.establishedMin" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Established: {{ criteria.establishedMin || 'Any' }} <button (click)="removeFilter('established')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-neutral-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-neutral-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-neutral-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="price-from" [ngModel]="criteria.minPrice" (ngModelChange)="updateCriteria({ minPrice: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="price-to" [ngModel]="criteria.maxPrice" (ngModelChange)="updateCriteria({ maxPrice: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="salesRevenue" class="block mb-2 text-sm font-medium text-neutral-900">Sales Revenue</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="salesRevenue-from" [ngModel]="criteria.minRevenue" (ngModelChange)="updateCriteria({ minRevenue: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="salesRevenue-to" [ngModel]="criteria.maxRevenue" (ngModelChange)="updateCriteria({ maxRevenue: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p.2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cashflow" class="block mb-2 text-sm font-medium text-neutral-900">Cashflow</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="cashflow-from" [ngModel]="criteria.minCashFlow" (ngModelChange)="updateCriteria({ minCashFlow: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="cashflow-to" [ngModel]="criteria.maxCashFlow" (ngModelChange)="updateCriteria({ maxCashFlow: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="block mb-2 text-sm font-medium text-neutral-900">Title / Description (Free Search)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[ngModel]="criteria.title"
|
||||
(ngModelChange)="updateCriteria({ title: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Restaurant"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Category</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.typesOfBusiness"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Type of Property</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="propertyTypeOptions"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="selectedPropertyType"
|
||||
(ngModelChange)="onPropertyTypeChange($event)"
|
||||
placeholder="Select property type"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="numberEmployees" class="block mb-2 text-sm font-medium text-neutral-900">Number of Employees</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
id="numberEmployees-from"
|
||||
[ngModel]="criteria.minNumberEmployees"
|
||||
(ngModelChange)="updateCriteria({ minNumberEmployees: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="From"
|
||||
/>
|
||||
<span>-</span>
|
||||
<input
|
||||
type="number"
|
||||
id="numberEmployees-to"
|
||||
[ngModel]="criteria.maxNumberEmployees"
|
||||
(ngModelChange)="updateCriteria({ maxNumberEmployees: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="To"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="establishedMin" class="block mb-2 text-sm font-medium text-neutral-900">Minimum years established</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
id="establishedMin"
|
||||
[ngModel]="criteria.establishedMin"
|
||||
(ngModelChange)="updateCriteria({ establishedMin: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="YY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername" class="block mb-2 text-sm font-medium text-neutral-900">Broker Name / Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Brokers Invest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ################################################################################## -->
|
||||
<!-- ################################################################################## -->
|
||||
<!-- ################################################################################## -->
|
||||
<div *ngIf="!isModal" class="space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<h3 class="text-xl font-semibold text-neutral-900">Filter ({{ numberOfResults$ | async }})</h3>
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-primary-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-neutral-900 bg-white border border-neutral-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Display active filters as tags -->
|
||||
<div class="flex flex-wrap gap-2" *ngIf="hasActiveFilters()">
|
||||
<span *ngIf="criteria.state" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
State: {{ criteria.state }} <button (click)="removeFilter('state')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.city" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
City: {{ criteria.city.name }} <button (click)="removeFilter('city')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minPrice || criteria.maxPrice" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Price: {{ criteria.minPrice || 'Any' }} - {{ criteria.maxPrice || 'Any' }} <button (click)="removeFilter('price')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minRevenue || criteria.maxRevenue" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Revenue: {{ criteria.minRevenue || 'Any' }} - {{ criteria.maxRevenue || 'Any' }} <button (click)="removeFilter('revenue')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minCashFlow || criteria.maxCashFlow" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Cashflow: {{ criteria.minCashFlow || 'Any' }} - {{ criteria.maxCashFlow || 'Any' }} <button (click)="removeFilter('cashflow')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.title" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Title: {{ criteria.title }} <button (click)="removeFilter('title')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.types.length" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Categories: {{ criteria.types.join(', ') }} <button (click)="removeFilter('types')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="selectedPropertyType" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Property Type: {{ getSelectedPropertyTypeName() }} <button (click)="removeFilter('propertyType')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.minNumberEmployees || criteria.maxNumberEmployees" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Employees: {{ criteria.minNumberEmployees || 'Any' }} - {{ criteria.maxNumberEmployees || 'Any' }} <button (click)="removeFilter('employees')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.establishedMin" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Years established: {{ criteria.establishedMin || 'Any' }} <button (click)="removeFilter('established')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
<span *ngIf="criteria.brokerName" class="bg-neutral-200 text-neutral-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
|
||||
Broker: {{ criteria.brokerName }} <button (click)="removeFilter('brokerName')" class="ml-1 text-red-500 hover:text-red-700">×</button>
|
||||
</span>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='businessListings') {
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-neutral-900">Location - State</label>
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-neutral-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Search Type</label>
|
||||
<div class="flex items-center space-x-4">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="exact" />
|
||||
<span class="ml-2">Exact City</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" class="form-radio" name="searchType" [ngModel]="criteria.searchType" (ngModelChange)="updateCriteria({ searchType: $event })" value="radius" />
|
||||
<span class="ml-2">Radius Search</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Select Radius (in miles)</label>
|
||||
<div class="flex flex-wrap">
|
||||
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-2 text-xs font-medium text-center border border-neutral-200 hover:bg-neutral-500 hover:text-white"
|
||||
[ngClass]="criteria.radius === radius ? 'text-white bg-neutral-500' : 'text-neutral-900 bg-white'"
|
||||
(click)="setRadius(radius)"
|
||||
>
|
||||
{{ radius }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-neutral-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="price-from" [ngModel]="criteria.minPrice" (ngModelChange)="updateCriteria({ minPrice: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5"> </app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="price-to" [ngModel]="criteria.maxPrice" (ngModelChange)="updateCriteria({ maxPrice: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5"> </app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="salesRevenue" class="block mb-2 text-sm font-medium text-neutral-900">Sales Revenue</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="salesRevenue-from" [ngModel]="criteria.minRevenue" (ngModelChange)="updateCriteria({ minRevenue: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="salesRevenue-to" [ngModel]="criteria.maxRevenue" (ngModelChange)="updateCriteria({ maxRevenue: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p.2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cashflow" class="block mb-2 text-sm font-medium text-neutral-900">Cashflow</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<app-validated-price name="cashflow-from" [ngModel]="criteria.minCashFlow" (ngModelChange)="updateCriteria({ minCashFlow: $event })" placeholder="From" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
<span>-</span>
|
||||
<app-validated-price name="cashflow-to" [ngModel]="criteria.maxCashFlow" (ngModelChange)="updateCriteria({ maxCashFlow: $event })" placeholder="To" inputClasses="bg-neutral-50 text-sm !mt-0 p-2.5">
|
||||
</app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="block mb-2 text-sm font-medium text-neutral-900">Title / Description (Free Search)</label>
|
||||
<input
|
||||
type="text"
|
||||
id="title"
|
||||
[ngModel]="criteria.title"
|
||||
(ngModelChange)="updateCriteria({ title: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Restaurant"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Category</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="selectOptions.typesOfBusiness"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="criteria.types"
|
||||
(ngModelChange)="onCategoryChange($event)"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="true"
|
||||
placeholder="Select categories"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block mb-2 text-sm font-medium text-neutral-900">Type of Property</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[items]="propertyTypeOptions"
|
||||
bindLabel="name"
|
||||
bindValue="value"
|
||||
[ngModel]="selectedPropertyType"
|
||||
(ngModelChange)="onPropertyTypeChange($event)"
|
||||
placeholder="Select property type"
|
||||
></ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="numberEmployees" class="block mb-2 text-sm font-medium text-neutral-900">Number of Employees</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
id="numberEmployees-from"
|
||||
[ngModel]="criteria.minNumberEmployees"
|
||||
(ngModelChange)="updateCriteria({ minNumberEmployees: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="From"
|
||||
/>
|
||||
<span>-</span>
|
||||
<input
|
||||
type="number"
|
||||
id="numberEmployees-to"
|
||||
[ngModel]="criteria.maxNumberEmployees"
|
||||
(ngModelChange)="updateCriteria({ maxNumberEmployees: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="To"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="establishedMin" class="block mb-2 text-sm font-medium text-neutral-900">Minimum years established</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="number"
|
||||
id="establishedMin"
|
||||
[ngModel]="criteria.establishedMin"
|
||||
(ngModelChange)="updateCriteria({ establishedMin: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-1/2 p-2.5"
|
||||
placeholder="YY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername" class="block mb-2 text-sm font-medium text-neutral-900">Broker Name / Company Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername"
|
||||
[ngModel]="criteria.brokerName"
|
||||
(ngModelChange)="updateCriteria({ brokerName: $event })"
|
||||
class="bg-neutral-50 border border-neutral-300 text-sm rounded-lg block w-full p-2.5"
|
||||
placeholder="e.g. Brokers Invest"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,445 +1,445 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user