Einbau klassische Filter als Overlay ...

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

View File

@ -12,7 +12,6 @@ import winston from 'winston';
import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js'; import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js';
import { emailToDirName, KeyValueStyle } from '../models/main.model.js'; import { emailToDirName, KeyValueStyle } from '../models/main.model.js';
import * as schema from './schema.js'; import * as schema from './schema.js';
import { users } from './schema.js';
const typesOfBusiness: Array<KeyValueStyle> = [ const typesOfBusiness: Array<KeyValueStyle> = [
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' }, { 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' }, { name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
@ -65,9 +64,9 @@ deleteFilesOfDir(targetPathProperty);
fs.ensureDirSync(`./pictures/logo`); fs.ensureDirSync(`./pictures/logo`);
fs.ensureDirSync(`./pictures/profile`); fs.ensureDirSync(`./pictures/profile`);
fs.ensureDirSync(`./pictures/property`); fs.ensureDirSync(`./pictures/property`);
type UserProfile = Omit<User, 'created' | 'updated' | 'hasCompanyLogo' | 'hasProfile' | 'id'>; // type UserProfile = Omit<User, 'created' | 'updated' | 'hasCompanyLogo' | 'hasProfile' | 'id'>;
type NewUser = typeof users.$inferInsert; // type NewUser = typeof users.$inferInsert;
//for (const userData of usersData) { //for (const userData of usersData) {
for (let index = 0; index < usersData.length; index++) { for (let index = 0; index < usersData.length; index++) {
const userData = usersData[index]; const userData = usersData[index];
@ -98,20 +97,17 @@ for (let index = 0; index < usersData.length; index++) {
user.customerSubType = 'broker'; user.customerSubType = 'broker';
user.created = new Date(); user.created = new Date();
user.updated = new Date(); user.updated = new Date();
const createUserProfile = (user: User): UserProfile => { // const createUserProfile = (user: User): UserProfile => {
const { id, created, updated, hasCompanyLogo, hasProfile, ...userProfile } = user; // const { id, created, updated, hasCompanyLogo, hasProfile, ...userProfile } = user;
return userProfile; // return userProfile;
}; // };
const userProfile = createUserProfile(user); // const userProfile = createUserProfile(user);
logger.info(`${index} - ${JSON.stringify(userProfile)}`); // logger.info(`${index} - ${JSON.stringify(userProfile)}`);
const embedding = await createEmbedding(JSON.stringify(userProfile)); // const embedding = await createEmbedding(JSON.stringify(userProfile));
sleep(200); //sleep(200);
const u = await db const u = await db
.insert(schema.users) .insert(schema.users)
.values({ .values(user)
...user,
embedding: embedding,
} as NewUser)
.returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname }); .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]); generatedUserData.push(u[0]);
i++; i++;
@ -145,25 +141,20 @@ for (let index = 0; index < commercialJsonData.length; index++) {
commercial.updated = insertionDate; commercial.updated = insertionDate;
commercial.email = user.email; commercial.email = user.email;
commercial.draft = false; commercial.draft = false;
const reducedCommercial = { commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercial.type)).value;
city: commercial.city, // const reducedCommercial = {
description: commercial.description, // city: commercial.city,
email: commercial.email, // description: commercial.description,
price: commercial.price, // email: commercial.email,
state: sso.locations.find(l => l.value === commercial.state)?.name, // price: commercial.price,
title: commercial.title, // state: sso.locations.find(l => l.value === commercial.state)?.name,
name: `${user.firstname} ${user.lastname}`, // title: commercial.title,
}; // name: `${user.firstname} ${user.lastname}`,
const embedding = await createEmbedding(JSON.stringify(reducedCommercial)); // };
sleep(200); // const embedding = await createEmbedding(JSON.stringify(reducedCommercial));
const result = await db // sleep(200);
.insert(schema.commercials) const result = await db.insert(schema.commercials).values(commercial).returning();
.values({ // logger.info(`commercial_${index} inserted`);
...commercial,
embedding: embedding,
})
.returning();
logger.info(`commercial_${index} inserted`);
try { try {
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`); fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
} catch (err) { } catch (err) {
@ -178,37 +169,34 @@ const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein
for (let index = 0; index < businessJsonData.length; index++) { for (let index = 0; index < businessJsonData.length; index++) {
const business = businessJsonData[index]; const business = businessJsonData[index];
delete business.id; delete business.id;
business.type = sso.typesOfBusiness.find(e => e.oldValue === String(business.type)).value;
business.created = new Date(business.created); business.created = new Date(business.created);
business.updated = new Date(business.created); business.updated = new Date(business.created);
const user = getRandomItem(generatedUserData); const user = getRandomItem(generatedUserData);
business.email = user.email; business.email = user.email;
business.imageName = emailToDirName(user.email); business.imageName = emailToDirName(user.email);
const embeddingText = JSON.stringify({ // const embeddingText = JSON.stringify({
type: typesOfBusiness.find(b => b.value === String(business.type))?.name, // type: typesOfBusiness.find(b => b.value === String(business.type))?.name,
title: business.title, // title: business.title,
description: business.description, // description: business.description,
email: business.email, // email: business.email,
city: business.city, // city: business.city,
state: sso.locations.find(l => l.value === business.state)?.name, // state: sso.locations.find(l => l.value === business.state)?.name,
price: business.price, // price: business.price,
realEstateIncluded: business.realEstateIncluded, // realEstateIncluded: business.realEstateIncluded,
leasedLocation: business.leasedLocation, // leasedLocation: business.leasedLocation,
franchiseResale: business.franchiseResale, // franchiseResale: business.franchiseResale,
salesRevenue: business.salesRevenue, // salesRevenue: business.salesRevenue,
cashFlow: business.cashFlow, // cashFlow: business.cashFlow,
supportAndTraining: business.supportAndTraining, // supportAndTraining: business.supportAndTraining,
employees: business.employees, // employees: business.employees,
established: business.established, // established: business.established,
reasonForSale: business.reasonForSale, // reasonForSale: business.reasonForSale,
name: `${user.firstname} ${user.lastname}`, // name: `${user.firstname} ${user.lastname}`,
}); // });
const embedding = await createEmbedding(embeddingText); // const embedding = await createEmbedding(embeddingText);
sleep(200); sleep(200);
await db.insert(schema.businesses).values({ await db.insert(schema.businesses).values(business);
...business,
embedding: embedding,
});
logger.info(`business_${index} inserted`);
} }
//End //End

View File

@ -0,0 +1,2 @@
ALTER TABLE "businesses" ALTER COLUMN "type" SET DATA TYPE varchar(255);--> statement-breakpoint
ALTER TABLE "commercials" ALTER COLUMN "type" SET DATA TYPE varchar(255);

View File

@ -0,0 +1,549 @@
{
"id": "93be31d4-beec-4ba8-8d4a-a52763342335",
"prevId": "2d8edad3-5544-4cb1-a543-84c07737ea9f",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"embedding": {
"name": "embedding",
"type": "vector(1536)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_email_users_email_fk": {
"name": "businesses_email_users_email_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"serial_id": {
"name": "serial_id",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"embedding": {
"name": "embedding",
"type": "vector(1536)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_email_users_email_fk": {
"name": "commercials_email_users_email_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gender": {
"name": "gender",
"type": "gender",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerType": {
"name": "customerType",
"type": "customerType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerSubType": {
"name": "customerSubType",
"type": "customerSubType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"embedding": {
"name": "embedding",
"type": "vector(1536)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
}
},
"enums": {
"public.customerSubType": {
"name": "customerSubType",
"schema": "public",
"values": [
"broker",
"cpa",
"attorney",
"titleCompany",
"surveyor",
"appraiser"
]
},
"public.customerType": {
"name": "customerType",
"schema": "public",
"values": [
"buyer",
"professional"
]
},
"public.gender": {
"name": "gender",
"schema": "public",
"values": [
"male",
"female"
]
}
},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -8,6 +8,13 @@
"when": 1720872296432, "when": 1720872296432,
"tag": "0000_slim_nova", "tag": "0000_slim_nova",
"breakpoints": true "breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1721134224160,
"tag": "0001_heavy_bloodscream",
"breakpoints": true
} }
] ]
} }

View File

@ -32,7 +32,7 @@ export const users = pgTable('users', {
export const businesses = pgTable('businesses', { export const businesses = pgTable('businesses', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
email: varchar('email', { length: 255 }).references(() => users.email), email: varchar('email', { length: 255 }).references(() => users.email),
type: integer('type'), type: varchar('type', { length: 255 }),
title: varchar('title', { length: 255 }), title: varchar('title', { length: 255 }),
description: text('description'), description: text('description'),
city: varchar('city', { length: 255 }), city: varchar('city', { length: 255 }),
@ -66,7 +66,7 @@ export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
serialId: serial('serial_id'), serialId: serial('serial_id'),
email: varchar('email', { length: 255 }).references(() => users.email), email: varchar('email', { length: 255 }).references(() => users.email),
type: integer('type'), type: varchar('type', { length: 255 }),
title: varchar('title', { length: 255 }), title: varchar('title', { length: 255 }),
description: text('description'), description: text('description'),
city: varchar('city', { length: 255 }), city: varchar('city', { length: 255 }),

View File

@ -3,7 +3,7 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { businesses } from '../drizzle/schema.js'; import { businesses } from '../drizzle/schema.js';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { JwtUser, ListingCriteria } from '../models/main.model.js'; import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
@Controller('listings/business') @Controller('listings/business')
@ -27,13 +27,13 @@ export class BusinessListingsController {
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Post('find') @Post('find')
find(@Request() req, @Body() criteria: ListingCriteria): any { find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser); return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
} }
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Post('search') @Post('search')
search(@Request() req, @Body() criteria: ListingCriteria): any { search(@Request() req, @Body() criteria: BusinessListingCriteria): any {
return this.listingsService.searchBusinessListings(criteria.prompt); return this.listingsService.searchBusinessListings(criteria.prompt);
} }

View File

@ -5,7 +5,7 @@ import { commercials } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { CommercialPropertyListing } from '../models/db.model'; import { CommercialPropertyListing } from '../models/db.model';
import { JwtUser, ListingCriteria } from '../models/main.model.js'; import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
@Controller('listings/commercialProperty') @Controller('listings/commercialProperty')
@ -29,7 +29,7 @@ export class CommercialPropertyListingsController {
} }
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Post('find') @Post('find')
async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> { async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser); return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
} }
@Get('states/all') @Get('states/all')

View File

@ -1,14 +1,14 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm'; import { and, eq, gte, ilike, inArray, lte, ne, or, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import OpenAI from 'openai'; import OpenAI from 'openai';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as schema from '../drizzle/schema.js'; import * as schema from '../drizzle/schema.js';
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js'; import { businesses, commercials, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { BusinessListing, CommercialPropertyListing } from '../models/db.model'; import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
import { JwtUser, ListingCriteria, emailToDirName } from '../models/main.model.js'; import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
@Injectable() @Injectable()
export class ListingsService { export class ListingsService {
@ -22,10 +22,10 @@ export class ListingsService {
apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen 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[] { private getConditions(criteria: BusinessListingCriteria | CommercialPropertyListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
const conditions = []; const conditions = [];
if (criteria.type) { if (criteria.types?.length > 0) {
conditions.push(eq(table.type, criteria.type)); conditions.push(inArray(table.type, criteria.types));
} }
if (criteria.state) { if (criteria.state) {
conditions.push(eq(table.state, criteria.state)); conditions.push(eq(table.state, criteria.state));
@ -36,9 +36,6 @@ export class ListingsService {
if (criteria.maxPrice) { if (criteria.maxPrice) {
conditions.push(lte(table.price, criteria.maxPrice)); conditions.push(lte(table.price, criteria.maxPrice));
} }
if (criteria.realEstateChecked) {
conditions.push(eq(businesses.realEstateIncluded, true));
}
if (criteria.title) { if (criteria.title) {
conditions.push(ilike(table.title, `%${criteria.title}%`)); conditions.push(ilike(table.title, `%${criteria.title}%`));
} }
@ -61,7 +58,7 @@ export class ListingsService {
return results as BusinessListing[]; return results as BusinessListing[];
} }
// #### Find by criteria ######################################## // #### Find by criteria ########################################
async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise<any> { async findCommercialPropertyListings(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0; const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12; const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria, commercials, user); const conditions = this.getConditions(criteria, commercials, user);
@ -83,7 +80,7 @@ export class ListingsService {
]); ]);
return { total, data }; return { total, data };
} }
async findBusinessListings(criteria: ListingCriteria, user: JwtUser): Promise<any> { async findBusinessListings(criteria: BusinessListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0; const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12; const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria, businesses, user); const conditions = this.getConditions(criteria, businesses, user);

View File

@ -45,7 +45,7 @@ export interface UserData {
export interface BusinessListing { export interface BusinessListing {
id: string; id: string;
email?: string; email?: string;
type?: number; type?: string;
title?: string; title?: string;
description?: string; description?: string;
city?: string; city?: string;
@ -77,7 +77,7 @@ export interface CommercialPropertyListing {
id: string; id: string;
serialId?: number; serialId?: number;
email?: string; email?: string;
type?: number; type?: string;
title?: string; title?: string;
description?: string; description?: string;
city?: string; city?: string;

View File

@ -16,6 +16,7 @@ export interface KeyValueRatio {
export interface KeyValueStyle { export interface KeyValueStyle {
name: string; name: string;
value: string; value: string;
oldValue?: string;
icon: string; icon: string;
textColorClass: string; textColorClass: string;
} }
@ -52,20 +53,51 @@ export type ResponseUsersArray = {
data: User[]; data: User[];
total: number; total: number;
}; };
export interface ListingCriteria { export interface ListCriteria {
start: number; start: number;
length: number; length: number;
page: number; page: number;
pageCount: number; pageCount: number;
type: number; city: string;
types: string[];
prompt: string;
criteriaType: 'business' | 'commercialProperty' | 'user';
}
export interface BusinessListingCriteria extends ListCriteria {
state: string; state: string;
county: string;
minPrice: number; minPrice: number;
maxPrice: number; maxPrice: number;
minRevenue: number;
maxRevenue: number;
minCashFlow: number;
maxCashFlow: number;
minNumberEmployees: number;
maxNumberEmployees: number;
establishedSince: number;
establishedUntil: number;
realEstateChecked: boolean; realEstateChecked: boolean;
leasedLocation: boolean;
franchiseResale: boolean;
title: string; title: string;
category: 'professional' | 'broker'; brokerName: string;
name: string; criteriaType: 'business';
prompt: string; }
export interface CommercialPropertyListingCriteria extends ListCriteria {
state: string;
county: string;
minPrice: number;
maxPrice: number;
title: string;
criteriaType: 'commercialProperty';
}
export interface UserListingCriteria extends ListCriteria {
firstname: string;
lastname: string;
companyName: string;
counties: string[];
states: string[];
criteriaType: 'user';
} }
export interface KeycloakUser { export interface KeycloakUser {

View File

@ -5,28 +5,28 @@ import { ImageType, KeyValue, KeyValueStyle } from '../models/main.model.js';
export class SelectOptionsService { export class SelectOptionsService {
constructor() {} constructor() {}
public typesOfBusiness: Array<KeyValueStyle> = [ public typesOfBusiness: Array<KeyValueStyle> = [
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' }, { name: 'Automotive', value: 'automotive', oldValue: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' }, { name: 'Industrial Services', value: 'industrialServices', oldValue: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
{ name: 'Real Estate', value: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' }, { name: 'Food and Restaurant', value: 'foodAndRestaurant', oldValue: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' },
{ name: 'Uncategorized', value: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' }, { name: 'Real Estate', value: 'realEstate', oldValue: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
{ name: 'Retail', value: '5', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' }, { name: 'Retail', value: 'retail', oldValue: '5', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' },
{ name: 'Oilfield SVE and MFG.', value: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' }, { name: 'Oilfield SVE and MFG.', value: 'oilfield', oldValue: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' },
{ name: 'Service', value: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' }, { name: 'Service', value: 'service', oldValue: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' },
{ name: 'Advertising', value: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' }, { name: 'Advertising', value: 'advertising', oldValue: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' },
{ name: 'Agriculture', value: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' }, { name: 'Agriculture', value: 'agriculture', oldValue: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' },
{ name: 'Franchise', value: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' }, { name: 'Franchise', value: 'franchise', oldValue: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' },
{ name: 'Professional', value: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' }, { name: 'Professional', value: 'professional', oldValue: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' },
{ name: 'Manufacturing', value: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' }, { name: 'Manufacturing', value: 'manufacturing', oldValue: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' },
{ name: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' }, { name: 'Uncategorized', value: 'uncategorized', oldValue: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' },
]; ];
public typesOfCommercialProperty: Array<KeyValueStyle> = [ public typesOfCommercialProperty: Array<KeyValueStyle> = [
{ name: 'Retail', value: '100', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' }, { name: 'Retail', value: 'retail', oldValue: '100', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' },
{ name: 'Land', value: '101', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' }, { name: 'Land', value: 'land', oldValue: '101', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
{ name: 'Industrial', value: '102', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' }, { name: 'Industrial', value: 'industrial', oldValue: '102', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
{ name: 'Office', value: '103', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' }, { name: 'Office', value: 'office', oldValue: '103', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' },
{ name: 'Mixed Use', value: '104', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' }, { name: 'Mixed Use', value: 'mixedUse', oldValue: '104', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' },
{ name: 'Multifamily', value: '105', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' }, { name: 'Multifamily', value: 'multifamily', oldValue: '105', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' },
{ name: 'Uncategorized', value: '106', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' }, { name: 'Uncategorized', value: 'uncategorized', oldValue: '106', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' },
]; ];
public prices: Array<KeyValue> = [ public prices: Array<KeyValue> = [
{ name: '$100K', value: '100000' }, { name: '$100K', value: '100000' },

View File

@ -7,7 +7,7 @@ import * as schema from '../drizzle/schema.js';
import { PG_CONNECTION } from '../drizzle/schema.js'; import { PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { User } from '../models/db.model.js'; import { User } from '../models/db.model.js';
import { ListingCriteria, emailToDirName } from '../models/main.model.js'; import { UserListingCriteria, emailToDirName } from '../models/main.model.js';
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -16,14 +16,14 @@ export class UserService {
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>, @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService: FileService, private fileService: FileService,
) {} ) {}
private getConditions(criteria: ListingCriteria): any[] { private getConditions(criteria: UserListingCriteria): any[] {
const conditions = []; const conditions = [];
if (criteria.state) { if (criteria.state) {
//conditions.push(sql`EXISTS (SELECT 1 FROM unnest(users."areasServed") AS area WHERE area LIKE '%' || ${criteria.state} || '%')`); //conditions.push(sql`EXISTS (SELECT 1 FROM unnest(users."areasServed") AS area WHERE area LIKE '%' || ${criteria.state} || '%')`);
conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: criteria.state }])}`); conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: criteria.state }])}`);
} }
if (criteria.name) { if (criteria.firstname || criteria.lastname) {
conditions.push(or(ilike(schema.users.firstname, `%${criteria.name}%`), ilike(schema.users.lastname, `%${criteria.name}%`))); conditions.push(or(ilike(schema.users.firstname, `%${criteria.lastname}%`), ilike(schema.users.lastname, `%${criteria.lastname}%`)));
} }
return conditions; return conditions;
} }
@ -60,7 +60,7 @@ export class UserService {
return newUser as User; return newUser as User;
} }
} }
async findUser(criteria: ListingCriteria) { async findUser(criteria: UserListingCriteria) {
const start = criteria.start ? criteria.start : 0; const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12; const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria); const conditions = this.getConditions(criteria);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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