Einbau klassische Filter als Overlay ...

This commit is contained in:
2024-07-16 17:09:59 +02:00
parent af982d19d8
commit bdafb03165
32 changed files with 1274 additions and 239 deletions

View File

@@ -31,3 +31,4 @@
</div>
}
<app-message-container></app-message-container>
<app-search-modal></app-search-modal>

View File

@@ -2,21 +2,20 @@ import { CommonModule } from '@angular/common';
import { Component, HostListener } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { filter } from 'rxjs/operators';
import { ListingCriteria } from '../../../bizmatch-server/src/models/main.model';
import build from '../build';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component';
import { SearchModalComponent } from './components/search-modal/search-modal.component';
import { LoadingService } from './services/loading.service';
import { UserService } from './services/user.service';
import { createDefaultListingCriteria } from './utils/utils';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent],
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent],
providers: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
@@ -25,9 +24,7 @@ export class AppComponent {
build = build;
title = 'bizmatch';
actualRoute = '';
listingCriteria: ListingCriteria = onChange(createDefaultListingCriteria(), (path, value, previousValue, applyData) => {
sessionStorage.setItem('criteria', JSON.stringify(value));
});
public constructor(public loadingService: LoadingService, private router: Router, private activatedRoute: ActivatedRoute, private keycloakService: KeycloakService, private userService: UserService) {
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
let currentRoute = this.activatedRoute.root;

View File

@@ -4,7 +4,7 @@ import { createPopper, Instance as PopperInstance } from '@popperjs/core';
@Component({
selector: 'app-dropdown',
template: `
<div #targetEl [class.hidden]="!isVisible" class="z-10">
<div #targetEl [class.hidden]="!isVisible" class="z-50">
<ng-content></ng-content>
</div>
`,

View File

@@ -120,6 +120,7 @@
<button
type="button"
#triggerButton
(click)="openModal()"
id="filterDropdownButton"
class="max-sm:hidden px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 md:me-2"
>
@@ -243,24 +244,14 @@
</button>
</div> -->
</nav>
<!-- ############################### -->
<!-- Filter Dropdown -->
<!-- ############################### -->
<app-dropdown [triggerEl]="triggerButton" [triggerType]="'click'">
<div id="filterDropdown" class="z-10 w-80 p-3 bg-slate-200 rounded-lg shadow-lg dark:bg-gray-700">
<h3 class="mb-3 text-sm font-medium text-gray-900 dark:text-white">Filter</h3>
<!-- Price Range -->
<!-- <app-dropdown [triggerEl]="triggerButton" [triggerType]="'click'">
<div id="filterDropdown" class="z-[50] w-80 p-3 bg-slate-200 rounded-lg shadow-lg dark:bg-gray-700">
<div class="mb-4">
<div class="flex items-center space-x-4">
<input
type="text"
[ngModel]="prompt"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
value="300"
/>
</div>
<!-- <label for="price-range" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Price Range</label>
<label for="price-range" class="block text-sm font-medium text-gray-900 dark:text-white">Price Range</label>
<div class="flex items-center space-x-4">
<input
type="number"
@@ -276,12 +267,10 @@
placeholder="To"
value="3500"
/>
</div> -->
</div>
</div>
<!-- Sales -->
<!-- <div class="mb-4">
<label for="sales-range" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Sales</label>
<div class="mb-4">
<label for="sales-range" class="block text-sm font-medium text-gray-900 dark:text-white">Sales Revenue</label>
<div class="flex items-center space-x-4">
<input
type="number"
@@ -298,10 +287,8 @@
value="100"
/>
</div>
</div> -->
<!-- Category -->
<!-- <div class="mb-4">
</div>
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Category</label>
<div class="flex flex-wrap gap-2">
<button
@@ -335,10 +322,8 @@
Watch
</button>
</div>
</div> -->
<!-- State -->
<!-- <div class="mb-4">
</div>
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">State</label>
<ul class="w-48 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<li class="w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600">
@@ -379,9 +364,8 @@
</div>
</li>
</ul>
</div> -->
</div>
<!-- Action Buttons -->
<div class="flex justify-between">
<button
type="button"
@@ -397,4 +381,4 @@
</button>
</div>
</div>
</app-dropdown>
</app-dropdown> -->

View File

@@ -1,4 +1,4 @@
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
import { BreakpointObserver } from '@angular/cdk/layout';
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@@ -6,14 +6,15 @@ import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
import { KeycloakService } from 'keycloak-angular';
import { Observable, Subject, takeUntil } from 'rxjs';
import { Observable, Subject, Subscription } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { emailToDirName, KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment';
import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service';
import { map2User } from '../../utils/utils';
import { createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service';
@Component({
selector: 'header',
standalone: true,
@@ -34,7 +35,15 @@ export class HeaderComponent {
isMobile: boolean = false;
private destroy$ = new Subject<void>();
prompt: string;
constructor(public keycloakService: KeycloakService, private router: Router, private userService: UserService, private sharedService: SharedService, private breakpointObserver: BreakpointObserver) {}
private subscription: Subscription;
constructor(
public keycloakService: KeycloakService,
private router: Router,
private userService: UserService,
private sharedService: SharedService,
private breakpointObserver: BreakpointObserver,
private modalService: ModalService,
) {}
async ngOnInit() {
const token = await this.keycloakService.getToken();
@@ -52,30 +61,40 @@ export class HeaderComponent {
initFlowbite();
}
});
this.breakpointObserver
.observe([Breakpoints.Handset])
.pipe(takeUntil(this.destroy$))
.subscribe(result => {
this.isMobile = result.matches;
const targetEl = document.getElementById('filterDropdown');
const triggerEl = this.isMobile ? document.getElementById('filterDropdownMobileButton') : document.getElementById('filterDropdownButton');
if (targetEl && triggerEl) {
this.filterDropdown = new Dropdown(targetEl, triggerEl);
}
});
// this.breakpointObserver
// .observe([Breakpoints.Handset])
// .pipe(takeUntil(this.destroy$))
// .subscribe(result => {
// this.isMobile = result.matches;
// const targetEl = document.getElementById('filterDropdown');
// const triggerEl = this.isMobile ? document.getElementById('filterDropdownMobileButton') : document.getElementById('filterDropdownButton');
// if (targetEl && triggerEl) {
// this.filterDropdown = new Dropdown(targetEl, triggerEl);
// }
// });
this.sharedService.currentProfilePhoto.subscribe(photoUrl => {
if (photoUrl) {
this.profileUrl = photoUrl;
}
});
}
toggleFilterDropdown() {
if (this.filterDropdown) {
this.filterDropdown.toggle();
// toggleFilterDropdown() {
// if (this.filterDropdown) {
// this.filterDropdown.toggle();
// }
// }
ngAfterViewInit() {}
openModal() {
if (this.isActive('/businessListings')) {
this.modalService.showModal(createEmptyBusinessListingCriteria());
} else if (this.isActive('/commercialPropertyListings')) {
this.modalService.showModal(createEmptyCommercialPropertyListingCriteria());
} else if (this.isActive('/brokerListings')) {
this.modalService.showModal(createEmptyUserListingCriteria());
}
}
ngAfterViewInit() {}
navigateWithState(dest: string, state: any) {
this.router.navigate([dest], { state: state });
}

View File

@@ -0,0 +1,34 @@
// 1. Shared Service (modal.service.ts)
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
@Injectable({
providedIn: 'root',
})
export class ModalService {
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
private messageSubject = new BehaviorSubject<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria>(null);
private resolvePromise!: (value: boolean) => void;
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
message$: Observable<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria> = this.messageSubject.asObservable();
showModal(message: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): Promise<boolean> {
this.messageSubject.next(message);
this.modalVisibleSubject.next(true);
return new Promise<boolean>(resolve => {
this.resolvePromise = resolve;
});
}
accept(): void {
this.modalVisibleSubject.next(false);
this.resolvePromise(true);
}
reject(): void {
this.modalVisibleSubject.next(false);
this.resolvePromise(false);
}
}

View File

@@ -0,0 +1,332 @@
<!-- <div class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full"> -->
<div *ngIf="modalService.modalVisible$ | async" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="relative w-full max-w-4xl max-h-full">
<div class="relative bg-white rounded-lg shadow">
<div class="flex items-start justify-between p-4 border-b rounded-t">
<h3 class="text-xl font-semibold text-gray-900">Business Listing Search</h3>
<button (click)="modalService.reject()" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-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-blue-600 font-medium border-b-2 border-blue-600 pb-2">Classic Search</button>
<button class="text-gray-500">AI Search <span class="bg-gray-200 text-xs font-semibold px-2 py-1 rounded">BETA</span></button>
</div>
@if(criteria.criteriaType==='business'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
<select id="state" [(ngModel)]="criteria.state" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option selected>Arkansas</option>
</select>
</div>
<div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<input
type="text"
id="city"
[(ngModel)]="criteria.city"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Houston"
/>
</div>
<div>
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
<div class="flex items-center space-x-2">
<input
type="number"
id="price-from"
[(ngModel)]="criteria.minPrice"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="From"
/>
<span>-</span>
<input
type="number"
id="price-to"
[(ngModel)]="criteria.maxPrice"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="To"
/>
</div>
</div>
<div>
<label for="salesRevenue" class="block mb-2 text-sm font-medium text-gray-900">Sales Revenue</label>
<div class="flex items-center space-x-2">
<input
type="number"
id="salesRevenue-from"
[(ngModel)]="criteria.minRevenue"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="From"
/>
<span>-</span>
<input
type="number"
id="salesRevenue-to"
[(ngModel)]="criteria.maxRevenue"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="To"
/>
</div>
</div>
<div>
<label for="cashflow" class="block mb-2 text-sm font-medium text-gray-900">Cashflow</label>
<div class="flex items-center space-x-2">
<input
type="number"
id="cashflow-from"
[(ngModel)]="criteria.minCashFlow"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="From"
/>
<span>-</span>
<input
type="number"
id="cashflow-to"
[(ngModel)]="criteria.maxCashFlow"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="To"
/>
</div>
</div>
<div>
<label for="title" class="block mb-2 text-sm font-medium text-gray-900">Title / Description (Free Search)</label>
<input
type="text"
id="title"
[(ngModel)]="criteria.title"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Restaurant"
/>
</div>
<div>
<label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Broker Name / Company Name</label>
<input
type="text"
id="brokername"
[(ngModel)]="criteria.brokerName"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Brokers Invest"
/>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block mb-2 text-sm font-medium text-gray-900">Category</label>
<div class="grid grid-cols-2 gap-2">
@for(tob of selectOptions.typesOfBusiness; track tob){
<div class="flex items-center">
<input
type="checkbox"
id="automotive"
[ngModel]="isTypeOfBusinessClicked(tob)"
(ngModelChange)="categoryClicked($event, tob.value)"
value="{{ tob.value }}"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label for="automotive" class="ml-2 text-sm font-medium text-gray-900">{{ tob.name }}</label>
</div>
}
</div>
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-900">Type of Property</label>
<div class="space-y-2">
<div class="flex items-center">
<input [(ngModel)]="criteria.realEstateChecked" type="radio" id="realEstateChecked" name="wbs" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500" checked />
<label for="realEstateChecked" class="ml-2 text-sm font-medium text-gray-900">Real Estate</label>
</div>
<div class="flex items-center">
<input [(ngModel)]="criteria.leasedLocation" type="radio" id="leasedLocation" name="wbs" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500" />
<label for="leasedLocation" class="ml-2 text-sm font-medium text-gray-900">Leased Location</label>
</div>
<div class="flex items-center">
<input [(ngModel)]="criteria.franchiseResale" type="radio" id="franchiseResale" name="wbs" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500" />
<label for="franchiseResale" class="ml-2 text-sm font-medium text-gray-900">Franchise</label>
</div>
</div>
</div>
<div>
<label for="numberEmployees" class="block mb-2 text-sm font-medium text-gray-900">Number of Employees</label>
<div class="flex items-center space-x-2">
<input
type="number"
id="numberEmployees-from"
[(ngModel)]="criteria.minNumberEmployees"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="From"
/>
<span>-</span>
<input
type="number"
id="numberEmployees-to"
[(ngModel)]="criteria.maxNumberEmployees"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="To"
/>
</div>
</div>
<div>
<label for="establishedSince" class="block mb-2 text-sm font-medium text-gray-900">Established Since</label>
<div class="flex items-center space-x-2">
<input
type="number"
id="establishedSince-From"
[(ngModel)]="criteria.establishedSince"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="YYYY"
/>
<span>-</span>
<input
type="number"
id="establishedSince-To"
[(ngModel)]="criteria.establishedUntil"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="YYYY"
/>
</div>
</div>
</div>
</div>
} @if(criteria.criteriaType==='commercialProperty'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
<select id="state" [(ngModel)]="criteria.state" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option selected>Arkansas</option>
</select>
</div>
<div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<input
type="text"
id="city"
[(ngModel)]="criteria.city"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Houston"
/>
</div>
<div>
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
<div class="flex items-center space-x-2">
<input
type="number"
id="price-from"
[(ngModel)]="criteria.minPrice"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="From"
/>
<span>-</span>
<input
type="number"
id="price-to"
[(ngModel)]="criteria.maxPrice"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-1/2 p-2.5"
placeholder="To"
/>
</div>
</div>
<div>
<label for="title" class="block mb-2 text-sm font-medium text-gray-900">Title / Description (Free Search)</label>
<input
type="text"
id="title"
[(ngModel)]="criteria.title"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Restaurant"
/>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block mb-2 text-sm font-medium text-gray-900">Category</label>
<div class="grid grid-cols-2 gap-2">
@for(tob of selectOptions.typesOfCommercialProperty; track tob){
<div class="flex items-center">
<input
type="checkbox"
id="automotive"
[ngModel]="isTypeOfBusinessClicked(tob)"
(ngModelChange)="categoryClicked($event, tob.value)"
value="{{ tob.value }}"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label for="automotive" class="ml-2 text-sm font-medium text-gray-900">{{ tob.name }}</label>
</div>
}
</div>
</div>
</div>
</div>
} @if(criteria.criteriaType==='user'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
<label for="states" class="block mb-2 text-sm font-medium text-gray-900">Locations served - States</label>
<select id="states" [(ngModel)]="criteria.states" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option selected>Arkansas</option>
</select>
</div>
<div>
<label for="counties" class="block mb-2 text-sm font-medium text-gray-900">Locations served - Counties</label>
<select id="counties" [(ngModel)]="criteria.counties" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
<option selected>Arkansas</option>
</select>
</div>
<div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<input
type="text"
id="city"
[(ngModel)]="criteria.city"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
placeholder="e.g. Houston"
/>
</div>
</div>
<div class="space-y-4">
<div>
<label class="block mb-2 text-sm font-medium text-gray-900">Category</label>
<div class="grid grid-cols-2 gap-2">
@for(tob of selectOptions.customerSubTypes; track tob){
<div class="flex items-center">
<input
type="checkbox"
id="automotive"
[ngModel]="isTypeOfProfessionalClicked(tob)"
(ngModelChange)="categoryClicked($event, tob.value)"
value="{{ tob.value }}"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<label for="automotive" class="ml-2 text-sm font-medium text-gray-900">{{ tob.name }}</label>
</div>
}
</div>
</div>
</div>
</div>
}
</div>
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b">
<button type="button" (click)="modalService.accept()" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center">
Search
</button>
<button
type="button"
(click)="modalService.reject()"
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10"
>
Cancel
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,46 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { SelectOptionsService } from '../../services/select-options.service';
import { SharedModule } from '../../shared/shared/shared.module';
import { ModalService } from './modal.service';
@Component({
selector: 'app-search-modal',
standalone: true,
imports: [SharedModule, AsyncPipe, NgIf],
templateUrl: './search-modal.component.html',
})
export class SearchModalComponent {
constructor(public selectOptions: SelectOptionsService, public modalService: ModalService) {}
ngOnInit() {
this.modalService.message$.subscribe(msg => {
this.criteria = msg;
});
}
public criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
categoryClicked(checked: boolean, value: string) {
if (checked) {
this.criteria.types.push(value);
} else {
const index = this.criteria.types.findIndex(t => t === value);
if (index > -1) {
this.criteria.types.splice(index, 1);
}
}
}
search() {
console.log('Search criteria:', this.criteria);
}
closeModal() {
console.log('Closing modal');
}
isTypeOfBusinessClicked(v: KeyValueStyle) {
return this.criteria.types.find(t => t === v.value);
}
isTypeOfProfessionalClicked(v: KeyValue) {
return this.criteria.types.find(t => t === v.value);
}
}

View File

@@ -5,7 +5,7 @@ import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { lastValueFrom } from 'rxjs';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { BusinessListingCriteria, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ListingsService } from '../../../services/listings.service';
@@ -43,7 +43,7 @@ export class DetailsBusinessListingComponent {
];
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
listing: BusinessListing;
criteria: ListingCriteria;
criteria: BusinessListingCriteria;
mailinfo: MailInfo;
environment = environment;
keycloakUser: KeycloakUser;
@@ -70,7 +70,7 @@ export class DetailsBusinessListingComponent {
}
});
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandler);
}
async ngOnInit() {

View File

@@ -6,7 +6,7 @@ import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { lastValueFrom } from 'rxjs';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { CommercialPropertyListingCriteria, ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.service';
@@ -45,7 +45,7 @@ export class DetailsCommercialPropertyListingComponent {
];
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
listing: CommercialPropertyListing;
criteria: ListingCriteria;
criteria: CommercialPropertyListingCriteria;
mailinfo: MailInfo;
environment = environment;
keycloakUser: KeycloakUser;
@@ -73,7 +73,7 @@ export class DetailsCommercialPropertyListingComponent {
) {
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.criteria = onChange(getCriteriaStateObject('commercialProperty'), getSessionStorageHandler);
}
async ngOnInit() {

View File

@@ -4,7 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import { Observable } from 'rxjs';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser, ListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { KeycloakUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.service';
@@ -27,7 +27,6 @@ export class DetailsUserComponent {
user$: Observable<KeycloakUser>;
keycloakUser: KeycloakUser;
environment = environment;
criteria: ListingCriteria;
businessListings: BusinessListing[];
commercialPropListings: CommercialPropertyListing[];
companyOverview: SafeHtml;

View File

@@ -4,10 +4,10 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { KeycloakUser, ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { ListingsService } from '../../services/listings.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { getCriteriaStateObject, getSessionStorageHandler, map2User, resetCriteria } from '../../utils/utils';
import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../../utils/utils';
@Component({
selector: 'app-home',
standalone: true,
@@ -20,14 +20,13 @@ export class HomeComponent {
type: string;
maxPrice: string;
minPrice: string;
criteria: ListingCriteria;
criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
states = [];
isMenuOpen = false;
user: KeycloakUser;
prompt: string;
public constructor(private router: Router, private activatedRoute: ActivatedRoute, public selectOptions: SelectOptionsService, public keycloakService: KeycloakService, private listingsService: ListingsService) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
resetCriteria(this.criteria);
this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandler);
}
async ngOnInit() {
const token = await this.keycloakService.getToken();

View File

@@ -4,13 +4,13 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import onChange from 'on-change';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { ListingType, UserListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../../utils/utils';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
@Component({
selector: 'app-broker-listings',
@@ -24,7 +24,7 @@ export class BrokerListingsComponent {
listings: Array<BusinessListing>;
users: Array<User>;
filteredListings: Array<ListingType>;
criteria: ListingCriteria;
criteria: UserListingCriteria;
realEstateChecked: boolean;
maxPrice: string;
minPrice: string;
@@ -49,10 +49,9 @@ export class BrokerListingsComponent {
private imageService: ImageService,
private route: ActivatedRoute,
) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.criteria = onChange(getCriteriaStateObject('user'), getSessionStorageHandler);
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
resetCriteria(this.criteria);
} else {
this.first = this.criteria.page * this.criteria.length;
this.rows = this.criteria.length;
@@ -86,7 +85,5 @@ export class BrokerListingsComponent {
this.criteria.pageCount = event.pageCount;
this.search();
}
reset() {
this.criteria.name = '';
}
reset() {}
}

View File

@@ -4,12 +4,12 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import onChange from 'on-change';
import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { BusinessListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../../utils/utils';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
@Component({
selector: 'app-business-listings',
@@ -22,7 +22,7 @@ export class BusinessListingsComponent {
environment = environment;
listings: Array<BusinessListing>;
filteredListings: Array<BusinessListing>;
criteria: ListingCriteria;
criteria: BusinessListingCriteria;
realEstateChecked: boolean;
maxPrice: string;
minPrice: string;
@@ -45,10 +45,9 @@ export class BusinessListingsComponent {
private imageService: ImageService,
private route: ActivatedRoute,
) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandler);
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
resetCriteria(this.criteria);
} else {
this.first = this.criteria.page * this.criteria.length;
this.rows = this.criteria.length;
@@ -70,12 +69,12 @@ export class BusinessListingsComponent {
this.search();
}
async search() {
this.listings = await this.listingsService.getListingsByPrompt(this.criteria);
// const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
// this.listings = listingReponse.data;
// this.totalRecords = listingReponse.total;
// this.cdRef.markForCheck();
// this.cdRef.detectChanges();
//this.listings = await this.listingsService.getListingsByPrompt(this.criteria);
const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
this.listings = listingReponse.data;
this.totalRecords = listingReponse.total;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(event: any) {
this.criteria.start = event.first;

View File

@@ -4,12 +4,12 @@ import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import onChange from 'on-change';
import { CommercialPropertyListing } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model';
import { CommercialPropertyListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../../utils/utils';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
@Component({
selector: 'app-commercial-property-listings',
@@ -22,7 +22,7 @@ export class CommercialPropertyListingsComponent {
environment = environment;
listings: Array<CommercialPropertyListing>;
filteredListings: Array<CommercialPropertyListing>;
criteria: ListingCriteria;
criteria: CommercialPropertyListingCriteria;
realEstateChecked: boolean;
first: number = 0;
rows: number = 12;
@@ -44,10 +44,9 @@ export class CommercialPropertyListingsComponent {
private imageService: ImageService,
private route: ActivatedRoute,
) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.criteria = onChange(getCriteriaStateObject('commercialProperty'), getSessionStorageHandler);
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
resetCriteria(this.criteria);
} else {
this.first = this.criteria.page * this.criteria.length;
this.rows = this.criteria.length;

View File

@@ -2,7 +2,14 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, lastValueFrom } from 'rxjs';
import { BusinessListing } from '../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, ListingType, ResponseBusinessListingArray, ResponseCommercialPropertyListingArray, StatesResult } from '../../../../bizmatch-server/src/models/main.model';
import {
BusinessListingCriteria,
CommercialPropertyListingCriteria,
ListingType,
ResponseBusinessListingArray,
ResponseCommercialPropertyListingArray,
StatesResult,
} from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
@@ -12,11 +19,14 @@ export class ListingsService {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {}
async getListings(criteria: ListingCriteria, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty'): Promise<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray> {
async getListings(
criteria: BusinessListingCriteria | CommercialPropertyListingCriteria,
listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty',
): Promise<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray> {
const result = await lastValueFrom(this.http.post<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/find`, criteria));
return result;
}
async getListingsByPrompt(criteria: ListingCriteria): Promise<BusinessListing[]> {
async getListingsByPrompt(criteria: BusinessListingCriteria | CommercialPropertyListingCriteria): Promise<BusinessListing[]> {
const result = await lastValueFrom(this.http.post<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/business/search`, criteria));
return result;
}

View File

@@ -39,11 +39,11 @@ export class SelectOptionsService {
getState(value: string): string {
return this.states.find(l => l.value === value)?.name;
}
getBusiness(value: number): string {
return this.typesOfBusiness.find(t => t.value === String(value))?.name;
getBusiness(value: string): string {
return this.typesOfBusiness.find(t => t.value === value)?.name;
}
getCommercialProperty(value: number): string {
return this.typesOfCommercialProperty.find(t => t.value === String(value))?.name;
getCommercialProperty(value: string): string {
return this.typesOfCommercialProperty.find(t => t.value === value)?.name;
}
getListingsCategory(value: string): string {
return this.listingCategories.find(l => l.value === value)?.name;
@@ -57,14 +57,14 @@ export class SelectOptionsService {
getIconType(value: string): string {
return this.typesOfBusiness.find(c => c.value === value)?.icon;
}
getTextColorType(value: number): string {
return this.typesOfBusiness.find(c => c.value === String(value))?.textColorClass;
getTextColorType(value: string): string {
return this.typesOfBusiness.find(c => c.value === value)?.textColorClass;
}
getIconAndTextColorType(value: number): string {
const category = this.typesOfBusiness.find(c => c.value === String(value));
getIconAndTextColorType(value: string): string {
const category = this.typesOfBusiness.find(c => c.value === value);
return `${category?.icon} ${category?.textColorClass}`;
}
getIconTypeOfCommercials(value: number): string {
return this.typesOfCommercialProperty.find(c => c.value === String(value))?.icon;
getIconTypeOfCommercials(value: string): string {
return this.typesOfCommercialProperty.find(c => c.value === value)?.icon;
}
}

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import urlcat from 'urlcat';
import { User } from '../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, ResponseUsersArray, StatesResult } from '../../../../bizmatch-server/src/models/main.model';
import { ResponseUsersArray, StatesResult, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
@@ -27,7 +27,7 @@ export class UserService {
const url = urlcat(`${this.apiBaseUrl}/bizmatch/user`, { mail });
return await lastValueFrom(this.http.get<User>(url));
}
async search(criteria?: ListingCriteria): Promise<ResponseUsersArray> {
async search(criteria?: UserListingCriteria): Promise<ResponseUsersArray> {
return await lastValueFrom(this.http.post<ResponseUsersArray>(`${this.apiBaseUrl}/bizmatch/user/search`, criteria));
}
async getAllStates(): Promise<any> {

View File

@@ -2,7 +2,7 @@ import { Router } from '@angular/router';
import { ConsoleFormattedStream, INFO, createLogger as _createLogger, stdSerializers } from 'browser-bunyan';
import { jwtDecode } from 'jwt-decode';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../bizmatch-server/src/models/db.model';
import { JwtToken, KeycloakUser, ListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, JwtToken, KeycloakUser, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
export function createDefaultUser(email: string, firstname: string, lastname: string): User {
return {
@@ -82,21 +82,69 @@ export function createDefaultBusinessListing(): BusinessListing {
listingsCategory: 'business',
};
}
export function createDefaultListingCriteria(): ListingCriteria {
export function createEmptyBusinessListingCriteria(): BusinessListingCriteria {
return {
start: 0,
length: 12,
length: 0,
page: 0,
pageCount: 0,
type: 0,
state: '',
city: '',
types: [],
prompt: '',
criteriaType: 'business',
county: '',
minPrice: null,
maxPrice: null,
minRevenue: null,
maxRevenue: null,
minCashFlow: null,
maxCashFlow: null,
minNumberEmployees: null,
maxNumberEmployees: null,
establishedSince: null,
establishedUntil: null,
realEstateChecked: false,
leasedLocation: false,
franchiseResale: false,
title: '',
brokerName: '',
};
}
export function createEmptyCommercialPropertyListingCriteria(): CommercialPropertyListingCriteria {
return {
start: 0,
length: 0,
page: 0,
pageCount: 0,
state: '',
city: '',
types: [],
prompt: '',
criteriaType: 'commercialProperty',
county: '',
minPrice: 0,
maxPrice: 0,
realEstateChecked: false,
title: '',
category: 'broker',
name: '',
};
}
export function createEmptyUserListingCriteria(): UserListingCriteria {
return {
start: 0,
length: 0,
page: 0,
pageCount: 0,
city: '',
types: [],
prompt: '',
criteriaType: 'user',
firstname: '',
lastname: '',
companyName: '',
counties: [],
states: [],
};
}
export function createLogger(name: string, level: number = INFO, options: any = {}) {
@@ -120,8 +168,15 @@ export const getSessionStorageHandler = function (path, value, previous, applyDa
sessionStorage.setItem('criteria', JSON.stringify(this));
};
export function getCriteriaStateObject() {
const initialState = createDefaultListingCriteria();
export function getCriteriaStateObject(criteriaType: 'business' | 'commercialProperty' | 'user') {
let initialState;
if (criteriaType === 'business') {
initialState = createEmptyBusinessListingCriteria();
} else if (criteriaType === 'commercialProperty') {
initialState = createEmptyCommercialPropertyListingCriteria();
} else {
initialState = createEmptyUserListingCriteria();
}
const storedState = sessionStorage.getItem('criteria');
return storedState ? JSON.parse(storedState) : initialState;
}
@@ -134,17 +189,6 @@ export function routeListingWithState(router: Router, value: string, data: any)
}
}
export function resetCriteria(criteria: ListingCriteria) {
criteria.type = null;
criteria.state = null;
criteria.minPrice = null;
criteria.maxPrice = null;
criteria.start = 0;
criteria.length = 12;
criteria.realEstateChecked = null;
criteria.title = null;
criteria.name = null;
}
export function map2User(jwt: string): KeycloakUser {
if (jwt) {
const token = jwtDecode<JwtToken>(jwt);