Umbau Filter linksseitig

This commit is contained in:
Andreas Knuth 2025-07-20 17:35:45 -05:00
parent 466e1dcdce
commit 24db8927e8
40 changed files with 894 additions and 626 deletions

View File

@ -31,6 +31,7 @@ export function createEmptyBusinessListingCriteria(): BusinessListingCriteria {
leasedLocation: false,
franchiseResale: false,
title: '',
email: 'bizmatch@proton.me',
brokerName: '',
searchType: 'exact',
radius: null,
@ -61,6 +62,7 @@ export function resetBusinessListingCriteria(criteria: BusinessListingCriteria)
criteria.leasedLocation = false;
criteria.franchiseResale = false;
criteria.title = '';
criteria.email = 'bizmatch@proton.me';
criteria.brokerName = '';
criteria.searchType = 'exact';
criteria.radius = null;

View File

@ -103,6 +103,9 @@ export class BusinessListingService {
whereConditions.push(and(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
}
}
if (criteria.email) {
whereConditions.push(eq(schema.users.email, criteria.email));
}
if (user?.role !== 'admin') {
whereConditions.push(or(eq(businesses.email, user?.email), ne(businesses.draft, true)));
}

View File

@ -91,6 +91,7 @@ export interface BusinessListingCriteria extends ListCriteria {
franchiseResale: boolean;
title: string;
brokerName: string;
email: string;
criteriaType: 'businessListings';
}
export interface CommercialPropertyListingCriteria extends ListCriteria {

View File

@ -27,6 +27,10 @@
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
},
"src/favicon.ico",
"src/assets"
],

View File

@ -3,7 +3,7 @@
"version": "0.0.1",
"scripts": {
"ng": "ng",
"start": "ng serve --port=4300 --host 0.0.0.0 & http-server ../bizmatch-server",
"start": "ng serve --host 0.0.0.0 & http-server ../bizmatch-server",
"prebuild": "node version.js",
"build": "node version.js && ng build",
"build.dev": "node version.js && ng build --configuration dev --output-hashing=all",

View File

@ -3,10 +3,16 @@
@if (actualRoute !=='home' && actualRoute !=='login' && actualRoute!=='emailVerification' && actualRoute!=='email-authorized'){
<header></header>
}
<main class="flex-1">
<main class="flex-1 flex">
@if (isFilterRoute()) {
<div class="hidden md:block w-1/4 bg-white shadow-lg p-6 overflow-y-auto">
<app-search-modal [isModal]="false"></app-search-modal>
</div>
}
<div [ngClass]="{ 'w-full': !isFilterRoute(), 'md:w-3/4': isFilterRoute() }">
<router-outlet></router-outlet>
</div>
</main>
<app-footer></app-footer>
</div>

View File

@ -1,25 +1,3 @@
// .progress-spinner {
// position: fixed;
// z-index: 999;
// top: 0;
// left: 0;
// bottom: 0;
// right: 0;
// display: flex;
// flex-direction: column;
// align-items: center;
// }
// .progress-spinner:before {
// content: '';
// display: block;
// position: fixed;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// background-color: rgba(0, 0, 0, 0.3);
// }
.spinner-text {
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */

View File

@ -47,42 +47,18 @@ export class AppComponent {
this.actualRoute = currentRoute.snapshot.url[0].path;
});
}
ngOnInit() {
// this.keycloakService.keycloakEvents$.subscribe({
// next: event => {
// if (event.type === KeycloakEventType.OnTokenExpired) {
// this.handleTokenExpiration();
// }
// },
// });
}
// private async handleTokenExpiration(): Promise<void> {
// try {
// // Versuche, den Token zu erneuern
// const refreshed = await this.keycloakService.updateToken();
// if (!refreshed) {
// // Wenn der Token nicht erneuert werden kann, leite zur Login-Seite weiter
// this.keycloakService.login({
// redirectUri: window.location.href, // oder eine andere Seite
// });
// }
// } catch (error) {
// if (error.error === 'invalid_grant' && error.error_description === 'Token is not active') {
// // Hier wird der Fehler "invalid_grant" abgefangen
// this.keycloakService.login({
// redirectUri: window.location.href,
// });
// }
// }
// }
ngOnInit() {}
@HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
this.showVersionDialog();
}
}
showVersionDialog() {
this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' });
}
isFilterRoute(): boolean {
const filterRoutes = ['/businessListings', '/commercialPropertyListings', '/brokerListings'];
return filterRoutes.includes(this.actualRoute);
}
}

View File

@ -6,7 +6,7 @@
<div class="flex items-center md:order-2 space-x-3 rtl:space-x-reverse">
<!-- Filter button -->
@if(isFilterUrl()){
<button
<!-- <button
type="button"
#triggerButton
(click)="openModal()"
@ -14,7 +14,7 @@
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 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
</button>
</button> -->
<!-- Sort button -->
<div class="relative">
<button
@ -217,14 +217,14 @@
</div>
<!-- Mobile filter button -->
<div class="md:hidden flex justify-center pb-4">
<button
<!-- <button
(click)="openModal()"
type="button"
id="filterDropdownMobileButton"
class="w-full mx-4 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: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"
>
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
</button>
</button> -->
<!-- Sorting -->
<button
(click)="toggleSortDropdown()"

View File

@ -21,7 +21,12 @@ export class ModalService {
this.resolvePromise = resolve;
});
}
sendCriteria(message: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): Promise<ModalResult> {
this.messageSubject.next(message);
return new Promise<ModalResult>(resolve => {
this.resolvePromise = resolve;
});
}
accept(): void {
this.modalVisibleSubject.next(false);
this.resolvePromise({ accepted: true });

View File

@ -1,10 +1,10 @@
<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 *ngIf="isModal && (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 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">
@if(criteria.criteriaType==='businessListings'){
@if(criteria.criteriaType==='businessListings') {
<h3 class="text-xl font-semibold text-gray-900">Business Listing Search</h3>
} @else if (criteria.criteriaType==='commercialPropertyListings'){
} @else if (criteria.criteriaType==='commercialPropertyListings') {
<h3 class="text-xl font-semibold text-gray-900">Property Listing Search</h3>
} @else {
<h3 class="text-xl font-semibold text-gray-900">Professional Listing Search</h3>
@ -18,59 +18,38 @@
</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> -->
<button class="text-blue-600 font-medium border-b-2 border-blue-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-blue-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-gray-900 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 tooltip">
Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
</div>
@if(criteria.criteriaType==='businessListings'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
@if(criteria){
<div class="space-y-6">
@if(criteria.criteriaType==='businessListings') {
<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-gray-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>
<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-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
</div>
<!-- <div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
[ngModel]="criteria.city"
(ngModelChange)="setCity($event)"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option>
}
</ng-select>
</div> -->
<!-- New section for city search type -->
<div *ngIf="criteria.city">
<label class="block mb-2 text-sm font-medium text-gray-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" value="exact" />
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" (ngModelChange)="onCriteriaChange()" 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" value="radius" />
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" (ngModelChange)="onCriteriaChange()" value="radius" />
<span class="ml-2">Radius Search</span>
</label>
</div>
</div>
<!-- New section for radius selection -->
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
<div class="flex flex-wrap">
@ -86,71 +65,34 @@
}
</div>
</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">
<app-validated-price name="price-from" [(ngModel)]="criteria.minPrice" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<!-- <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"
/> -->
<app-validated-price name="price-from" [(ngModel)]="criteria.minPrice" (ngModelChange)="onCriteriaChange()" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<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"
/> -->
<app-validated-price name="price-to" [(ngModel)]="criteria.maxPrice" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<app-validated-price name="price-to" [(ngModel)]="criteria.maxPrice" (ngModelChange)="onCriteriaChange()" placeholder="To" inputClasses="bg-gray-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-gray-900">Sales Revenue</label>
<div class="flex items-center space-x-2">
<!-- <input
type="number"
id="salesRevenue-from"
<app-validated-price
name="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"
(ngModelChange)="onCriteriaChange()"
placeholder="From"
/> -->
<app-validated-price name="salesRevenue-from" [(ngModel)]="criteria.minRevenue" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"
></app-validated-price>
<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"
/> -->
<app-validated-price name="salesRevenue-to" [(ngModel)]="criteria.maxRevenue" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<app-validated-price name="salesRevenue-to" [(ngModel)]="criteria.maxRevenue" (ngModelChange)="onCriteriaChange()" placeholder="To" inputClasses="bg-gray-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-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"
/> -->
<app-validated-price name="cashflow-from" [(ngModel)]="criteria.minCashFlow" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<app-validated-price name="cashflow-from" [(ngModel)]="criteria.minCashFlow" (ngModelChange)="onCriteriaChange()" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<span>-</span>
<app-validated-price name="cashflow-to" [(ngModel)]="criteria.maxCashFlow" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<!-- <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"
/> -->
<app-validated-price name="cashflow-to" [(ngModel)]="criteria.maxCashFlow" (ngModelChange)="onCriteriaChange()" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
</div>
</div>
<div>
@ -159,16 +101,15 @@
type="text"
id="title"
[(ngModel)]="criteria.title"
(ngModelChange)="onCriteriaChange()"
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.typesOfBusiness; track tob){
@for(tob of selectOptions.typesOfBusiness; track tob) {
<div class="flex items-center">
<input
type="checkbox"
@ -225,6 +166,7 @@
type="number"
id="numberEmployees-from"
[(ngModel)]="criteria.minNumberEmployees"
(ngModelChange)="onCriteriaChange()"
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"
/>
@ -233,6 +175,7 @@
type="number"
id="numberEmployees-to"
[(ngModel)]="criteria.maxNumberEmployees"
(ngModelChange)="onCriteriaChange()"
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"
/>
@ -245,6 +188,7 @@
type="number"
id="establishedSince-From"
[(ngModel)]="criteria.establishedSince"
(ngModelChange)="onCriteriaChange()"
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"
/>
@ -253,6 +197,7 @@
type="number"
id="establishedSince-To"
[(ngModel)]="criteria.establishedUntil"
(ngModelChange)="onCriteriaChange()"
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"
/>
@ -264,40 +209,23 @@
type="text"
id="brokername"
[(ngModel)]="criteria.brokerName"
(ngModelChange)="onCriteriaChange()"
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>
} @if(criteria.criteriaType==='commercialPropertyListings'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
} @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-gray-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>
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"></ng-select>
</div>
<div>
<!-- <label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
[ngModel]="criteria.city"
(ngModelChange)="setCity($event)"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
}
</ng-select> -->
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
</div>
<!-- New section for city search type -->
<div *ngIf="criteria.city">
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
<div class="flex items-center space-x-4">
@ -311,7 +239,6 @@
</label>
</div>
</div>
<!-- New section for radius selection -->
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
<div class="flex flex-wrap">
@ -330,23 +257,9 @@
<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"
/> -->
<app-validated-price name="price-from" [(ngModel)]="criteria.minPrice" 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" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<!-- <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>
@ -359,12 +272,10 @@
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){
@for(tob of selectOptions.typesOfCommercialProperty; track tob) {
<div class="flex items-center">
<input
type="checkbox"
@ -381,12 +292,12 @@
</div>
</div>
</div>
} @if(criteria.criteriaType==='brokerListings'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
} @if(criteria.criteriaType==='brokerListings') {
<div class="grid grid-cols-1 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>
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state" [multiple]="false"> </ng-select>
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state" [multiple]="false"></ng-select>
</div>
<div>
<label for="counties" class="block mb-2 text-sm font-medium text-gray-900">Locations served - Counties</label>
@ -402,27 +313,9 @@
typeToSearchText="Please enter 2 or more characters"
[typeahead]="countyInput$"
[(ngModel)]="criteria.counties"
>
</ng-select>
></ng-select>
</div>
<div>
<!-- <label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
[ngModel]="criteria.city"
(ngModelChange)="setCity($event)"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.name }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
}
</ng-select> -->
<app-validated-city
label="Company Location - City"
name="city"
@ -432,7 +325,6 @@
[state]="criteria.state"
></app-validated-city>
</div>
<!-- New section for city search type -->
<div *ngIf="criteria.city">
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
<div class="flex items-center space-x-4">
@ -446,16 +338,6 @@
</label>
</div>
</div>
<div>
<label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Name of Professional</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"
/>
</div>
<!-- New section for radius selection -->
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
<div class="flex flex-wrap">
@ -471,28 +353,19 @@
}
</div>
</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">
<label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Name of Professional</label>
<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"
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"
/>
<label for="automotive" class="ml-2 text-sm font-medium text-gray-900">{{ tob.name }}</label>
</div>
</div>
</div>
}
</div>
</div>
</div>
</div>
}
</div>
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b">
@ -510,3 +383,331 @@
</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-gray-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-blue-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-gray-900 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 tooltip">
Clear all Filter
<div class="tooltip-arrow" data-popper-arrow></div>
</div>
</div>
@if(criteria.criteriaType==='businessListings') {
<div class="space-y-4">
<div>
<label for="state" class="block mb-2 text-sm font-medium text-gray-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-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
</div>
<div *ngIf="criteria.city">
<label class="block mb-2 text-sm font-medium text-gray-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" (ngModelChange)="onCriteriaChange()" [(ngModel)]="criteria.searchType" value="exact" />
<span class="ml-2">Exact City</span>
</label>
<label class="inline-flex items-center">
<input type="radio" class="form-radio" name="searchType" (ngModelChange)="onCriteriaChange()" [(ngModel)]="criteria.searchType" 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-gray-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-gray-200 hover:bg-gray-500 hover:text-white"
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-gray-900 bg-white'"
(click)="setRadius(radius)"
>
{{ radius }}
</button>
}
</div>
</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">
<app-validated-price name="price-from" (ngModelChange)="onCriteriaChange()" [(ngModel)]="criteria.minPrice" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<span>-</span>
<app-validated-price name="price-to" (ngModelChange)="onCriteriaChange()" [(ngModel)]="criteria.maxPrice" placeholder="To" inputClasses="bg-gray-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-gray-900">Sales Revenue</label>
<div class="flex items-center space-x-2">
<app-validated-price name="salesRevenue-from" (ngModelChange)="onCriteriaChange()" [(ngModel)]="criteria.minRevenue" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<span>-</span>
<app-validated-price name="salesRevenue-to" (ngModelChange)="onCriteriaChange()" [(ngModel)]="criteria.maxRevenue" placeholder="To" inputClasses="bg-gray-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-gray-900">Cashflow</label>
<div class="flex items-center space-x-2">
<app-validated-price name="cashflow-from" (ngModelChange)="onCriteriaChange()" [(ngModel)]="criteria.minCashFlow" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
<span>-</span>
<app-validated-price name="cashflow-to" (ngModelChange)="onCriteriaChange()" [(ngModel)]="criteria.maxCashFlow" 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-gray-900">Title / Description (Free Search)</label>
<input
type="text"
id="title"
[(ngModel)]="criteria.title"
(ngModelChange)="onCriteriaChange()"
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 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"
(ngModelChange)="onCheckboxChange('realEstateChecked', $event)"
type="checkbox"
name="realEstateChecked"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
/>
<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"
(ngModelChange)="onCheckboxChange('leasedLocation', $event)"
type="checkbox"
name="leasedLocation"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded 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"
(ngModelChange)="onCheckboxChange('franchiseResale', $event)"
type="checkbox"
name="franchiseResale"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded 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"
(ngModelChange)="onCriteriaChange()"
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"
(ngModelChange)="onCriteriaChange()"
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"
(ngModelChange)="onCriteriaChange()"
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"
(ngModelChange)="onCriteriaChange()"
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>
<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>
} @if(criteria.criteriaType==='commercialPropertyListings') {
<div class="space-y-4">
<div>
<label for="state" class="block mb-2 text-sm font-medium text-gray-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-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
</div>
<div *ngIf="criteria.city">
<label class="block mb-2 text-sm font-medium text-gray-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" 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" 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-gray-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-gray-200 hover:bg-gray-500 hover:text-white"
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-gray-900 bg-white'"
(click)="criteria.radius = radius"
>
{{ radius }}
</button>
}
</div>
</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">
<app-validated-price name="price-from" [(ngModel)]="criteria.minPrice" 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" 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-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 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>
} @if(criteria.criteriaType==='brokerListings') {
<div class="space-y-4">
<div>
<label for="states" class="block mb-2 text-sm font-medium text-gray-900">Locations served - States</label>
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state" [multiple]="false"></ng-select>
</div>
<div>
<label for="counties" class="block mb-2 text-sm font-medium text-gray-900">Locations served - Counties</label>
<ng-select
[items]="counties$ | async"
bindLabel="name"
class="custom"
[multiple]="true"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="countyLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="countyInput$"
[(ngModel)]="criteria.counties"
></ng-select>
</div>
<div>
<app-validated-city label="Company Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
</div>
<div *ngIf="criteria.city">
<label class="block mb-2 text-sm font-medium text-gray-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" 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" 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-gray-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-gray-200 hover:bg-gray-500 hover:text-white"
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-gray-900 bg-white'"
(click)="criteria.radius = radius"
>
{{ radius }}
</button>
}
</div>
</div>
<div>
<label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Name of Professional</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" />
</div>
</div>
}
<!-- <div class="flex items-center space-x-2">
<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 ({{ numberOfResults$ | async }})
</button>
</div> -->
</div>

