Paginator & SQL Querries where clauses & city search
This commit is contained in:
parent
f88eebe8d3
commit
abcde3991d
|
|
@ -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' },
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
) {}
|
) {}
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<p>paginator works!</p>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: '',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue