diff --git a/bizmatch-server/.vscode/launch.json b/bizmatch-server/.vscode/launch.json index 6b473fd..e9b2e00 100644 --- a/bizmatch-server/.vscode/launch.json +++ b/bizmatch-server/.vscode/launch.json @@ -19,13 +19,15 @@ { "type": "node", "request": "launch", - "name": "Debug Current TS File", - "program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js", - "preLaunchTask": "tsc: build - tsconfig.json", - "outFiles": ["${workspaceFolder}/out/**/*.js"], + "name": "Launch TypeScript file with tsx", + "runtimeExecutable": "npx", + "runtimeArgs": ["tsx", "--inspect"], + "args": ["${workspaceFolder}/src/drizzle/import.ts"], + "cwd": "${workspaceFolder}", + "outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"], "sourceMaps": true, - "smartStep": true, - "internalConsoleOptions": "openOnSessionStart" + "resolveSourceMapLocations": ["${workspaceFolder}/src/**/*.ts", "!**/node_modules/**"], + "skipFiles": ["/**", "${workspaceFolder}/node_modules/**/*.js"] }, { "type": "node", diff --git a/bizmatch-server/package.json b/bizmatch-server/package.json index 9a1ce67..5d22c4e 100644 --- a/bizmatch-server/package.json +++ b/bizmatch-server/package.json @@ -54,7 +54,7 @@ "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sharp": "^0.33.2", - "tsx": "^4.7.2", + "tsx": "^4.16.2", "urlcat": "^3.1.0", "winston": "^3.11.0" }, @@ -78,6 +78,7 @@ "@typescript-eslint/parser": "^6.0.0", "commander": "^12.0.0", "drizzle-kit": "^0.23.0", + "esbuild-register": "^3.5.0", "eslint": "^8.42.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", @@ -111,4 +112,4 @@ "coverageDirectory": "../coverage", "testEnvironment": "node" } -} \ No newline at end of file +} diff --git a/bizmatch-server/src/drizzle/import.ts b/bizmatch-server/src/drizzle/import.ts index 312710e..fd1ac96 100644 --- a/bizmatch-server/src/drizzle/import.ts +++ b/bizmatch-server/src/drizzle/import.ts @@ -7,10 +7,12 @@ import { join } from 'path'; import pkg from 'pg'; import { rimraf } from 'rimraf'; import sharp from 'sharp'; +import { SelectOptionsService } from 'src/select-options/select-options.service.js'; import winston from 'winston'; import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js'; import { emailToDirName, KeyValueStyle } from '../models/main.model.js'; import * as schema from './schema.js'; +import { users } from './schema.js'; const typesOfBusiness: Array = [ { name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' }, { name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' }, @@ -44,6 +46,7 @@ await db.delete(schema.commercials); await db.delete(schema.businesses); await db.delete(schema.users); +const sso = new SelectOptionsService(); //Broker let filePath = `./data/broker.json`; let data: string = readFileSync(filePath, 'utf8'); @@ -63,6 +66,8 @@ fs.ensureDirSync(`./pictures/logo`); fs.ensureDirSync(`./pictures/profile`); fs.ensureDirSync(`./pictures/property`); type UserProfile = Omit; + +type NewUser = typeof users.$inferInsert; //for (const userData of usersData) { for (let index = 0; index < usersData.length; index++) { const userData = usersData[index]; @@ -100,18 +105,17 @@ for (let index = 0; index < usersData.length; index++) { const userProfile = createUserProfile(user); logger.info(`${index} - ${JSON.stringify(userProfile)}`); const embedding = await createEmbedding(JSON.stringify(userProfile)); - sleep(500); + sleep(200); const u = await db .insert(schema.users) .values({ ...user, embedding: embedding, - }) - .returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email }); - // const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email }); + } as NewUser) + .returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname }); generatedUserData.push(u[0]); i++; - + logger.info(`user_${index} inserted`); if (u[0].gender === 'male') { male++; const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`); @@ -125,46 +129,12 @@ for (let index = 0; index < usersData.length; index++) { await storeCompanyLogo(data, emailToDirName(u[0].email)); } -//Business Listings -filePath = `./data/businesses.json`; -data = readFileSync(filePath, 'utf8'); -const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten -for (const business of businessJsonData) { - delete business.id; - business.created = new Date(business.created); - business.updated = new Date(business.created); - const user = getRandomItem(generatedUserData); - business.email = user.email; - business.imageName = emailToDirName(user.email); - const embeddingText = JSON.stringify({ - type: typesOfBusiness.find(b => b.value === String(business.type))?.name, - title: business.title, - description: business.description, - city: business.city, - state: business.state, - price: business.price, - realEstateIncluded: business.realEstateIncluded, - leasedLocation: business.leasedLocation, - franchiseResale: business.franchiseResale, - salesRevenue: business.salesRevenue, - cashFlow: business.cashFlow, - supportAndTraining: business.supportAndTraining, - employees: business.employees, - established: business.established, - reasonForSale: business.reasonForSale, - }); - const embedding = await createEmbedding(embeddingText); - sleep(300); - await db.insert(schema.businesses).values({ - ...business, - embedding: embedding, - }); -} //Corporate Listings filePath = `./data/commercials.json`; data = readFileSync(filePath, 'utf8'); const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten -for (const commercial of commercialJsonData) { +for (let index = 0; index < commercialJsonData.length; index++) { + const commercial = commercialJsonData[index]; const id = commercial.id; delete commercial.id; const user = getRandomItem(generatedUserData); @@ -175,7 +145,25 @@ for (const commercial of commercialJsonData) { commercial.updated = insertionDate; commercial.email = user.email; commercial.draft = false; - const result = await db.insert(schema.commercials).values(commercial).returning(); + const reducedCommercial = { + city: commercial.city, + description: commercial.description, + email: commercial.email, + price: commercial.price, + state: sso.locations.find(l => l.value === commercial.state)?.name, + title: commercial.title, + name: `${user.firstname} ${user.lastname}`, + }; + const embedding = await createEmbedding(JSON.stringify(reducedCommercial)); + sleep(200); + const result = await db + .insert(schema.commercials) + .values({ + ...commercial, + embedding: embedding, + }) + .returning(); + logger.info(`commercial_${index} inserted`); try { fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`); } catch (err) { @@ -183,6 +171,46 @@ for (const commercial of commercialJsonData) { } } +//Business Listings +filePath = `./data/businesses.json`; +data = readFileSync(filePath, 'utf8'); +const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten +for (let index = 0; index < businessJsonData.length; index++) { + const business = businessJsonData[index]; + delete business.id; + business.created = new Date(business.created); + business.updated = new Date(business.created); + const user = getRandomItem(generatedUserData); + business.email = user.email; + business.imageName = emailToDirName(user.email); + const embeddingText = JSON.stringify({ + type: typesOfBusiness.find(b => b.value === String(business.type))?.name, + title: business.title, + description: business.description, + email: business.email, + city: business.city, + state: sso.locations.find(l => l.value === business.state)?.name, + price: business.price, + realEstateIncluded: business.realEstateIncluded, + leasedLocation: business.leasedLocation, + franchiseResale: business.franchiseResale, + salesRevenue: business.salesRevenue, + cashFlow: business.cashFlow, + supportAndTraining: business.supportAndTraining, + employees: business.employees, + established: business.established, + reasonForSale: business.reasonForSale, + name: `${user.firstname} ${user.lastname}`, + }); + const embedding = await createEmbedding(embeddingText); + sleep(200); + await db.insert(schema.businesses).values({ + ...business, + embedding: embedding, + }); + logger.info(`business_${index} inserted`); +} + //End await client.end(); diff --git a/bizmatch-server/src/listings/business-listings.controller.ts b/bizmatch-server/src/listings/business-listings.controller.ts index b7d0ffc..4644f49 100644 --- a/bizmatch-server/src/listings/business-listings.controller.ts +++ b/bizmatch-server/src/listings/business-listings.controller.ts @@ -26,11 +26,17 @@ export class BusinessListingsController { } @UseGuards(OptionalJwtAuthGuard) - @Post('search') + @Post('find') find(@Request() req, @Body() criteria: ListingCriteria): any { return this.listingsService.findBusinessListings(criteria, req.user as JwtUser); } + @UseGuards(OptionalJwtAuthGuard) + @Post('search') + search(@Request() req, @Body() criteria: ListingCriteria): any { + return this.listingsService.searchBusinessListings(criteria.prompt); + } + @Post() create(@Body() listing: any) { this.logger.info(`Save Listing`); diff --git a/bizmatch-server/src/listings/commercial-property-listings.controller.ts b/bizmatch-server/src/listings/commercial-property-listings.controller.ts index 7054a5b..a0e4a49 100644 --- a/bizmatch-server/src/listings/commercial-property-listings.controller.ts +++ b/bizmatch-server/src/listings/commercial-property-listings.controller.ts @@ -28,7 +28,7 @@ export class CommercialPropertyListingsController { return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser); } @UseGuards(OptionalJwtAuthGuard) - @Post('search') + @Post('find') async find(@Request() req, @Body() criteria: ListingCriteria): Promise { return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser); } diff --git a/bizmatch-server/src/listings/listings.service.ts b/bizmatch-server/src/listings/listings.service.ts index 6456262..f4fa35c 100644 --- a/bizmatch-server/src/listings/listings.service.ts +++ b/bizmatch-server/src/listings/listings.service.ts @@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; +import OpenAI from 'openai'; import { Logger } from 'winston'; import * as schema from '../drizzle/schema.js'; import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js'; @@ -11,11 +12,16 @@ import { JwtUser, ListingCriteria, emailToDirName } from '../models/main.model.j @Injectable() export class ListingsService { + openai: OpenAI; constructor( @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(PG_CONNECTION) private conn: NodePgDatabase, private fileService: FileService, - ) {} + ) { + this.openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen + }); + } private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] { const conditions = []; if (criteria.type) { @@ -42,6 +48,19 @@ export class ListingsService { // Listings general // ############################################################## + // #### Find by embeddng ######################################## + async searchBusinessListings(query: string, limit: number = 20): Promise { + const queryEmbedding = await this.createEmbedding(query); + + const results = await this.conn + .select() + .from(businesses) + .orderBy(sql`embedding <-> ${JSON.stringify(queryEmbedding)}`) + .limit(limit); + + return results as BusinessListing[]; + } + // #### Find by criteria ######################################## async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise { const start = criteria.start ? criteria.start : 0; const length = criteria.length ? criteria.length : 12; @@ -86,6 +105,8 @@ export class ListingsService { ]); return { total, data }; } + + // #### Find by ID ######################################## async findCommercialPropertiesById(id: string, user: JwtUser): Promise { let result = await this.conn .select() @@ -102,6 +123,8 @@ export class ListingsService { result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN')); return result[0] as BusinessListing; } + + // #### Find by User EMail ######################################## async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise { const conditions = []; conditions.push(eq(commercials.imagePath, emailToDirName(email))); @@ -124,6 +147,8 @@ export class ListingsService { .from(businesses) .where(and(...conditions))) as CommercialPropertyListing[]; } + + // #### Find by imagePath ######################################## async findByImagePath(imagePath: string, serial: string): Promise { const result = await this.conn .select() @@ -131,13 +156,15 @@ export class ListingsService { .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`)); return result[0] as CommercialPropertyListing; } + + // #### CREATE ######################################## async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise { data.created = new Date(); data.updated = new Date(); const [createdListing] = await this.conn.insert(table).values(data).returning(); return createdListing as BusinessListing | CommercialPropertyListing; } - + // #### UPDATE CommercialProps ######################################## async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise { data.updated = new Date(); data.created = new Date(data.created); @@ -150,22 +177,18 @@ export class ListingsService { const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning(); return updateListing as BusinessListing | CommercialPropertyListing; } + // #### UPDATE Business ######################################## async updateBusinessListing(id: string, data: BusinessListing): Promise { data.updated = new Date(); data.created = new Date(data.created); const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning(); return updateListing as BusinessListing | CommercialPropertyListing; } + // #### DELETE ######################################## async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise { await this.conn.delete(table).where(eq(table.id, id)); } - async getStates(table: typeof businesses | typeof commercials): Promise { - return await this.conn - .select({ state: table.state, count: sql`count(${table.id})`.mapWith(Number) }) - .from(table) - .groupBy(sql`${table.state}`) - .orderBy(sql`count desc`); - } + // ############################################################## // Images for commercial Properties // ############################################################## @@ -182,4 +205,24 @@ export class ListingsService { listing.imageOrder.push(imagename); await this.updateCommercialPropertyListing(listing.id, listing); } + // ############################################################## + // States + // ############################################################## + async getStates(table: typeof businesses | typeof commercials): Promise { + return await this.conn + .select({ state: table.state, count: sql`count(${table.id})`.mapWith(Number) }) + .from(table) + .groupBy(sql`${table.state}`) + .orderBy(sql`count desc`); + } + // ############################################################## + // Embedding + // ############################################################## + async createEmbedding(text: string): Promise { + const response = await this.openai.embeddings.create({ + model: 'text-embedding-3-small', + input: text, + }); + return response.data[0].embedding; + } } diff --git a/bizmatch-server/src/models/main.model.ts b/bizmatch-server/src/models/main.model.ts index f108271..a2f4e72 100644 --- a/bizmatch-server/src/models/main.model.ts +++ b/bizmatch-server/src/models/main.model.ts @@ -65,6 +65,7 @@ export interface ListingCriteria { title: string; category: 'professional' | 'broker'; name: string; + prompt: string; } export interface KeycloakUser { diff --git a/bizmatch/src/app/app.component.ts b/bizmatch/src/app/app.component.ts index 35e7fd7..09e47cd 100644 --- a/bizmatch/src/app/app.component.ts +++ b/bizmatch/src/app/app.component.ts @@ -1,7 +1,6 @@ import { CommonModule } from '@angular/common'; import { Component, HostListener } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router'; -import { initFlowbite } from 'flowbite'; import { KeycloakService } from 'keycloak-angular'; import onChange from 'on-change'; @@ -42,7 +41,11 @@ export class AppComponent { ngOnInit() {} @HostListener('window:keydown', ['$event']) handleKeyboardEvent(event: KeyboardEvent) { - initFlowbite(); + // this.router.events.subscribe(event => { + // if (event instanceof NavigationEnd) { + // initFlowbite(); + // } + // }); if (event.shiftKey && event.ctrlKey && event.key === 'V') { this.showVersionDialog(); } diff --git a/bizmatch/src/app/components/dropdown/dropdown.component.html b/bizmatch/src/app/components/dropdown/dropdown.component.html new file mode 100644 index 0000000..b7ec396 --- /dev/null +++ b/bizmatch/src/app/components/dropdown/dropdown.component.html @@ -0,0 +1 @@ +

dropdown works!

diff --git a/bizmatch/src/app/components/dropdown/dropdown.component.scss b/bizmatch/src/app/components/dropdown/dropdown.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/bizmatch/src/app/components/dropdown/dropdown.component.ts b/bizmatch/src/app/components/dropdown/dropdown.component.ts new file mode 100644 index 0000000..468b220 --- /dev/null +++ b/bizmatch/src/app/components/dropdown/dropdown.component.ts @@ -0,0 +1,129 @@ +import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core'; +import { createPopper, Instance as PopperInstance } from '@popperjs/core'; + +@Component({ + selector: 'app-dropdown', + template: ` +
+ +
+ `, + standalone: true, +}) +export class DropdownComponent implements AfterViewInit, OnDestroy { + @ViewChild('targetEl') targetEl!: ElementRef; + @Input() triggerEl!: HTMLElement; + + @Input() placement: any = 'bottom'; + @Input() triggerType: 'click' | 'hover' = 'click'; + @Input() offsetSkidding: number = 0; + @Input() offsetDistance: number = 10; + @Input() delay: number = 300; + @Input() ignoreClickOutsideClass: string | false = false; + + @HostBinding('class.hidden') isHidden: boolean = true; + + private popperInstance: PopperInstance | null = null; + isVisible: boolean = false; + private clickOutsideListener: any; + private hoverShowListener: any; + private hoverHideListener: any; + + ngAfterViewInit() { + if (!this.triggerEl) { + console.error('Trigger element is not provided to the dropdown component.'); + return; + } + this.initializePopper(); + this.setupEventListeners(); + } + + ngOnDestroy() { + this.destroyPopper(); + this.removeEventListeners(); + } + + private initializePopper() { + this.popperInstance = createPopper(this.triggerEl, this.targetEl.nativeElement, { + placement: this.placement, + modifiers: [ + { + name: 'offset', + options: { + offset: [this.offsetSkidding, this.offsetDistance], + }, + }, + ], + }); + } + + private setupEventListeners() { + if (this.triggerType === 'click') { + this.triggerEl.addEventListener('click', () => this.toggle()); + } else if (this.triggerType === 'hover') { + this.hoverShowListener = () => this.show(); + this.hoverHideListener = () => this.hide(); + this.triggerEl.addEventListener('mouseenter', this.hoverShowListener); + this.triggerEl.addEventListener('mouseleave', this.hoverHideListener); + this.targetEl.nativeElement.addEventListener('mouseenter', this.hoverShowListener); + this.targetEl.nativeElement.addEventListener('mouseleave', this.hoverHideListener); + } + + this.clickOutsideListener = (event: MouseEvent) => this.handleClickOutside(event); + document.addEventListener('click', this.clickOutsideListener); + } + + private removeEventListeners() { + if (this.triggerType === 'click') { + this.triggerEl.removeEventListener('click', () => this.toggle()); + } else if (this.triggerType === 'hover') { + this.triggerEl.removeEventListener('mouseenter', this.hoverShowListener); + this.triggerEl.removeEventListener('mouseleave', this.hoverHideListener); + this.targetEl.nativeElement.removeEventListener('mouseenter', this.hoverShowListener); + this.targetEl.nativeElement.removeEventListener('mouseleave', this.hoverHideListener); + } + + document.removeEventListener('click', this.clickOutsideListener); + } + + toggle() { + this.isVisible ? this.hide() : this.show(); + } + + show() { + this.isVisible = true; + this.isHidden = false; + this.targetEl.nativeElement.classList.remove('hidden'); + this.popperInstance?.update(); + } + + hide() { + this.isVisible = false; + this.isHidden = true; + this.targetEl.nativeElement.classList.add('hidden'); + } + + private handleClickOutside(event: MouseEvent) { + if (!this.isVisible) return; + + const clickedElement = event.target as HTMLElement; + if (this.ignoreClickOutsideClass) { + const ignoredElements = document.querySelectorAll(`.${this.ignoreClickOutsideClass}`); + const arr = Array.from(ignoredElements); + for (const el of arr) { + if (el.contains(clickedElement)) return; + } + } + + if (!this.targetEl.nativeElement.contains(clickedElement) && !this.triggerEl.contains(clickedElement)) { + this.hide(); + } + } + + private destroyPopper() { + if (this.popperInstance) { + this.popperInstance.destroy(); + this.popperInstance = null; + } + } +} diff --git a/bizmatch/src/app/components/footer/footer.component.ts b/bizmatch/src/app/components/footer/footer.component.ts index d364b88..0c43eec 100644 --- a/bizmatch/src/app/components/footer/footer.component.ts +++ b/bizmatch/src/app/components/footer/footer.component.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { RouterModule } from '@angular/router'; +import { NavigationEnd, Router, RouterModule } from '@angular/router'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { initFlowbite } from 'flowbite'; @Component({ @@ -15,7 +15,12 @@ export class FooterComponent { privacyVisible = false; termsVisible = false; currentYear: number = new Date().getFullYear(); + constructor(private router: Router) {} ngOnInit() { - initFlowbite(); + this.router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + initFlowbite(); + } + }); } } diff --git a/bizmatch/src/app/components/header/header.component.html b/bizmatch/src/app/components/header/header.component.html index 96bdacf..40a6660 100644 --- a/bizmatch/src/app/components/header/header.component.html +++ b/bizmatch/src/app/components/header/header.component.html @@ -1,15 +1,114 @@ - + --> + + + + +
+

Filter

+ + +
+
+ +
+ +
+ + + + + + + + + + + +
+ + +
+
+
diff --git a/bizmatch/src/app/components/header/header.component.ts b/bizmatch/src/app/components/header/header.component.ts index 33d535b..ebe3d15 100644 --- a/bizmatch/src/app/components/header/header.component.ts +++ b/bizmatch/src/app/components/header/header.component.ts @@ -1,20 +1,23 @@ +import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout'; import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { Router, RouterModule } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +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 } from 'rxjs'; +import { Observable, Subject, takeUntil } 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 { DropdownComponent } from '../dropdown/dropdown.component'; @Component({ selector: 'header', standalone: true, - imports: [CommonModule, RouterModule], + imports: [CommonModule, RouterModule, DropdownComponent, FormsModule], templateUrl: './header.component.html', styleUrl: './header.component.scss', }) @@ -27,23 +30,50 @@ export class HeaderComponent { faUserGear = faUserGear; profileUrl: string; env = environment; - constructor(public keycloakService: KeycloakService, private router: Router, private userService: UserService, private sharedService: SharedService) {} + private filterDropdown: Dropdown | null = null; + isMobile: boolean = false; + private destroy$ = new Subject(); + prompt: string; + constructor(public keycloakService: KeycloakService, private router: Router, private userService: UserService, private sharedService: SharedService, private breakpointObserver: BreakpointObserver) {} async ngOnInit() { const token = await this.keycloakService.getToken(); this.keycloakUser = map2User(token); - this.user = await this.userService.getByMail(this.keycloakUser.email); - this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; - setTimeout(() => { - initFlowbite(); + if (this.keycloakUser) { + this.user = await this.userService.getByMail(this.keycloakUser?.email); + this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; + } + + // setTimeout(() => { + // initFlowbite(); + // }); + this.router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + 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.sharedService.currentProfilePhoto.subscribe(photoUrl => { if (photoUrl) { this.profileUrl = photoUrl; } }); } - + toggleFilterDropdown() { + if (this.filterDropdown) { + this.filterDropdown.toggle(); + } + } ngAfterViewInit() {} navigateWithState(dest: string, state: any) { @@ -82,4 +112,9 @@ export class HeaderComponent { this.closeDropdown(); this.closeMobileMenu(); } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/bizmatch/src/app/pages/home/home.component.html b/bizmatch/src/app/pages/home/home.component.html index 9a65865..5a927f9 100644 --- a/bizmatch/src/app/pages/home/home.component.html +++ b/bizmatch/src/app/pages/home/home.component.html @@ -77,7 +77,7 @@
- +
--> +
@for (listing of listings; track listing.id) {
-
+
{{ selectOptions.getBusiness(listing.type) }} @@ -111,4 +112,17 @@ }
+ + + diff --git a/bizmatch/src/app/pages/listings/business-listings/business-listings.component.ts b/bizmatch/src/app/pages/listings/business-listings/business-listings.component.ts index be870ea..db3b469 100644 --- a/bizmatch/src/app/pages/listings/business-listings/business-listings.component.ts +++ b/bizmatch/src/app/pages/listings/business-listings/business-listings.component.ts @@ -70,9 +70,10 @@ export class BusinessListingsComponent { this.search(); } async search() { - const listingReponse = await this.listingsService.getListings(this.criteria, 'business'); - this.listings = listingReponse.data; - this.totalRecords = listingReponse.total; + 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(); } diff --git a/bizmatch/src/app/services/listings.service.ts b/bizmatch/src/app/services/listings.service.ts index 87a36b0..dd5cc6a 100644 --- a/bizmatch/src/app/services/listings.service.ts +++ b/bizmatch/src/app/services/listings.service.ts @@ -13,7 +13,11 @@ export class ListingsService { constructor(private http: HttpClient) {} async getListings(criteria: ListingCriteria, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty'): Promise { - const result = await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/search`, criteria)); + const result = await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/find`, criteria)); + return result; + } + async getListingsByPrompt(criteria: ListingCriteria): Promise { + const result = await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/listings/business/search`, criteria)); return result; } getListingById(id: string, listingsCategory?: 'business' | 'commercialProperty'): Observable { diff --git a/bizmatch/src/app/services/user.service.ts b/bizmatch/src/app/services/user.service.ts index 4de591c..6dc8fe1 100644 --- a/bizmatch/src/app/services/user.service.ts +++ b/bizmatch/src/app/services/user.service.ts @@ -33,13 +33,4 @@ export class UserService { async getAllStates(): Promise { return await lastValueFrom(this.http.get(`${this.apiBaseUrl}/bizmatch/user/states/all`)); } - // async getId(email: string): Promise { - // if (sessionStorage.getItem('USERID')) { - // return sessionStorage.getItem('USERID'); - // } else { - // const user = await this.getByMail(email); - // sessionStorage.setItem('USERID', user.id); - // return user.id; - // } - // } } diff --git a/bizmatch/src/app/utils/utils.ts b/bizmatch/src/app/utils/utils.ts index 9196b93..f6bbe43 100644 --- a/bizmatch/src/app/utils/utils.ts +++ b/bizmatch/src/app/utils/utils.ts @@ -96,6 +96,7 @@ export function createDefaultListingCriteria(): ListingCriteria { title: '', category: 'broker', name: '', + prompt: '', }; } export function createLogger(name: string, level: number = INFO, options: any = {}) { @@ -179,3 +180,58 @@ export function getDialogWidth(dimensions): string { } return dialogWidth; } + +import { initFlowbite } from 'flowbite'; +import { Subject, concatMap, delay, of } from 'rxjs'; + +const flowbiteQueue = new Subject<() => void>(); + +flowbiteQueue.pipe(concatMap(item => of(item).pipe(delay(100)))).subscribe(x => { + x(); +}); + +export function Flowbite() { + return function (target: any) { + const originalOnInit = target.prototype.ngOnInit; + target.prototype.ngOnInit = function () { + if (originalOnInit) { + originalOnInit.apply(this); + } + initFlowbiteFix(); + }; + }; +} + +export function initFlowbiteFix() { + flowbiteQueue.next(() => { + const elements = Array.from(document.querySelectorAll('*')); + const flowbiteElements: Element[] = []; + const initializedElements = Array.from(document.querySelectorAll('[flowbite-initialized]')); + + for (const element of elements) { + const attributes = Array.from(element.attributes); + + for (const attribute of attributes) { + if (attribute.name.startsWith('data-') && !initializedElements.includes(element)) { + flowbiteElements.push(element); + break; + } + } + } + + for (const element of flowbiteElements) { + element.setAttribute('flowbite-initialized', ''); + } + initFlowbite(); + + for (const element of flowbiteElements) { + const attributes: { name: string; value: string }[] = Array.from(element.attributes); + const dataAttributes = attributes.filter(attribute => attribute.name.startsWith('data-')); + + for (const attribute of dataAttributes) { + element.setAttribute(attribute.name.replace('data-', 'fb-'), attribute.value); + element.removeAttribute(attribute.name); + } + } + }); +}