View File

@ -1,5 +1,5 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { Component, Input, Output } 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, Subscription, switchMap, tap } from 'rxjs';
@ -7,10 +7,11 @@ import { BusinessListingCriteria, CommercialPropertyListingCriteria, CountyResul
import { CriteriaChangeService } from '../../services/criteria-change.service';
import { GeoService } from '../../services/geo.service';
import { ListingsService } from '../../services/listings.service';
import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { SharedModule } from '../../shared/shared/shared.module';
import { resetBusinessListingCriteria, resetCommercialPropertyListingCriteria, resetUserListingCriteria } from '../../utils/utils';
import { getCriteriaStateObject, resetBusinessListingCriteria, resetCommercialPropertyListingCriteria, resetUserListingCriteria } from '../../utils/utils';
import { ValidatedCityComponent } from '../validated-city/validated-city.component';
import { ValidatedPriceComponent } from '../validated-price/validated-price.component';
import { ModalService } from './modal.service';
@ -23,6 +24,9 @@ import { ModalService } from './modal.service';
styleUrl: './search-modal.component.scss',
})
export class SearchModalComponent {
@Output()
@Input()
isModal: boolean = true;
// cities$: Observable<GeoResult[]>;
counties$: Observable<CountyResult[]>;
// cityLoading = false;
@ -31,7 +35,8 @@ export class SearchModalComponent {
countyInput$ = new Subject<string>();
private criteriaChangeSubscription: Subscription;
public criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
public backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria = getCriteriaStateObject('businessListings');
numberOfResults$: Observable<number>;
cancelDisable = false;
constructor(
@ -41,9 +46,11 @@ export class SearchModalComponent {
private criteriaChangeService: CriteriaChangeService,
private listingService: ListingsService,
private userService: UserService,
private searchService: SearchService,
) {}
ngOnInit() {
this.setupCriteriaChangeListener();
this.modalService.message$.pipe(untilDestroyed(this)).subscribe(msg => {
this.criteria = msg;
this.backupCriteria = JSON.parse(JSON.stringify(msg));
@ -69,6 +76,7 @@ export class SearchModalComponent {
this.criteria.types.splice(index, 1);
}
}
this.searchService.search(this.criteria);
}
private loadCounties() {
this.counties$ = concat(
@ -86,6 +94,9 @@ export class SearchModalComponent {
),
);
}
onCriteriaChange() {
this.searchService.search(this.criteria);
}
setCity(city) {
if (city) {
this.criteria.city = city;
@ -95,6 +106,7 @@ export class SearchModalComponent {
this.criteria.radius = null;
this.criteria.searchType = 'exact';
}
this.searchService.search(this.criteria);
}
setState(state: string) {
if (state) {
@ -103,6 +115,11 @@ export class SearchModalComponent {
this.criteria.state = null;
this.setCity(null);
}
this.searchService.search(this.criteria);
}
setRadius(radius: number) {
this.criteria.radius = radius;
this.searchService.search(this.criteria);
}
private setupCriteriaChangeListener() {
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => {
@ -160,5 +177,6 @@ export class SearchModalComponent {
// Aktivieren Sie nur die aktuell ausgewählte Checkbox
this.criteria[checkbox] = value;
this.searchService.search(this.criteria);
}
}

View File

@ -1,26 +1,28 @@
<div class="container mx-auto p-4">
@if(listings?.length>0){
<div class="flex flex-col md:flex-row">
<!-- Filter Panel for Desktop -->
<div class="hidden md:block w-full md:w-1/4 h-full bg-white shadow-lg p-6 overflow-y-auto z-10">
<app-search-modal [isModal]="false"></app-search-modal>
</div>
<!-- Main Content -->
<div class="w-full p-4">
<div class="container mx-auto">
@if(listings?.length > 0) {
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Anzahl der Spalten auf 3 reduziert und den Abstand erhöht -->
@for (listing of listings; track listing.id) {
<div class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg overflow-hidden hover:shadow-xl">
<!-- Hover-Effekt hinzugefügt -->
<div class="p-6 flex flex-col h-full relative z-[0]">
<div class="flex items-center mb-4">
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2 text-xl"></i>
<!-- Icon vergrößert -->
<span [class]="selectOptions.getTextColorType(listing.type)" class="font-bold text-lg">{{ selectOptions.getBusiness(listing.type) }}</span>
<!-- Schriftgröße erhöht -->
</div>
<h2 class="text-xl font-semibold mb-4">
<!-- Überschrift vergrößert -->
{{ listing.title }}
@if(listing.draft){
@if(listing.draft) {
<span class="bg-red-100 text-red-800 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">Draft</span>
}
</h2>
<div class="flex justify-between">
<!-- State Badge -->
<span class="w-fit inline-flex items-center justify-center px-2 py-1 mb-4 text-xs font-bold leading-none bg-gray-200 text-gray-700 rounded-full">
{{ selectOptions.getState(listing.location.state) }}
</span>
@ -28,7 +30,6 @@
<strong>{{ getDaysListed(listing) }} days listed</strong>
</p>
</div>
<!-- Asking Price hervorgehoben -->
<p class="text-base font-bold text-gray-800 mb-2">
<strong>Asking price:</strong> <span class="text-green-600"> {{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</span>
</p>
@ -36,15 +37,12 @@
<p class="text-sm text-gray-600 mb-2"><strong>Net profit:</strong> {{ listing.cashFlow | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
<p class="text-sm text-gray-600 mb-2"><strong>Location:</strong> {{ listing.location.name ? listing.location.name : listing.location.county }}</p>
<p class="text-sm text-gray-600 mb-4"><strong>Established:</strong> {{ listing.established }}</p>
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" alt="Company logo" class="absolute bottom-[80px] right-[20px] h-[45px] w-auto" />
<!-- Position und Größe des Bildes angepasst -->
<div class="flex-grow"></div>
<button
class="bg-green-500 text-white px-5 py-3 rounded-full w-full flex items-center justify-center mt-4 transition-colors duration-200 hover:bg-green-600"
[routerLink]="['/details-business-listing', listing.id]"
>
<!-- Button-Größe und Hover-Effekt verbessert -->
View Full Listing
<i class="fas fa-arrow-right ml-2"></i>
</button>
@ -52,7 +50,7 @@
</div>
}
</div>
} @else if (listings?.length===0){
} @else if (listings?.length === 0) {
<div class="w-full flex items-center flex-wrap justify-center gap-10">
<div class="grid gap-4 w-60">
<svg class="mx-auto" xmlns="http://www.w3.org/2000/svg" width="154" height="161" viewBox="0 0 154 161" fill="none">
@ -77,7 +75,7 @@
stroke="#818CF8"
/>
<path
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 83.3821 77.9987 83.3821 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
d="M83.185 87.2285C82.5387 87.2285 82.0027 86.6926 82.0027 86.0305C82.0027 86.0305 82.0027 83.3821 77.9987 83.3821C77.9987 83.3821 77.9987 86.0305 77.9987 86.0305C77.9987 86.6926 77.4627 87.2285 76.8006 87.2285C76.1543 87.2285 75.6183 86.6926 75.6183 86.0305C75.6183 80.2294 84.3831 80.2451 84.3831 86.0305C84.3831 86.6926 83.8471 87.2285 83.185 87.2285Z"
fill="#4F46E5"
/>
<path
@ -106,8 +104,12 @@
</div>
</div>
}
</div>
</div>
@if(pageCount > 1) {
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
}
</div>
@if(pageCount>1){
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
}
<!-- Filter Button for Mobile -->
<button (click)="openFilterModal()" class="md:hidden fixed bottom-4 right-4 bg-blue-500 text-white p-3 rounded-full shadow-lg z-20"><i class="fas fa-filter"></i> Filter</button>
</div>

View File

@ -9,6 +9,7 @@ import { BusinessListingCriteria, LISTINGS_PER_PAGE, ListingType, emailToDirName
import { environment } from '../../../../environments/environment';
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
import { ModalService } from '../../../components/search-modal/modal.service';
import { SearchModalComponent } from '../../../components/search-modal/search-modal.component';
import { CriteriaChangeService } from '../../../services/criteria-change.service';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
@ -19,7 +20,7 @@ import { assignProperties, getCriteriaProxy, resetBusinessListingCriteria } from
@Component({
selector: 'app-business-listings',
standalone: true,
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent],
imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent, SearchModalComponent],
templateUrl: './business-listings.component.html',
styleUrls: ['./business-listings.component.scss', '../../pages.scss'],
})
@ -55,6 +56,7 @@ export class BusinessListingsComponent {
private criteriaChangeService: CriteriaChangeService,
) {
this.criteria = getCriteriaProxy('businessListings', this) as BusinessListingCriteria;
this.modalService.sendCriteria(this.criteria);
this.init();
this.searchService.currentCriteria.pipe(untilDestroyed(this)).subscribe(criteria => {
if (criteria && criteria.criteriaType === 'businessListings') {

View File

@ -0,0 +1,60 @@
import { Injectable } from '@angular/core';
export interface ValidationMessage {
field: string;
message: string;
}
@Injectable({
providedIn: 'root',
})
export class ValidationService {
private messages: ValidationMessage[] = [];
constructor() {}
/**
* Fügt Validierungsnachrichten hinzu oder aktualisiert bestehende
* @param messages Array von Validierungsnachrichten
*/
setMessages(messages: ValidationMessage[]): void {
this.messages = messages;
}
/**
* Löscht alle Validierungsmeldungen
*/
clearMessages(): void {
this.messages = [];
}
/**
* Prüft, ob für ein bestimmtes Feld eine Validierungsmeldung existiert
* @param field Name des Feldes
* @returns true, wenn eine Meldung existiert
*/
hasMessage(field: string): boolean {
return this.messages.some(message => message.field === field);
}
/**
* Gibt die Validierungsmeldung für ein bestimmtes Feld zurück
* @param field Name des Feldes
* @returns ValidationMessage oder null, wenn keine Meldung existiert
*/
getMessage(field: string): ValidationMessage | null {
return this.messages.find(message => message.field === field) || null;
}
/**
* Hilfsmethode zur Verarbeitung von API-Fehlermeldungen
* @param error API-Fehler mit message-Array
*/
handleApiError(error: any): void {
if (error && error.message && Array.isArray(error.message)) {
this.setMessages(error.message);
} else {
this.clearMessages();
}
}
}

View File

@ -31,6 +31,7 @@ export function createEmptyBusinessListingCriteria(): BusinessListingCriteria {
leasedLocation: false,
franchiseResale: false,
title: '',
email: '',
brokerName: '',
searchType: 'exact',
radius: null,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB