Paginator & SQL Querries where clauses & city search

This commit is contained in:
Andreas Knuth 2024-07-18 19:02:32 +02:00
parent f88eebe8d3
commit abcde3991d
30 changed files with 850 additions and 421 deletions

View File

@ -7,10 +7,10 @@ import { join } from 'path';
import pkg from 'pg'; import pkg from 'pg';
import { rimraf } from 'rimraf'; import { rimraf } from 'rimraf';
import sharp from 'sharp'; import sharp from 'sharp';
import { SelectOptionsService } from 'src/select-options/select-options.service.js';
import winston from 'winston'; 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 { SelectOptionsService } from '../select-options/select-options.service.js';
import * as schema from './schema.js'; import * as schema 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' },

View File

@ -1,8 +1,9 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import path, { join } from 'path'; import path, { join } from 'path';
import { City, Geo, State } from 'src/models/server.model.js'; import { GeoResult } from 'src/models/main.model.js';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { City, Geo, State } from '../models/server.model.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -20,15 +21,16 @@ export class GeoService {
} }
findCitiesStartingWith(prefix: string, state?: string): { city: string; state: string; state_code: string }[] { findCitiesStartingWith(prefix: string, state?: string): { city: string; state: string; state_code: string }[] {
const result: { city: string; state: string; state_code: string }[] = []; const result: GeoResult[] = [];
this.geo.states.forEach((state: State) => { this.geo.states.forEach((state: State) => {
state.cities.forEach((city: City) => { state.cities.forEach((city: City) => {
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) { if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
result.push({ result.push({
id: city.id,
city: city.name, city: city.name,
state: state.name, state: state.name,
state_code: state.state_code state_code: state.state_code,
}); });
} }
}); });
@ -37,4 +39,3 @@ export class GeoService {
return state ? result.filter(e => e.state_code.toLowerCase() === state.toLowerCase()) : result; return state ? result.filter(e => e.state_code.toLowerCase() === state.toLowerCase()) : result;
} }
} }

View File

@ -3,14 +3,14 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { ListingsService } from '../listings/listings.service.js'; import { CommercialPropertyService } from '../listings/commercial-property.service.js';
import { SelectOptionsService } from '../select-options/select-options.service.js'; import { SelectOptionsService } from '../select-options/select-options.service.js';
@Controller('image') @Controller('image')
export class ImageController { export class ImageController {
constructor( constructor(
private fileService: FileService, private fileService: FileService,
private listingService: ListingsService, private listingService: CommercialPropertyService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
private selectOptions: SelectOptionsService, private selectOptions: SelectOptionsService,
) {} ) {}

View File

@ -1,14 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { FileService } from '../file/file.service.js';
import { ListingsModule } from '../listings/listings.module.js';
import { SelectOptionsService } from '../select-options/select-options.service.js';
import { ImageController } from './image.controller.js'; import { ImageController } from './image.controller.js';
import { ImageService } from './image.service.js'; import { ImageService } from './image.service.js';
import { FileService } from '../file/file.service.js';
import { SelectOptionsService } from '../select-options/select-options.service.js';
import { ListingsService } from '../listings/listings.service.js';
import { ListingsModule } from '../listings/listings.module.js';
@Module({ @Module({
imports: [ListingsModule], imports: [ListingsModule],
controllers: [ImageController], controllers: [ImageController],
providers: [ImageService,FileService,SelectOptionsService] providers: [ImageService, FileService, SelectOptionsService],
}) })
export class ImageModule {} export class ImageModule {}

View File

@ -1,21 +1,18 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Inject, Post } from '@nestjs/common';
import { FileService } from '../file/file.service.js';
import { convertStringToNullUndefined } from '../utils.js';
import { ListingsService } from './listings.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { UserListingCriteria } from 'src/models/main.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { UserService } from '../user/user.service.js'; import { UserService } from '../user/user.service.js';
@Controller('listings/professionals_brokers') @Controller('listings/professionals_brokers')
export class BrokerListingsController { export class BrokerListingsController {
constructor(
constructor(private readonly userService:UserService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { private readonly userService: UserService,
} @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
@Post('search') @Post('search')
find(@Body() criteria: any): any { find(@Body() criteria: UserListingCriteria): any {
return this.userService.findUser(criteria); return this.userService.searchUserListings(criteria);
} }
} }

View File

@ -0,0 +1,192 @@
import { Inject, Injectable } from '@nestjs/common';
import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import * as schema from '../drizzle/schema.js';
import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js';
import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
@Injectable()
export class BusinessListingService {
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService: FileService,
) {}
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
const whereConditions: SQL[] = [];
if (criteria.city) {
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(businesses.type, criteria.types));
}
if (criteria.state) {
whereConditions.push(eq(businesses.state, criteria.state));
}
if (criteria.county) {
whereConditions.push(ilike(businesses.city, `%${criteria.county}%`)); // Assuming county is part of city, adjust if necessary
}
if (criteria.minPrice) {
whereConditions.push(gte(businesses.price, criteria.minPrice));
}
if (criteria.maxPrice) {
whereConditions.push(lte(businesses.price, criteria.maxPrice));
}
if (criteria.minRevenue) {
whereConditions.push(gte(businesses.salesRevenue, criteria.minRevenue));
}
if (criteria.maxRevenue) {
whereConditions.push(lte(businesses.salesRevenue, criteria.maxRevenue));
}
if (criteria.minCashFlow) {
whereConditions.push(gte(businesses.cashFlow, criteria.minCashFlow));
}
if (criteria.maxCashFlow) {
whereConditions.push(lte(businesses.cashFlow, criteria.maxCashFlow));
}
if (criteria.minNumberEmployees) {
whereConditions.push(gte(businesses.employees, criteria.minNumberEmployees));
}
if (criteria.maxNumberEmployees) {
whereConditions.push(lte(businesses.employees, criteria.maxNumberEmployees));
}
if (criteria.establishedSince) {
whereConditions.push(gte(businesses.established, criteria.establishedSince));
}
if (criteria.establishedUntil) {
whereConditions.push(lte(businesses.established, criteria.establishedUntil));
}
if (criteria.realEstateChecked) {
whereConditions.push(eq(businesses.realEstateIncluded, criteria.realEstateChecked));
}
if (criteria.leasedLocation) {
whereConditions.push(eq(businesses.leasedLocation, criteria.leasedLocation));
}
if (criteria.franchiseResale) {
whereConditions.push(eq(businesses.franchiseResale, criteria.franchiseResale));
}
if (criteria.title) {
whereConditions.push(or(ilike(businesses.title, `%${criteria.title}%`), ilike(businesses.description, `%${criteria.title}%`)));
}
if (criteria.brokerName) {
whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${criteria.brokerName}%`)));
}
return whereConditions;
}
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const query = this.conn
.select({
business: businesses,
brokerFirstName: schema.users.firstname,
brokerLastName: schema.users.lastname,
})
.from(businesses)
.leftJoin(schema.users, eq(businesses.email, schema.users.email));
const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
query.where(whereClause);
}
// Paginierung
query.limit(length).offset(start);
const data = await query;
const totalCount = await this.getBusinessListingsCount(criteria);
const results = data.map(r => r.business);
return {
results,
totalCount,
};
}
async getBusinessListingsCount(criteria: BusinessListingCriteria): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(businesses).leftJoin(schema.users, eq(businesses.email, schema.users.email));
const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
countQuery.where(whereClause);
}
const [{ value: totalCount }] = await countQuery;
return totalCount;
}
async findBusinessesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
let result = await this.conn
.select()
.from(businesses)
.where(and(sql`${businesses.id} = ${id}`));
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
return result[0] as BusinessListing;
}
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
const conditions = [];
conditions.push(eq(businesses.imageName, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(businesses.draft, true));
}
return (await this.conn
.select()
.from(businesses)
.where(and(...conditions))) as CommercialPropertyListing[];
}
// #### CREATE ########################################
async createListing(data: BusinessListing): Promise<BusinessListing> {
data.created = new Date();
data.updated = new Date();
const [createdListing] = await this.conn.insert(businesses).values(data).returning();
return createdListing as BusinessListing;
}
// #### UPDATE Business ########################################
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
data.updated = new Date();
data.created = new Date(data.created);
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
return updateListing as BusinessListing | CommercialPropertyListing;
}
// #### DELETE ########################################
async deleteListing(id: string): Promise<void> {
await this.conn.delete(businesses).where(eq(businesses.id, id));
}
// ##############################################################
// States
// ##############################################################
async getStates(): Promise<any[]> {
return await this.conn
.select({ state: businesses.state, count: sql<number>`count(${businesses.id})`.mapWith(Number) })
.from(businesses)
.groupBy(sql`${businesses.state}`)
.orderBy(sql`count desc`);
}
}

View File

@ -1,15 +1,14 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
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 { BusinessListingCriteria, JwtUser } from '../models/main.model.js'; import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
import { ListingsService } from './listings.service.js'; import { BusinessListingService } from './business-listing.service.js';
@Controller('listings/business') @Controller('listings/business')
export class BusinessListingsController { export class BusinessListingsController {
constructor( constructor(
private readonly listingsService: ListingsService, private readonly listingsService: BusinessListingService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@ -28,19 +27,19 @@ export class BusinessListingsController {
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Post('find') @Post('find')
find(@Request() req, @Body() criteria: BusinessListingCriteria): any { find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser); return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
} }
@UseGuards(OptionalJwtAuthGuard) // @UseGuards(OptionalJwtAuthGuard)
@Post('search') // @Post('search')
search(@Request() req, @Body() criteria: BusinessListingCriteria): any { // search(@Request() req, @Body() criteria: BusinessListingCriteria): any {
return this.listingsService.searchBusinessListings(criteria.prompt); // return this.listingsService.searchBusinessListings(criteria.prompt);
} // }
@Post() @Post()
create(@Body() listing: any) { create(@Body() listing: any) {
this.logger.info(`Save Listing`); this.logger.info(`Save Listing`);
return this.listingsService.createListing(listing, businesses); return this.listingsService.createListing(listing);
} }
@Put() @Put()
update(@Body() listing: any) { update(@Body() listing: any) {
@ -49,10 +48,10 @@ export class BusinessListingsController {
} }
@Delete(':id') @Delete(':id')
deleteById(@Param('id') id: string) { deleteById(@Param('id') id: string) {
this.listingsService.deleteListing(id, businesses); this.listingsService.deleteListing(id);
} }
@Get('states/all') @Get('states/all')
getStates(): any { getStates(): any {
return this.listingsService.getStates(businesses); return this.listingsService.getStates();
} }
} }

View File

@ -1,17 +1,16 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
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 { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model.js'; import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model.js';
import { ListingsService } from './listings.service.js'; import { CommercialPropertyService } from './commercial-property.service.js';
@Controller('listings/commercialProperty') @Controller('listings/commercialProperty')
export class CommercialPropertyListingsController { export class CommercialPropertyListingsController {
constructor( constructor(
private readonly listingsService: ListingsService, private readonly listingsService: CommercialPropertyService,
private fileService: FileService, private fileService: FileService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@ -30,16 +29,16 @@ export class CommercialPropertyListingsController {
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Post('find') @Post('find')
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): 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.searchCommercialProperties(criteria, req.user as JwtUser);
} }
@Get('states/all') @Get('states/all')
getStates(): any { getStates(): any {
return this.listingsService.getStates(commercials); return this.listingsService.getStates();
} }
@Post() @Post()
async create(@Body() listing: any) { async create(@Body() listing: any) {
this.logger.info(`Save Listing`); this.logger.info(`Save Listing`);
return await this.listingsService.createListing(listing, commercials); return await this.listingsService.createListing(listing);
} }
@Put() @Put()
async update(@Body() listing: any) { async update(@Body() listing: any) {
@ -48,7 +47,7 @@ export class CommercialPropertyListingsController {
} }
@Delete(':id/:imagePath') @Delete(':id/:imagePath')
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) { deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
this.listingsService.deleteListing(id, commercials); this.listingsService.deleteListing(id);
this.fileService.deleteDirectoryIfExists(imagePath); this.fileService.deleteDirectoryIfExists(imagePath);
} }
} }

View File

@ -0,0 +1,168 @@
import { Inject, Injectable } from '@nestjs/common';
import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import * as schema from '../drizzle/schema.js';
import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js';
import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
@Injectable()
export class CommercialPropertyService {
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService: FileService,
) {}
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
const whereConditions: SQL[] = [];
if (criteria.city) {
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(schema.commercials.type, criteria.types));
}
if (criteria.state) {
whereConditions.push(eq(schema.commercials.state, criteria.state));
}
if (criteria.county) {
whereConditions.push(ilike(schema.commercials.county, `%${criteria.county}%`));
}
if (criteria.minPrice) {
whereConditions.push(gte(schema.commercials.price, criteria.minPrice));
}
if (criteria.maxPrice) {
whereConditions.push(lte(schema.commercials.price, criteria.maxPrice));
}
if (criteria.title) {
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
}
return whereConditions;
}
// #### Find by criteria ########################################
async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const query = this.conn.select().from(schema.commercials);
const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
query.where(whereClause);
}
// Paginierung
query.limit(length).offset(start);
const results = await query;
const totalCount = await this.getCommercialPropertiesCount(criteria);
return {
results,
totalCount,
};
}
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(schema.commercials);
const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
countQuery.where(whereClause);
}
const [{ value: totalCount }] = await countQuery;
return totalCount;
}
// #### Find by ID ########################################
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
let result = await this.conn
.select()
.from(commercials)
.where(and(sql`${commercials.id} = ${id}`));
result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
return result[0] as CommercialPropertyListing;
}
// #### Find by User EMail ########################################
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
const conditions = [];
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(commercials.draft, true));
}
return (await this.conn
.select()
.from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[];
}
// #### Find by imagePath ########################################
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
const result = await this.conn
.select()
.from(commercials)
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
return result[0] as CommercialPropertyListing;
}
// #### CREATE ########################################
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
data.created = new Date();
data.updated = new Date();
const [createdListing] = await this.conn.insert(commercials).values(data).returning();
return createdListing as CommercialPropertyListing;
}
// #### UPDATE CommercialProps ########################################
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
data.updated = new Date();
data.created = new Date(data.created);
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
if (difference.length > 0) {
this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
data.imageOrder = imageOrder;
}
const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
return updateListing as BusinessListing | CommercialPropertyListing;
}
// ##############################################################
// Images for commercial Properties
// ##############################################################
async deleteImage(imagePath: string, serial: string, name: string) {
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) {
listing.imageOrder.splice(index, 1);
await this.updateCommercialPropertyListing(listing.id, listing);
}
}
async addImage(imagePath: string, serial: string, imagename: string) {
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
listing.imageOrder.push(imagename);
await this.updateCommercialPropertyListing(listing.id, listing);
}
// #### DELETE ########################################
async deleteListing(id: string): Promise<void> {
await this.conn.delete(commercials).where(eq(commercials.id, id));
}
// ##############################################################
// States
// ##############################################################
async getStates(): Promise<any[]> {
return await this.conn
.select({ state: commercials.state, count: sql<number>`count(${commercials.id})`.mapWith(Number) })
.from(commercials)
.groupBy(sql`${commercials.state}`)
.orderBy(sql`count desc`);
}
}

View File

@ -6,13 +6,15 @@ import { UserService } from '../user/user.service.js';
import { BrokerListingsController } from './broker-listings.controller.js'; import { BrokerListingsController } from './broker-listings.controller.js';
import { BusinessListingsController } from './business-listings.controller.js'; import { BusinessListingsController } from './business-listings.controller.js';
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js'; import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
import { ListingsService } from './listings.service.js';
import { BusinessListingService } from './business-listing.service.js';
import { CommercialPropertyService } from './commercial-property.service.js';
import { UnknownListingsController } from './unknown-listings.controller.js'; import { UnknownListingsController } from './unknown-listings.controller.js';
@Module({ @Module({
imports: [DrizzleModule, AuthModule], imports: [DrizzleModule, AuthModule],
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController], controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
providers: [ListingsService, FileService, UserService], providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService],
exports: [ListingsService], exports: [BusinessListingService, CommercialPropertyService],
}) })
export class ListingsModule {} export class ListingsModule {}

View File

@ -1,225 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { and, eq, gte, ilike, inArray, lte, ne, or, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import OpenAI from 'openai';
import { Logger } from 'winston';
import * as schema from '../drizzle/schema.js';
import { businesses, commercials, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js';
import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
@Injectable()
export class ListingsService {
openai: OpenAI;
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService: FileService,
) {
this.openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen
});
}
private getConditions(criteria: BusinessListingCriteria | CommercialPropertyListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
const conditions = [];
if (criteria.types?.length > 0) {
conditions.push(inArray(table.type, criteria.types));
}
if (criteria.state) {
conditions.push(eq(table.state, criteria.state));
}
if (criteria.minPrice) {
conditions.push(gte(table.price, criteria.minPrice));
}
if (criteria.maxPrice) {
conditions.push(lte(table.price, criteria.maxPrice));
}
if (criteria.title) {
conditions.push(ilike(table.title, `%${criteria.title}%`));
}
return conditions;
}
// ##############################################################
// Listings general
// ##############################################################
// #### Find by embeddng ########################################
async searchBusinessListings(query: string, limit: number = 20): Promise<BusinessListing[]> {
const queryEmbedding = await this.createEmbedding(query);
const results = await this.conn
.select()
.from(businesses)
.orderBy(sql`embedding <-> ${JSON.stringify(queryEmbedding)}`)
.limit(limit);
return results as BusinessListing[];
}
// #### Find by criteria ########################################
async findCommercialPropertyListings(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria, commercials, user);
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(or(eq(commercials.draft, false), eq(commercials.imagePath, emailToDirName(user?.username))));
}
const [data, total] = await Promise.all([
this.conn
.select()
.from(commercials)
.where(and(...conditions))
.offset(start)
.limit(length),
this.conn
.select({ count: sql`count(*)` })
.from(commercials)
.where(and(...conditions))
.then(result => Number(result[0].count)),
]);
return { total, data };
}
async findBusinessListings(criteria: BusinessListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria, businesses, user);
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(or(eq(businesses.draft, false), eq(businesses.imageName, emailToDirName(user?.username))));
}
const [data, total] = await Promise.all([
this.conn
.select()
.from(businesses)
.where(and(...conditions))
.offset(start)
.limit(length),
this.conn
.select({ count: sql`count(*)` })
.from(businesses)
.where(and(...conditions))
.then(result => Number(result[0].count)),
]);
return { total, data };
}
// #### Find by ID ########################################
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
let result = await this.conn
.select()
.from(commercials)
.where(and(sql`${commercials.id} = ${id}`));
result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
return result[0] as CommercialPropertyListing;
}
async findBusinessesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
let result = await this.conn
.select()
.from(businesses)
.where(and(sql`${businesses.id} = ${id}`));
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
return result[0] as BusinessListing;
}
// #### Find by User EMail ########################################
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
const conditions = [];
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(commercials.draft, true));
}
return (await this.conn
.select()
.from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[];
}
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
const conditions = [];
conditions.push(eq(businesses.imageName, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(businesses.draft, true));
}
return (await this.conn
.select()
.from(businesses)
.where(and(...conditions))) as CommercialPropertyListing[];
}
// #### Find by imagePath ########################################
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
const result = await this.conn
.select()
.from(commercials)
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
return result[0] as CommercialPropertyListing;
}
// #### CREATE ########################################
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
data.created = new Date();
data.updated = new Date();
const [createdListing] = await this.conn.insert(table).values(data).returning();
return createdListing as BusinessListing | CommercialPropertyListing;
}
// #### UPDATE CommercialProps ########################################
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
data.updated = new Date();
data.created = new Date(data.created);
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
if (difference.length > 0) {
this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
data.imageOrder = imageOrder;
}
const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
return updateListing as BusinessListing | CommercialPropertyListing;
}
// #### UPDATE Business ########################################
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
data.updated = new Date();
data.created = new Date(data.created);
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
return updateListing as BusinessListing | CommercialPropertyListing;
}
// #### DELETE ########################################
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
await this.conn.delete(table).where(eq(table.id, id));
}
// ##############################################################
// Images for commercial Properties
// ##############################################################
async deleteImage(imagePath: string, serial: string, name: string) {
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) {
listing.imageOrder.splice(index, 1);
await this.updateCommercialPropertyListing(listing.id, listing);
}
}
async addImage(imagePath: string, serial: string, imagename: string) {
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
listing.imageOrder.push(imagename);
await this.updateCommercialPropertyListing(listing.id, listing);
}
// ##############################################################
// States
// ##############################################################
async getStates(table: typeof businesses | typeof commercials): Promise<any[]> {
return await this.conn
.select({ state: table.state, count: sql<number>`count(${table.id})`.mapWith(Number) })
.from(table)
.groupBy(sql`${table.state}`)
.orderBy(sql`count desc`);
}
// ##############################################################
// Embedding
// ##############################################################
async createEmbedding(text: string): Promise<number[]> {
const response = await this.openai.embeddings.create({
model: 'text-embedding-3-small',
input: text,
});
return response.data[0].embedding;
}
}

View File

@ -1,14 +1,10 @@
import { Controller, Inject } from '@nestjs/common'; import { Controller, Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ListingsService } from './listings.service.js';
@Controller('listings/undefined') @Controller('listings/undefined')
export class UnknownListingsController { export class UnknownListingsController {
constructor( constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
private readonly listingsService: ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
// @Get(':id') // @Get(':id')
// async findById(@Param('id') id: string): Promise<any> { // async findById(@Param('id') id: string): Promise<any> {

View File

@ -36,32 +36,32 @@ export type ListingCategory = {
export type ListingType = BusinessListing | CommercialPropertyListing; export type ListingType = BusinessListing | CommercialPropertyListing;
export type ResponseBusinessListingArray = { export type ResponseBusinessListingArray = {
data: BusinessListing[]; results: BusinessListing[];
total: number; totalCount: number;
}; };
export type ResponseBusinessListing = { export type ResponseBusinessListing = {
data: BusinessListing; data: BusinessListing;
}; };
export type ResponseCommercialPropertyListingArray = { export type ResponseCommercialPropertyListingArray = {
data: CommercialPropertyListing[]; results: CommercialPropertyListing[];
total: number; totalCount: number;
}; };
export type ResponseCommercialPropertyListing = { export type ResponseCommercialPropertyListing = {
data: CommercialPropertyListing; data: CommercialPropertyListing;
}; };
export type ResponseUsersArray = { export type ResponseUsersArray = {
data: User[]; results: User[];
total: number; totalCount: number;
}; };
export interface ListCriteria { export interface ListCriteria {
start: number; start: number;
length: number; length: number;
page: number; page: number;
pageCount: number; pageCount: number;
city: string;
types: string[]; types: string[];
city: string;
prompt: string; prompt: string;
criteriaType: 'business' | 'commercialProperty' | 'user'; criteriaType: 'business' | 'commercialProperty' | 'broker';
} }
export interface BusinessListingCriteria extends ListCriteria { export interface BusinessListingCriteria extends ListCriteria {
state: string; state: string;
@ -97,7 +97,7 @@ export interface UserListingCriteria extends ListCriteria {
companyName: string; companyName: string;
counties: string[]; counties: string[];
states: string[]; states: string[];
criteriaType: 'user'; criteriaType: 'broker';
} }
export interface KeycloakUser { export interface KeycloakUser {
@ -224,6 +224,12 @@ export interface UploadParams {
imagePath: string; imagePath: string;
serialId?: number; serialId?: number;
} }
export interface GeoResult {
id: number;
city: string;
state: string;
state_code: string;
}
export function isEmpty(value: any): boolean { export function isEmpty(value: any): boolean {
// Check for undefined or null // Check for undefined or null
if (value === undefined || value === null) { if (value === undefined || value === null) {
@ -258,3 +264,4 @@ export function emailToDirName(email: string): string {
return normalizedEmail; return normalizedEmail;
} }
export const LISTINGS_PER_PAGE = 12;

View File

@ -4,7 +4,7 @@ import { Logger } from 'winston';
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 { User } from '../models/db.model'; import { User } from '../models/db.model';
import { JwtUser, Subscription } from '../models/main.model.js'; import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model.js';
import { UserService } from './user.service.js'; import { UserService } from './user.service.js';
@Controller('user') @Controller('user')
@ -39,9 +39,9 @@ export class UserController {
} }
@Post('search') @Post('search')
find(@Body() criteria: any): any { find(@Body() criteria: UserListingCriteria): any {
this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`); this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
const foundUsers = this.userService.findUser(criteria); const foundUsers = this.userService.searchUserListings(criteria);
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`); this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
return foundUsers; return foundUsers;
} }

View File

@ -1,14 +1,15 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { and, eq, ilike, or, sql } from 'drizzle-orm'; import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js'; import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
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 } from '../drizzle/schema.js'; import { customerSubTypeEnum, 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 { JwtUser, UserListingCriteria, emailToDirName } from '../models/main.model.js'; import { emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
@Injectable() @Injectable()
export class UserService { export class UserService {
constructor( constructor(
@ -16,17 +17,85 @@ 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: UserListingCriteria): any[] { // private getConditions(criteria: UserListingCriteria): any[] {
const conditions = []; // const conditions = [];
if (criteria.states?.length > 0) { // if (criteria.states?.length > 0) {
criteria.states.forEach(state => { // criteria.states.forEach(state => {
conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: state }])}`); // conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: state }])}`);
}); // });
// }
// if (criteria.firstname || criteria.lastname) {
// conditions.push(or(ilike(schema.users.firstname, `%${criteria.lastname}%`), ilike(schema.users.lastname, `%${criteria.lastname}%`)));
// }
// return conditions;
// }
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
const whereConditions: SQL[] = [];
if (criteria.city) {
whereConditions.push(ilike(schema.users.companyLocation, `%${criteria.city}%`));
} }
if (criteria.firstname || criteria.lastname) {
conditions.push(or(ilike(schema.users.firstname, `%${criteria.lastname}%`), ilike(schema.users.lastname, `%${criteria.lastname}%`))); if (criteria.types && criteria.types.length > 0) {
// whereConditions.push(inArray(schema.users.customerSubType, criteria.types));
whereConditions.push(inArray(schema.users.customerSubType, criteria.types as CustomerSubType[]));
} }
return conditions;
if (criteria.firstname) {
whereConditions.push(ilike(schema.users.firstname, `%${criteria.firstname}%`));
}
if (criteria.lastname) {
whereConditions.push(ilike(schema.users.lastname, `%${criteria.lastname}%`));
}
if (criteria.companyName) {
whereConditions.push(ilike(schema.users.companyName, `%${criteria.companyName}%`));
}
if (criteria.counties && criteria.counties.length > 0) {
whereConditions.push(or(...criteria.counties.map(county => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'county' ILIKE ${`%${county}%`})`)));
}
if (criteria.states && criteria.states.length > 0) {
whereConditions.push(or(...criteria.states.map(state => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${state})`)));
}
return whereConditions;
}
async searchUserListings(criteria: UserListingCriteria) {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const query = this.conn.select().from(schema.users);
const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
query.where(whereClause);
}
// Paginierung
query.limit(length).offset(start);
const results = await query;
const totalCount = await this.getUserListingsCount(criteria);
return {
results,
totalCount,
};
}
async getUserListingsCount(criteria: UserListingCriteria): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(schema.users);
const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) {
const whereClause = and(...whereConditions);
countQuery.where(whereClause);
}
const [{ value: totalCount }] = await countQuery;
return totalCount;
} }
async getUserByMail(email: string, jwtuser?: JwtUser) { async getUserByMail(email: string, jwtuser?: JwtUser) {
const users = (await this.conn const users = (await this.conn
@ -68,25 +137,25 @@ export class UserService {
return newUser as User; return newUser as User;
} }
} }
async findUser(criteria: UserListingCriteria) { // 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);
const [data, total] = await Promise.all([ // const [data, total] = await Promise.all([
this.conn // this.conn
.select() // .select()
.from(schema.users) // .from(schema.users)
.where(and(...conditions)) // .where(and(...conditions))
.offset(start) // .offset(start)
.limit(length), // .limit(length),
this.conn // this.conn
.select({ count: sql`count(*)` }) // .select({ count: sql`count(*)` })
.from(schema.users) // .from(schema.users)
.where(and(...conditions)) // .where(and(...conditions))
.then(result => Number(result[0].count)), // .then(result => Number(result[0].count)),
]); // ]);
return { total, data }; // return { total, data };
} // }
async getStates(): Promise<any[]> { async getStates(): Promise<any[]> {
const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`; const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`;
const result = await this.conn.execute(query); const result = await this.conn.execute(query);

View File

@ -11,9 +11,10 @@ import { filter, Observable, Subject, Subscription } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model'; import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { SearchService } from '../../services/search.service';
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 { createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaStateObject, getSessionStorageHandlerWrapper, map2User } from '../../utils/utils'; import { getCriteriaStateObject, getSessionStorageHandlerWrapper, map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component'; import { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service'; import { ModalService } from '../search-modal/modal.service';
@Component({ @Component({
@ -46,9 +47,8 @@ export class HeaderComponent {
private sharedService: SharedService, private sharedService: SharedService,
private breakpointObserver: BreakpointObserver, private breakpointObserver: BreakpointObserver,
private modalService: ModalService, private modalService: ModalService,
) { private searchService: SearchService,
//this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandlerWrapper(this.activeTabAction)); ) {}
}
async ngOnInit() { async ngOnInit() {
const token = await this.keycloakService.getToken(); const token = await this.keycloakService.getToken();
@ -89,14 +89,18 @@ export class HeaderComponent {
} }
ngAfterViewInit() {} ngAfterViewInit() {}
openModal() { async openModal() {
if (this.isActive('/businessListings')) { const accepted = await this.modalService.showModal(this.criteria);
this.modalService.showModal(createEmptyBusinessListingCriteria()); if (accepted) {
} else if (this.isActive('/commercialPropertyListings')) { this.searchService.search(this.criteria);
this.modalService.showModal(createEmptyCommercialPropertyListingCriteria());
} else if (this.isActive('/brokerListings')) {
this.modalService.showModal(createEmptyUserListingCriteria());
} }
// 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 @@
<p>paginator works!</p>

View File

@ -0,0 +1,98 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-paginator',
standalone: true,
imports: [CommonModule],
template: `
<nav class="my-2" aria-label="Page navigation">
<ul class="flex justify-center items-center -space-x-px h-8 text-sm">
<li>
<a
(click)="onPageChange(currentPage - 1)"
[class.pointer-events-none]="currentPage === 1"
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
<span class="sr-only">Previous</span>
<svg class="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 1 1 5l4 4" />
</svg>
</a>
</li>
<ng-container *ngFor="let page of visiblePages">
<li *ngIf="page !== '...'">
<a
(click)="onPageChange(page)"
[ngClass]="
page === currentPage
? 'z-10 flex items-center justify-center px-3 h-8 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white'
: 'flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
"
>
{{ page }}
</a>
</li>
<li *ngIf="page === '...'">
<span class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400">...</span>
</li>
</ng-container>
<li>
<a
(click)="onPageChange(currentPage + 1)"
[class.pointer-events-none]="currentPage === pageCount"
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
>
<span class="sr-only">Next</span>
<svg class="w-2.5 h-2.5 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 6 10">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 9 4-4-4-4" />
</svg>
</a>
</li>
</ul>
</nav>
`,
})
export class PaginatorComponent implements OnChanges {
@Input() page = 1;
@Input() pageCount = 1;
@Output() pageChange = new EventEmitter<number>();
currentPage = 1;
visiblePages: (number | string)[] = [];
ngOnChanges(changes: SimpleChanges): void {
if (changes['page'] || changes['pageCount']) {
this.currentPage = this.page;
this.updateVisiblePages();
}
}
updateVisiblePages(): void {
const totalPages = this.pageCount;
const current = this.currentPage;
if (totalPages <= 6) {
this.visiblePages = Array.from({ length: totalPages }, (_, i) => i + 1);
} else {
if (current <= 3) {
this.visiblePages = [1, 2, 3, 4, '...', totalPages];
} else if (current >= totalPages - 2) {
this.visiblePages = [1, '...', totalPages - 3, totalPages - 2, totalPages - 1, totalPages];
} else {
this.visiblePages = [1, '...', current - 1, current, current + 1, '...', totalPages];
}
}
}
onPageChange(page: number | string): void {
if (typeof page === 'string') {
return;
}
if (page >= 1 && page <= this.pageCount && page !== this.currentPage) {
this.currentPage = page;
this.pageChange.emit(page);
this.updateVisiblePages();
}
}
}

View File

@ -28,13 +28,28 @@
</div> </div>
<div> <div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label> <label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<input <!-- <input
type="text" type="text"
id="city" id="city"
[(ngModel)]="criteria.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" 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" placeholder="e.g. Houston"
/> /> -->
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
[(ngModel)]="criteria.city"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
}
</ng-select>
</div> </div>
<div> <div>
@ -205,13 +220,28 @@
</div> </div>
<div> <div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label> <label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<input <!-- <input
type="text" type="text"
id="city" id="city"
[(ngModel)]="criteria.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" 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" placeholder="e.g. Houston"
/> /> -->
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
[(ngModel)]="criteria.city"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
}
</ng-select>
</div> </div>
<div> <div>
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label> <label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
@ -265,7 +295,7 @@
</div> </div>
</div> </div>
</div> </div>
} @if(criteria.criteriaType==='user'){ } @if(criteria.criteriaType==='broker'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
@ -280,13 +310,28 @@
</div> </div>
<div> <div>
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label> <label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
<input <!-- <input
type="text" type="text"
id="city" id="city"
[(ngModel)]="criteria.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" 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" placeholder="e.g. Houston"
/> /> -->
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
[(ngModel)]="criteria.city"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
}
</ng-select>
</div> </div>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">

View File

@ -3,4 +3,7 @@
background-color: rgb(249 250 251 / var(--tw-bg-opacity)); background-color: rgb(249 250 251 / var(--tw-bg-opacity));
height: 46px; height: 46px;
border-radius: 0.5rem; border-radius: 0.5rem;
.ng-value-container .ng-input {
top: 10px;
}
} }

View File

@ -1,7 +1,9 @@
import { AsyncPipe, NgIf } from '@angular/common'; import { AsyncPipe, NgIf } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, GeoResult, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { GeoService } from '../../services/geo.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { SharedModule } from '../../shared/shared/shared.module'; import { SharedModule } from '../../shared/shared/shared.module';
import { ModalService } from './modal.service'; import { ModalService } from './modal.service';
@ -14,11 +16,15 @@ import { ModalService } from './modal.service';
styleUrl: './search-modal.component.scss', styleUrl: './search-modal.component.scss',
}) })
export class SearchModalComponent { export class SearchModalComponent {
constructor(public selectOptions: SelectOptionsService, public modalService: ModalService) {} cities$: Observable<GeoResult[]>;
cityLoading = false;
cityInput$ = new Subject<string>();
constructor(public selectOptions: SelectOptionsService, public modalService: ModalService, private geoService: GeoService) {}
ngOnInit() { ngOnInit() {
this.modalService.message$.subscribe(msg => { this.modalService.message$.subscribe(msg => {
this.criteria = msg; this.criteria = msg;
}); });
this.loadCities();
} }
public criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria; public criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
@ -32,6 +38,25 @@ export class SearchModalComponent {
} }
} }
} }
private loadCities() {
this.cities$ = concat(
of([]), // default items
this.cityInput$.pipe(
distinctUntilChanged(),
tap(() => (this.cityLoading = true)),
switchMap(term =>
this.geoService.findCitiesStartingWith(term).pipe(
catchError(() => of([])), // empty list on error
// map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names
tap(() => (this.cityLoading = false)),
),
),
),
);
}
trackByFn(item: GeoResult) {
return item.id;
}
search() { search() {
console.log('Search criteria:', this.criteria); console.log('Search criteria:', this.criteria);
} }

View File

@ -2,15 +2,15 @@ import { CommonModule, NgOptimizedImage } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import 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 { ListingType, UserListingCriteria, 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 { SearchService } from '../../../services/search.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { getCriteriaStateObject, getSessionStorageHandlerWrapper } from '../../../utils/utils'; import { getCriteriaStateObject } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-broker-listings', selector: 'app-broker-listings',
@ -48,15 +48,23 @@ export class BrokerListingsComponent {
private cdRef: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private imageService: ImageService, private imageService: ImageService,
private route: ActivatedRoute, private route: ActivatedRoute,
private searchService: SearchService,
) { ) {
this.criteria = onChange(getCriteriaStateObject('broker'), getSessionStorageHandlerWrapper('broker')); this.criteria = getCriteriaStateObject('broker');
this.route.data.subscribe(async () => { // this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) { // if (this.router.getCurrentNavigation().extras.state) {
} 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;
} // }
// this.init();
// });
this.init(); this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'broker') {
this.criteria = criteria as UserListingCriteria;
this.search();
}
}); });
} }
async ngOnInit() { async ngOnInit() {
@ -73,8 +81,8 @@ export class BrokerListingsComponent {
} }
async search() { async search() {
const usersReponse = await this.userService.search(this.criteria); const usersReponse = await this.userService.search(this.criteria);
this.users = usersReponse.data; this.users = usersReponse.results;
this.totalRecords = usersReponse.total; this.totalRecords = usersReponse.totalCount;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }

View File

@ -112,6 +112,7 @@
} }
</div> </div>
</div> </div>
<app-paginator [page]="page" [pageCount]="pageCount" (pageChange)="onPageChange($event)"></app-paginator>
<!-- <div class="container mx-auto px-4 py-8"> <!-- <div class="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">

