Feature: #84, BugFix #93, Init Free Plan, Selection type of Listing

This commit is contained in:
Andreas Knuth 2024-08-27 20:01:56 +02:00
parent c1b72bbc12
commit 8721be4a90
14 changed files with 306 additions and 67 deletions

View File

@ -3,30 +3,85 @@ import Groq from 'groq-sdk';
import OpenAI from 'openai'; import OpenAI from 'openai';
import { BusinessListingCriteria } from '../models/main.model'; import { BusinessListingCriteria } from '../models/main.model';
const businessListingCriteriaStructure = { // const businessListingCriteriaStructure = {
criteriaType: 'business | commercialProperty | broker', // criteriaType: 'business | commercialProperty | broker',
types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'", // types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
city: 'string', // city: 'string',
state: 'string', // state: 'string',
county: 'string', // county: 'string',
minPrice: 'number', // minPrice: 'number',
maxPrice: 'number', // maxPrice: 'number',
minRevenue: 'number', // minRevenue: 'number',
maxRevenue: 'number', // maxRevenue: 'number',
minCashFlow: 'number', // minCashFlow: 'number',
maxCashFlow: 'number', // maxCashFlow: 'number',
minNumberEmployees: 'number', // minNumberEmployees: 'number',
maxNumberEmployees: 'number', // maxNumberEmployees: 'number',
establishedSince: 'number', // establishedSince: 'number',
establishedUntil: 'number', // establishedUntil: 'number',
realEstateChecked: 'boolean', // realEstateChecked: 'boolean',
leasedLocation: 'boolean', // leasedLocation: 'boolean',
franchiseResale: 'boolean', // franchiseResale: 'boolean',
title: 'string', // title: 'string',
brokerName: 'string', // brokerName: 'string',
searchType: "'exact' | 'radius'", // searchType: "'exact' | 'radius'",
radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'", // radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'",
}; // };
const BusinessListingCriteriaStructure = `
export interface BusinessListingCriteria {
state: string;
city: string;
searchType: 'exact' | 'radius';
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
minPrice: number;
maxPrice: number;
minRevenue: number;
maxRevenue: number;
minCashFlow: number;
maxCashFlow: number;
minNumberEmployees: number;
maxNumberEmployees: number;
establishedSince: number;
establishedUntil: number;
realEstateChecked: boolean;
leasedLocation: boolean;
franchiseResale: boolean;
//title: string;
brokerName: string;
//types:"'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
criteriaType: 'businessListings';
}
`;
const CommercialPropertyListingCriteriaStructure = `
export interface CommercialPropertyListingCriteria {
state: string;
city: string;
searchType: 'exact' | 'radius';
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
minPrice: number;
maxPrice: number;
//title: string;
//types:"'Retail'|'Land'|'Industrial'|'Office'|'Mixed Use'|'Multifamily'|'Uncategorized'"
criteriaType: 'commercialPropertyListings';
}
`;
const UserListingCriteriaStructure = `
export interface UserListingCriteria {
state: string;
city: string;
searchType: 'exact' | 'radius';
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
brokerName: string;
companyName: string;
counties: string[];
criteriaType: 'brokerListings';
}
`;
@Injectable() @Injectable()
export class AiService { export class AiService {
private readonly openai: OpenAI; private readonly openai: OpenAI;
@ -67,8 +122,10 @@ export class AiService {
{ {
role: 'system', role: 'system',
content: `Please create unformatted JSON Object from a user input. content: `Please create unformatted JSON Object from a user input.
The type must be: ${JSON.stringify(businessListingCriteriaStructure)}. The criteriaType must be only either 'businessListings' or 'commercialPropertyListings' or 'brokerListings' !!!!
If location details available please fill city and state as State Code and only county if explicitly mentioned`, The format of the object (depending on your choice of criteriaType) must be either ${BusinessListingCriteriaStructure}, ${CommercialPropertyListingCriteriaStructure} or ${UserListingCriteriaStructure} !!!!
If location details available please fill city and state as State Code and only county if explicitly mentioned.
If you decide for searchType==='exact', please do not set the attribute radius`,
}, },
{ {
role: 'user', role: 'user',

View File

@ -4,6 +4,7 @@ import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons'; import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { Collapse, Dropdown, initFlowbite } from 'flowbite'; import { Collapse, Dropdown, initFlowbite } from 'flowbite';
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from 'keycloak-angular';
import { filter, Observable, Subject, Subscription } from 'rxjs'; import { filter, Observable, Subject, Subscription } from 'rxjs';
@ -17,6 +18,7 @@ import { UserService } from '../../services/user.service';
import { assignProperties, compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils'; import { assignProperties, compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component'; import { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service'; import { ModalService } from '../search-modal/modal.service';
@UntilDestroy()
@Component({ @Component({
selector: 'header', selector: 'header',
standalone: true, standalone: true,
@ -75,6 +77,12 @@ export class HeaderComponent {
this.routerSubscription = this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: any) => { this.routerSubscription = this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: any) => {
this.checkCurrentRoute(event.urlAfterRedirects); this.checkCurrentRoute(event.urlAfterRedirects);
}); });
this.userService.currentUser.pipe(untilDestroyed(this)).subscribe(u => {
if (u !== undefined) {
this.user = u;
}
});
} }
private checkCurrentRoute(url: string): void { private checkCurrentRoute(url: string): void {
this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/' this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/'

View File

@ -445,7 +445,7 @@
</div> </div>
</div> </div>
<div> <div>
<label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Broker Name</label> <label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Name of Professional</label>
<input <input
type="text" type="text"
id="brokername" id="brokername"

View File

@ -22,7 +22,7 @@ import { ValidationMessagesService } from '../validation-messages.service';
}) })
export class ValidatedPriceComponent extends BaseInputComponent { export class ValidatedPriceComponent extends BaseInputComponent {
@Input() inputClasses: string; @Input() inputClasses: string;
@Input() placeholder: string; @Input() placeholder: string = '';
constructor(validationMessagesService: ValidationMessagesService) { constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService); super(validationMessagesService);
} }

View File

@ -35,7 +35,8 @@
<div class="w-11/12 md:w-2/3 lg:w-1/2"> <div class="w-11/12 md:w-2/3 lg:w-1/2">
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-blue-900 mb-4 text-center">Find businesses for sale.</h1> <h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-blue-900 mb-4 text-center">Find businesses for sale.</h1>
<p class="text-base md:text-lg lg:text-xl text-blue-600 mb-8 text-center">Unlocking Exclusive Opportunities - Empowering Entrepreneurial Dreams</p> <p class="text-base md:text-lg lg:text-xl text-blue-600 mb-8 text-center">Unlocking Exclusive Opportunities - Empowering Entrepreneurial Dreams</p>
<div class="bg-white bg-opacity-80 p-2 rounded-lg shadow-lg w-full"> <div class="bg-white bg-opacity-80 p-2 rounded-lg shadow-lg w-full" [ngClass]="{ 'pt-6': aiSearch }">
@if(!aiSearch){
<div class="text-sm lg:text-base mb-1 text-center text-gray-500 border-gray-200 dark:text-gray-400 dark:border-gray-700 flex justify-center"> <div class="text-sm lg:text-base mb-1 text-center text-gray-500 border-gray-200 dark:text-gray-400 dark:border-gray-700 flex justify-center">
<ul class="flex flex-wrap -mb-px"> <ul class="flex flex-wrap -mb-px">
<li class="me-2"> <li class="me-2">
@ -76,7 +77,40 @@
</li> </li>
</ul> </ul>
</div> </div>
@if(criteria){ } @if(aiSearch){
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-gray-300">
<div class="md:w-48 flex-1 md:border-r border-gray-300 overflow-hidden mb-2 md:mb-0">
<div class="relative max-sm:border border-gray-300 rounded-md">
<input #aiSearchInput type="text" [(ngModel)]="aiSearchText" name="aiSearchText" class="w-full p-2 border border-gray-300 rounded-md" (focus)="stopTypingEffect()" (blur)="startTypingEffect()" />
</div>
</div>
<div class="bg-blue-600 hover:bg-blue-500 transition-colors duration-200 max-sm:rounded-md">
<button
type="button"
class="w-full h-full text-white font-semibold py-3 px-6 focus:outline-none rounded-md md:rounded-none flex items-center justify-center min-w-[180px] min-h-[48px]"
(click)="generateAiResponse()"
>
<span class="flex items-center">
@if(loadingAi){
<svg aria-hidden="true" role="status" class="w-4 h-4 mr-3 text-white animate-spin" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="#E5E7EB"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentColor"
/>
</svg>
<span>Loading ...</span>
} @else {
<span>Search</span>
}
</span>
</button>
</div>
</div>
} @if(criteria && !aiSearch){
<div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-gray-300"> <div class="w-full max-w-3xl mx-auto bg-white rounded-lg flex flex-col md:flex-row md:border md:border-gray-300">
<div class="md:flex-none md:w-48 flex-1 md:border-r border-gray-300 overflow-hidden mb-2 md:mb-0"> <div class="md:flex-none md:w-48 flex-1 md:border-r border-gray-300 overflow-hidden mb-2 md:mb-0">
<div class="relative max-sm:border border-gray-300 rounded-md"> <div class="relative max-sm:border border-gray-300 rounded-md">
@ -119,7 +153,7 @@
</ng-select> </ng-select>
</div> </div>
</div> </div>
@if (criteria.radius){ @if (criteria.radius && !aiSearch){
<div class="md:flex-none md:w-36 flex-1 md:border-r border-gray-300 mb-2 md:mb-0"> <div class="md:flex-none md:w-36 flex-1 md:border-r border-gray-300 mb-2 md:mb-0">
<div class="relative max-sm:border border-gray-300 rounded-md"> <div class="relative max-sm:border border-gray-300 rounded-md">
<select <select
@ -150,10 +184,10 @@
} }
<div class="mt-4 flex items-center justify-center text-gray-700"> <div class="mt-4 flex items-center justify-center text-gray-700">
<span class="mr-2">AI-Search</span> <span class="mr-2">AI-Search</span>
<span class="bg-teal-100 text-teal-800 text-xs font-semibold px-2 py-1 rounded">BETA</span> <span class="bg-sky-300 text-teal-800 text-xs font-semibold px-2 py-1 rounded">BETA</span>
<span class="ml-2">- Try now</span> <span class="ml-2">- Try now</span>
<div class="ml-4 relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in"> <div class="ml-4 relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
<input type="checkbox" name="toggle" id="toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 border-gray-300 appearance-none cursor-pointer" /> <input (click)="toggleAiSearch()" type="checkbox" name="toggle" id="toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 border-gray-300 appearance-none cursor-pointer" />
<label for="toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label> <label for="toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div> </div>
</div> </div>

View File

@ -28,10 +28,10 @@ select {
} }
.toggle-checkbox:checked { .toggle-checkbox:checked {
right: 0; right: 0;
border-color: #4fd1c5; border-color: rgb(125 211 252);
} }
.toggle-checkbox:checked + .toggle-label { .toggle-checkbox:checked + .toggle-label {
background-color: #4fd1c5; background-color: rgb(125 211 252);
} }
:host ::ng-deep .ng-select.ng-select-single .ng-select-container { :host ::ng-deep .ng-select.ng-select-single .ng-select-container {
height: 48px; height: 48px;
@ -56,9 +56,19 @@ select option {
select.placeholder-selected { select.placeholder-selected {
color: #999; /* Farbe für den Platzhalter */ color: #999; /* Farbe für den Platzhalter */
} }
input::placeholder {
color: #555; /* Dunkleres Grau */
opacity: 1; /* Stellt sicher, dass die Deckkraft 100% ist */
}
/* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */ /* Stellt sicher, dass die Optionen im Dropdown immer schwarz sind */
select:focus option, select:focus option,
select:hover option { select:hover option {
color: #000 !important; color: #000 !important;
} }
input[type='text'][name='aiSearchText'] {
padding: 14px; /* Innerer Abstand */
font-size: 16px; /* Schriftgröße anpassen */
box-sizing: border-box; /* Padding und Border in die Höhe und Breite einrechnen */
height: 48px;
}

View File

@ -1,20 +1,30 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component, ElementRef, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from 'keycloak-angular';
import { catchError, concat, debounceTime, distinctUntilChanged, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs'; import { catchError, concat, debounceTime, distinctUntilChanged, lastValueFrom, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs';
import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { ModalService } from '../../components/search-modal/modal.service'; import { ModalService } from '../../components/search-modal/modal.service';
import { AiService } from '../../services/ai.service';
import { CriteriaChangeService } from '../../services/criteria-change.service'; import { CriteriaChangeService } from '../../services/criteria-change.service';
import { GeoService } from '../../services/geo.service'; import { GeoService } from '../../services/geo.service';
import { ListingsService } from '../../services/listings.service'; import { ListingsService } from '../../services/listings.service';
import { SearchService } from '../../services/search.service'; import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, createEnhancedProxy, getCriteriaStateObject, map2User } from '../../utils/utils'; import {
assignProperties,
compareObjects,
createEmptyBusinessListingCriteria,
createEmptyCommercialPropertyListingCriteria,
createEmptyUserListingCriteria,
createEnhancedProxy,
getCriteriaStateObject,
map2User,
} from '../../utils/utils';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -24,6 +34,7 @@ import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommerci
styleUrl: './home.component.scss', styleUrl: './home.component.scss',
}) })
export class HomeComponent { export class HomeComponent {
placeholders: string[] = ['waterfront property close to Houston less than 1M', 'construction area with beach access close to San Diego'];
activeTabAction: 'business' | 'commercialProperty' | 'broker' = 'business'; activeTabAction: 'business' | 'commercialProperty' | 'broker' = 'business';
type: string; type: string;
maxPrice: string; maxPrice: string;
@ -39,6 +50,18 @@ export class HomeComponent {
cityOrState = undefined; cityOrState = undefined;
private criteriaChangeSubscription: Subscription; private criteriaChangeSubscription: Subscription;
numberOfResults$: Observable<number>; numberOfResults$: Observable<number>;
aiSearch = false;
aiSearchText = '';
loadingAi = false;
@ViewChild('aiSearchInput', { static: false }) searchInput!: ElementRef;
typingSpeed: number = 100; // Geschwindigkeit des Tippens (ms)
pauseTime: number = 2000; // Pausezeit, bevor der Text verschwindet (ms)
index: number = 0;
charIndex: number = 0;
typingInterval: any;
showInput: boolean = true; // Steuerung der Anzeige des Eingabefelds
public constructor( public constructor(
private router: Router, private router: Router,
private modalService: ModalService, private modalService: ModalService,
@ -51,6 +74,7 @@ export class HomeComponent {
public cdRef: ChangeDetectorRef, public cdRef: ChangeDetectorRef,
private listingService: ListingsService, private listingService: ListingsService,
private userService: UserService, private userService: UserService,
private aiService: AiService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
const token = await this.keycloakService.getToken(); const token = await this.keycloakService.getToken();
@ -193,4 +217,87 @@ export class HomeComponent {
return 0; return 0;
} }
} }
toggleAiSearch() {
this.aiSearch = !this.aiSearch;
if (!this.aiSearch) {
this.aiSearchText = '';
this.stopTypingEffect();
} else {
setTimeout(() => this.startTypingEffect(), 0);
}
}
ngOnDestroy(): void {
clearTimeout(this.typingInterval); // Stelle sicher, dass das Intervall gestoppt wird, wenn die Komponente zerstört wird
}
startTypingEffect(): void {
if (!this.aiSearchText) {
this.typePlaceholder();
}
}
stopTypingEffect(): void {
clearTimeout(this.typingInterval);
}
typePlaceholder(): void {
if (!this.searchInput || !this.searchInput.nativeElement) {
return; // Falls das Eingabefeld nicht verfügbar ist (z.B. durch ngIf)
}
if (this.aiSearchText) {
return; // Stoppe, wenn der Benutzer Text eingegeben hat
}
const inputField = this.searchInput.nativeElement as HTMLInputElement;
if (document.activeElement === inputField) {
this.stopTypingEffect();
return;
}
inputField.placeholder = this.placeholders[this.index].substring(0, this.charIndex);
if (this.charIndex < this.placeholders[this.index].length) {
this.charIndex++;
this.typingInterval = setTimeout(() => this.typePlaceholder(), this.typingSpeed);
} else {
// Nach dem vollständigen Tippen eine Pause einlegen
this.typingInterval = setTimeout(() => {
inputField.placeholder = ''; // Schlagartiges Löschen des Platzhalters
this.charIndex = 0;
this.index = (this.index + 1) % this.placeholders.length;
this.typingInterval = setTimeout(() => this.typePlaceholder(), this.typingSpeed);
}, this.pauseTime);
}
}
async generateAiResponse() {
this.loadingAi = true;
const result = await this.aiService.generateAiReponse(this.aiSearchText);
console.log(result);
let criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria | any;
if (result.criteriaType === 'businessListings') {
this.changeTab('business');
criteria = result as BusinessListingCriteria;
} else if (result.criteriaType === 'commercialPropertyListings') {
this.changeTab('commercialProperty');
criteria = result as CommercialPropertyListingCriteria;
} else {
this.changeTab('broker');
criteria = result as UserListingCriteria;
}
const city = criteria.city as string;
if (city && city.length > 0) {
let results = await lastValueFrom(this.geoService.findCitiesStartingWith(city, criteria.state));
if (results.length > 0) {
criteria.city = results[0];
} else {
criteria.city = null;
}
}
if (criteria.radius && criteria.radius.length > 0) {
criteria.radius = parseInt(criteria.radius);
}
this.loadingAi = false;
this.criteria = assignProperties(this.criteria, criteria);
this.search();
}
} }

View File

@ -28,16 +28,22 @@ export class PricingComponent {
async ngOnInit() { async ngOnInit() {
const token = await this.keycloakService.getToken(); const token = await this.keycloakService.getToken();
this.keycloakUser = map2User(token); this.keycloakUser = map2User(token);
if (this.id) { if (this.keycloakUser) {
if (this.id === 'free') {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.user.subscriptionPlan = 'free';
await this.userService.save(this.user);
this.router.navigate([`/account`]);
} else if (this.id) {
this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` }); this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
} } else if (!this.id) {
if (this.keycloakUser && !this.id) {
this.user = await this.userService.getByMail(this.keycloakUser.email); this.user = await this.userService.getByMail(this.keycloakUser.email);
if (this.user.subscriptionId) { if (this.user.subscriptionId) {
this.router.navigate([`/account`]); this.router.navigate([`/account`]);
} }
} }
} }
}
async register(priceId?: string) { async register(priceId?: string) {
if (this.keycloakUser) { if (this.keycloakUser) {
@ -55,7 +61,7 @@ export class PricingComponent {
redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}`, redirectUri: `${window.location.origin}/pricing/${btoa(priceId)}`,
}); });
} else { } else {
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` }); this.keycloakService.register({ redirectUri: `${window.location.origin}/pricing/free` });
} }
} }
} }

View File

@ -10,7 +10,7 @@ import { ImageCropperComponent } from 'ngx-image-cropper';
import { QuillModule } from 'ngx-quill'; import { QuillModule } from 'ngx-quill';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { User } from '../../../../../../bizmatch-server/src/models/db.model'; import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, Invoice, StripeSubscription, UploadParams, ValidationMessage, createDefaultUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model'; import { AutoCompleteCompleteEvent, Invoice, StripeSubscription, UploadParams, ValidationMessage, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component'; import { ConfirmationComponent } from '../../../components/confirmation/confirmation.component';
import { ConfirmationService } from '../../../components/confirmation/confirmation.service'; import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
@ -111,10 +111,6 @@ export class AccountComponent {
this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email)); this.subscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.email));
await this.synchronizeSubscriptions(this.subscriptions); await this.synchronizeSubscriptions(this.subscriptions);
// if (this.subscriptions.length === 0) {
// this.subscriptions = [{ ended_at: null, start_date: Math.floor(new Date(this.user.created).getTime() / 1000), status: null, metadata: { plan: 'Free Plan' } }];
// }
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
@ -168,22 +164,9 @@ export class AccountComponent {
printInvoice(invoice: Invoice) {} printInvoice(invoice: Invoice) {}
async updateProfile(user: User) { async updateProfile(user: User) {
if (this.user.customerType === 'buyer') {
const confirmed = await this.confirmationService.showConfirmation({ message: 'Are you sure you want to switch to Buyer ? All your listings as well as all your professionals informations will be deleted' });
if (confirmed) {
const id = this.user.id;
this.user = createDefaultUser(this.user.email, this.user.firstname, this.user.lastname, null);
this.user.customerType = 'buyer';
this.user.id = id;
this.imageService.deleteLogoImagesByMail(this.user.email);
this.imageService.deleteProfileImagesByMail(this.user.email);
} else {
this.user.customerType = 'professional';
return;
}
}
try { try {
await this.userService.save(this.user); await this.userService.save(this.user);
this.userService.changeUser(this.user);
this.messageService.addMessage({ severity: 'success', text: 'Account changes have been persisted', duration: 3000 }); this.messageService.addMessage({ severity: 'success', text: 'Account changes have been persisted', duration: 3000 });
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
this.validationMessages = []; this.validationMessages = [];

View File

@ -7,11 +7,12 @@
<label for="listingsCategory" class="block text-sm font-bold text-gray-700 mb-1">Listing category</label> <label for="listingsCategory" class="block text-sm font-bold text-gray-700 mb-1">Listing category</label>
<ng-select <ng-select
[readonly]="mode === 'edit'" [readonly]="mode === 'edit'"
[items]="selectOptions?.listingCategories" [items]="listingCategories"
bindLabel="name" bindLabel="name"
bindValue="value" bindValue="value"
(ngModelChange)="changeListingCategory($event)" (ngModelChange)="changeListingCategory($event)"
[(ngModel)]="listing.listingsCategory" [(ngModel)]="listing.listingsCategory"
[clearable]="false"
name="listingsCategory" name="listingsCategory"
> >
</ng-select> </ng-select>

View File

@ -98,6 +98,7 @@ export class EditCommercialPropertyListingComponent {
data: BusinessListing; data: BusinessListing;
userId: string; userId: string;
typesOfCommercialProperty = []; typesOfCommercialProperty = [];
listingCategories = [];
env = environment; env = environment;
ts = new Date().getTime(); ts = new Date().getTime();
quillModules = { quillModules = {
@ -144,6 +145,13 @@ export class EditCommercialPropertyListingComponent {
async ngOnInit() { async ngOnInit() {
const token = await this.keycloakService.getToken(); const token = await this.keycloakService.getToken();
const keycloakUser = map2User(token); const keycloakUser = map2User(token);
const email = keycloakUser.email;
this.user = await this.userService.getByMail(email);
this.listingCategories = this.selectOptions.listingCategories
.filter(lc => lc.value === 'commercialProperty' || (this.user.customerSubType === 'broker' && lc.value === 'business'))
.map(e => {
return { name: e.name, value: e.value };
});
if (this.mode === 'edit') { if (this.mode === 'edit') {
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing; this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
} else { } else {

View File

@ -0,0 +1,19 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class AiService {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {}
async generateAiReponse(prompt: string): Promise<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria> {
let headers = new HttpHeaders();
headers = headers.set('X-Hide-Loading', 'true');
return lastValueFrom(this.http.post<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria>(`${this.apiBaseUrl}/bizmatch/ai`, { query: prompt }, { headers }));
}
}

View File

@ -1,6 +1,6 @@
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { lastValueFrom, Observable } from 'rxjs'; import { BehaviorSubject, lastValueFrom, Observable } from 'rxjs';
import urlcat from 'urlcat'; import urlcat from 'urlcat';
import { User } from '../../../../bizmatch-server/src/models/db.model'; import { User } from '../../../../bizmatch-server/src/models/db.model';
import { ResponseUsersArray, StatesResult, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model'; import { ResponseUsersArray, StatesResult, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
@ -12,8 +12,14 @@ import { environment } from '../../environments/environment';
export class UserService { export class UserService {
private apiBaseUrl = environment.apiBaseUrl; private apiBaseUrl = environment.apiBaseUrl;
private userSource = new BehaviorSubject<User>(undefined);
currentUser = this.userSource.asObservable();
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
changeUser(user: User) {
this.userSource.next(user);
}
// ----------------------------- // -----------------------------
// DB services // DB services
// ----------------------------- // -----------------------------

View File

@ -92,5 +92,5 @@ p-menubarsub ul {
} }
input::placeholder, input::placeholder,
textarea::placeholder { textarea::placeholder {
color: #cfd7e0 !important; color: #999 !important;
} }