View File

@ -2,19 +2,20 @@ import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import onChange from 'on-change';
import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model'; import { BusinessListingCriteria, LISTINGS_PER_PAGE, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { PaginatorComponent } from '../../../components/paginator/paginator.component';
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 { SearchService } from '../../../services/search.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { getCriteriaStateObject, getSessionStorageHandlerWrapper } from '../../../utils/utils'; import { getCriteriaStateObject } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-business-listings', selector: 'app-business-listings',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule, RouterModule], imports: [CommonModule, FormsModule, RouterModule, PaginatorComponent],
templateUrl: './business-listings.component.html', templateUrl: './business-listings.component.html',
styleUrls: ['./business-listings.component.scss', '../../pages.scss'], styleUrls: ['./business-listings.component.scss', '../../pages.scss'],
}) })
@ -35,6 +36,8 @@ export class BusinessListingsComponent {
rows: number = 12; rows: number = 12;
env = environment; env = environment;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined; public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
page = 1;
pageCount = 1;
emailToDirName = emailToDirName; emailToDirName = emailToDirName;
constructor( constructor(
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
@ -44,15 +47,23 @@ export class BusinessListingsComponent {
private cdRef: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private imageService: ImageService, private imageService: ImageService,
private route: ActivatedRoute, private route: ActivatedRoute,
private searchService: SearchService,
) { ) {
this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandlerWrapper('business')); this.criteria = getCriteriaStateObject('business');
this.route.data.subscribe(async () => { // this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) { // if (this.router.getCurrentNavigation().extras.state) {
} 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;
} // }
// this.init();
// });
this.init(); this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'business') {
this.criteria = criteria as BusinessListingCriteria;
this.search();
}
}); });
} }
async ngOnInit() { async ngOnInit() {
@ -64,24 +75,25 @@ export class BusinessListingsComponent {
this.states = statesResult.map(ls => ({ name: this.selectOptions.getState(ls.state as string), value: ls.state, count: ls.count })); this.states = statesResult.map(ls => ({ name: this.selectOptions.getState(ls.state as string), value: ls.state, count: ls.count }));
this.search(); this.search();
} }
refine() { // refine() {
this.criteria.start = 0; // this.criteria.start = 0;
this.criteria.page = 0; // this.criteria.page = 0;
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.results;
this.totalRecords = listingReponse.total; this.totalRecords = listingReponse.totalCount;
this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
onPageChange(event: any) { onPageChange(page: any) {
this.criteria.start = event.first; this.criteria.start = (page - 1) * LISTINGS_PER_PAGE + 1;
this.criteria.length = event.rows; this.criteria.length = LISTINGS_PER_PAGE;
this.criteria.page = event.page; this.criteria.page = page;
this.criteria.pageCount = event.pageCount; // this.criteria.pageCount = event.pageCount;
this.search(); this.search();
} }
imageErrorHandler(listing: ListingType) { imageErrorHandler(listing: ListingType) {

View File

@ -2,14 +2,14 @@ import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core'; import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import onChange from 'on-change';
import { CommercialPropertyListing } from '../../../../../../bizmatch-server/src/models/db.model'; import { CommercialPropertyListing } from '../../../../../../bizmatch-server/src/models/db.model';
import { CommercialPropertyListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model'; import { CommercialPropertyListingCriteria, ResponseCommercialPropertyListingArray } 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 { SearchService } from '../../../services/search.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { getCriteriaStateObject, getSessionStorageHandlerWrapper } from '../../../utils/utils'; import { getCriteriaStateObject } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-commercial-property-listings', selector: 'app-commercial-property-listings',
@ -43,15 +43,23 @@ export class CommercialPropertyListingsComponent {
private cdRef: ChangeDetectorRef, private cdRef: ChangeDetectorRef,
private imageService: ImageService, private imageService: ImageService,
private route: ActivatedRoute, private route: ActivatedRoute,
private searchService: SearchService,
) { ) {
this.criteria = onChange(getCriteriaStateObject('commercialProperty'), getSessionStorageHandlerWrapper('commercialProperty')); this.criteria = getCriteriaStateObject('commercialProperty');
this.route.data.subscribe(async () => { // this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) { // if (this.router.getCurrentNavigation().extras.state) {
} 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;
} // }
// this.init();
// });
this.init(); this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'commercialProperty') {
this.criteria = criteria as CommercialPropertyListingCriteria;
this.search();
}
}); });
} }
async ngOnInit() {} async ngOnInit() {}
@ -67,8 +75,8 @@ export class CommercialPropertyListingsComponent {
} }
async search() { async search() {
const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty'); const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty');
this.listings = listingReponse.data; this.listings = (<ResponseCommercialPropertyListingArray>listingReponse).results;
this.totalRecords = listingReponse.total; this.totalRecords = (<ResponseCommercialPropertyListingArray>listingReponse).totalCount;
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }

View File

@ -1,18 +1,18 @@
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { Observable } from 'rxjs';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
export interface GeoResult { city: string; state: string; state_code: string } import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { GeoResult } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class GeoService { export class GeoService {
private apiBaseUrl = environment.apiBaseUrl; private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
findCitiesStartingWith(prefix: string, state?: string): Observable<GeoResult[]> { findCitiesStartingWith(prefix: string, state?: string): Observable<GeoResult[]> {
const stateString = state?`/${state}`:'' const stateString = state ? `/${state}` : '';
return this.http.get<GeoResult[]>(`${this.apiBaseUrl}/bizmatch/geo/${prefix}${stateString}`); return this.http.get<GeoResult[]>(`${this.apiBaseUrl}/bizmatch/geo/${prefix}${stateString}`);
} }
} }

View File

@ -0,0 +1,17 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
@Injectable({
providedIn: 'root',
})
export class SearchService {
private criteriaSource = new BehaviorSubject<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria>(null);
currentCriteria = this.criteriaSource.asObservable();
constructor() {}
search(criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): void {
this.criteriaSource.next(criteria);
}
}

View File

@ -39,6 +39,9 @@ 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;
} }
getStateInitials(name: string): string {
return this.states.find(l => l.name === name?.toUpperCase())?.value;
}
getBusiness(value: string): string { getBusiness(value: string): string {
return this.typesOfBusiness.find(t => t.value === value)?.name; return this.typesOfBusiness.find(t => t.value === value)?.name;
} }

View File

@ -124,8 +124,8 @@ export function createEmptyCommercialPropertyListingCriteria(): CommercialProper
prompt: '', prompt: '',
criteriaType: 'commercialProperty', criteriaType: 'commercialProperty',
county: '', county: '',
minPrice: 0, minPrice: null,
maxPrice: 0, maxPrice: null,
title: '', title: '',
}; };
} }
@ -139,7 +139,7 @@ export function createEmptyUserListingCriteria(): UserListingCriteria {
city: '', city: '',
types: [], types: [],
prompt: '', prompt: '',
criteriaType: 'user', criteriaType: 'broker',
firstname: '', firstname: '',
lastname: '', lastname: '',
companyName: '', companyName: '',