location radius search
This commit is contained in:
parent
acec14d372
commit
38e943c18e
|
|
@ -96,8 +96,8 @@ for (let index = 0; index < usersData.length; index++) {
|
||||||
user.companyLocation = userData.companyLocation;
|
user.companyLocation = userData.companyLocation;
|
||||||
const [city, state] = user.companyLocation.split('-').map(e => e.trim());
|
const [city, state] = user.companyLocation.split('-').map(e => e.trim());
|
||||||
const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||||
const latitude = cityGeo.latitude;
|
user.latitude = cityGeo.latitude;
|
||||||
const longitude = cityGeo.longitude;
|
user.longitude = cityGeo.longitude;
|
||||||
user.offeredServices = userData.offeredServices;
|
user.offeredServices = userData.offeredServices;
|
||||||
user.gender = userData.gender;
|
user.gender = userData.gender;
|
||||||
user.customerType = 'professional';
|
user.customerType = 'professional';
|
||||||
|
|
@ -151,8 +151,8 @@ for (let index = 0; index < commercialJsonData.length; index++) {
|
||||||
commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercial.type)).value;
|
commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercial.type)).value;
|
||||||
const cityGeo = geos.states.find(s => s.state_code === commercial.state).cities.find(c => c.name === commercial.city);
|
const cityGeo = geos.states.find(s => s.state_code === commercial.state).cities.find(c => c.name === commercial.city);
|
||||||
try {
|
try {
|
||||||
const latitude = cityGeo.latitude;
|
commercial.latitude = cityGeo.latitude;
|
||||||
const longitude = cityGeo.longitude;
|
commercial.longitude = cityGeo.longitude;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`----------------> ERROR ${commercial.state} - ${commercial.city}`);
|
console.log(`----------------> ERROR ${commercial.state} - ${commercial.city}`);
|
||||||
}
|
}
|
||||||
|
|
@ -191,8 +191,8 @@ for (let index = 0; index < businessJsonData.length; index++) {
|
||||||
business.imageName = emailToDirName(user.email);
|
business.imageName = emailToDirName(user.email);
|
||||||
const cityGeo = geos.states.find(s => s.state_code === business.state).cities.find(c => c.name === business.city);
|
const cityGeo = geos.states.find(s => s.state_code === business.state).cities.find(c => c.name === business.city);
|
||||||
try {
|
try {
|
||||||
const latitude = cityGeo.latitude;
|
business.latitude = cityGeo.latitude;
|
||||||
const longitude = cityGeo.longitude;
|
business.longitude = cityGeo.longitude;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`----------------> ERROR ${business.state} - ${business.city}`);
|
console.log(`----------------> ERROR ${business.state} - ${business.city}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,9 @@ 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;
|
||||||
}
|
}
|
||||||
|
getCityWithCoords(state: string, city: string): City {
|
||||||
|
return this.geo.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,10 @@ import { Logger } from 'winston';
|
||||||
import * as schema from '../drizzle/schema.js';
|
import * as schema from '../drizzle/schema.js';
|
||||||
import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
|
import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
|
import { GeoService } from '../geo/geo.service.js';
|
||||||
import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
|
import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
|
||||||
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||||
|
import { getDistanceQuery } from '../utils.js';
|
||||||
const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
|
|
||||||
|
|
||||||
const getDistanceQuery = (lat: number, lon: number) => sql`
|
|
||||||
${EARTH_RADIUS_KM} * 2 * ASIN(SQRT(
|
|
||||||
POWER(SIN((${lat} - ${businesses.latitude}) * PI() / 180 / 2), 2) +
|
|
||||||
COS(${lat} * PI() / 180) * COS(${businesses.latitude} * PI() / 180) *
|
|
||||||
POWER(SIN((${lon} - ${businesses.longitude}) * PI() / 180 / 2), 2)
|
|
||||||
))
|
|
||||||
`;
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BusinessListingService {
|
export class BusinessListingService {
|
||||||
|
|
@ -25,15 +17,19 @@ export class BusinessListingService {
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
|
private geoService: GeoService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
|
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
|
||||||
const whereConditions: SQL[] = [];
|
const whereConditions: SQL[] = [];
|
||||||
|
|
||||||
if (criteria.city) {
|
if (criteria.city && criteria.searchType === 'exact') {
|
||||||
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
|
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
|
||||||
}
|
}
|
||||||
|
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||||
|
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||||
|
whereConditions.push(sql`${getDistanceQuery(businesses, parseFloat(cityGeo.latitude), parseFloat(cityGeo.longitude))} <= ${criteria.radius}`);
|
||||||
|
}
|
||||||
if (criteria.types && criteria.types.length > 0) {
|
if (criteria.types && criteria.types.length > 0) {
|
||||||
whereConditions.push(inArray(businesses.type, criteria.types));
|
whereConditions.push(inArray(businesses.type, criteria.types));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ import { Logger } from 'winston';
|
||||||
import * as schema from '../drizzle/schema.js';
|
import * as schema from '../drizzle/schema.js';
|
||||||
import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
|
import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
|
import { GeoService } from '../geo/geo.service.js';
|
||||||
import { CommercialPropertyListing } from '../models/db.model';
|
import { CommercialPropertyListing } from '../models/db.model';
|
||||||
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||||
|
import { getDistanceQuery } from '../utils.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommercialPropertyService {
|
export class CommercialPropertyService {
|
||||||
|
|
@ -15,14 +17,18 @@ export class CommercialPropertyService {
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
|
private geoService: GeoService,
|
||||||
) {}
|
) {}
|
||||||
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
|
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
|
||||||
const whereConditions: SQL[] = [];
|
const whereConditions: SQL[] = [];
|
||||||
|
|
||||||
if (criteria.city) {
|
if (criteria.city && criteria.searchType === 'exact') {
|
||||||
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
|
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
|
||||||
}
|
}
|
||||||
|
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||||
|
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||||
|
whereConditions.push(sql`${getDistanceQuery(commercials, parseFloat(cityGeo.latitude), parseFloat(cityGeo.longitude))} <= ${criteria.radius}`);
|
||||||
|
}
|
||||||
if (criteria.types && criteria.types.length > 0) {
|
if (criteria.types && criteria.types.length > 0) {
|
||||||
whereConditions.push(inArray(schema.commercials.type, criteria.types));
|
whereConditions.push(inArray(schema.commercials.type, criteria.types));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,16 @@ 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 { GeoModule } from '../geo/geo.module.js';
|
||||||
|
import { GeoService } from '../geo/geo.service.js';
|
||||||
import { BusinessListingService } from './business-listing.service.js';
|
import { BusinessListingService } from './business-listing.service.js';
|
||||||
import { CommercialPropertyService } from './commercial-property.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, GeoModule],
|
||||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||||
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService],
|
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
||||||
exports: [BusinessListingService, CommercialPropertyService],
|
exports: [BusinessListingService, CommercialPropertyService],
|
||||||
})
|
})
|
||||||
export class ListingsModule {}
|
export class ListingsModule {}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import path, { join } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
|
import { GeoModule } from '../geo/geo.module.js';
|
||||||
|
import { GeoService } from '../geo/geo.service.js';
|
||||||
import { UserModule } from '../user/user.module.js';
|
import { UserModule } from '../user/user.module.js';
|
||||||
import { UserService } from '../user/user.service.js';
|
import { UserService } from '../user/user.service.js';
|
||||||
import { MailController } from './mail.controller.js';
|
import { MailController } from './mail.controller.js';
|
||||||
|
|
@ -17,6 +19,7 @@ const password = process.env.amazon_password;
|
||||||
imports: [
|
imports: [
|
||||||
DrizzleModule,
|
DrizzleModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
GeoModule,
|
||||||
MailerModule.forRoot({
|
MailerModule.forRoot({
|
||||||
transport: {
|
transport: {
|
||||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||||
|
|
@ -39,7 +42,7 @@ const password = process.env.amazon_password;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [MailService, UserService, FileService],
|
providers: [MailService, UserService, FileService, GeoService],
|
||||||
controllers: [MailController],
|
controllers: [MailController],
|
||||||
})
|
})
|
||||||
export class MailModule {}
|
export class MailModule {}
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,14 @@ export interface ListCriteria {
|
||||||
length: number;
|
length: number;
|
||||||
page: number;
|
page: number;
|
||||||
types: string[];
|
types: string[];
|
||||||
|
state: string;
|
||||||
city: string;
|
city: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
|
searchType: 'exact' | 'radius';
|
||||||
|
radius: number;
|
||||||
criteriaType: 'business' | 'commercialProperty' | 'broker';
|
criteriaType: 'business' | 'commercialProperty' | 'broker';
|
||||||
}
|
}
|
||||||
export interface BusinessListingCriteria extends ListCriteria {
|
export interface BusinessListingCriteria extends ListCriteria {
|
||||||
state: string;
|
|
||||||
county: string;
|
county: string;
|
||||||
minPrice: number;
|
minPrice: number;
|
||||||
maxPrice: number;
|
maxPrice: number;
|
||||||
|
|
@ -83,7 +85,6 @@ export interface BusinessListingCriteria extends ListCriteria {
|
||||||
criteriaType: 'business';
|
criteriaType: 'business';
|
||||||
}
|
}
|
||||||
export interface CommercialPropertyListingCriteria extends ListCriteria {
|
export interface CommercialPropertyListingCriteria extends ListCriteria {
|
||||||
state: string;
|
|
||||||
county: string;
|
county: string;
|
||||||
minPrice: number;
|
minPrice: number;
|
||||||
maxPrice: number;
|
maxPrice: number;
|
||||||
|
|
@ -95,7 +96,6 @@ export interface UserListingCriteria extends ListCriteria {
|
||||||
lastname: string;
|
lastname: string;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
counties: string[];
|
counties: string[];
|
||||||
states: string[];
|
|
||||||
criteriaType: 'broker';
|
criteriaType: 'broker';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
|
import { GeoModule } from '../geo/geo.module.js';
|
||||||
|
import { GeoService } from '../geo/geo.service.js';
|
||||||
import { UserController } from './user.controller.js';
|
import { UserController } from './user.controller.js';
|
||||||
import { UserService } from './user.service.js';
|
import { UserService } from './user.service.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DrizzleModule],
|
imports: [DrizzleModule, GeoModule],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService, FileService],
|
providers: [UserService, FileService, GeoService],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ import { Logger } from 'winston';
|
||||||
import * as schema from '../drizzle/schema.js';
|
import * as schema from '../drizzle/schema.js';
|
||||||
import { customerSubTypeEnum, 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 { GeoService } from '../geo/geo.service.js';
|
||||||
import { User } from '../models/db.model.js';
|
import { User } from '../models/db.model.js';
|
||||||
import { emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
|
import { emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
|
||||||
|
import { getDistanceQuery } from '../utils.js';
|
||||||
|
|
||||||
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
|
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -16,6 +18,7 @@ export class UserService {
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
|
private geoService: GeoService,
|
||||||
) {}
|
) {}
|
||||||
// private getConditions(criteria: UserListingCriteria): any[] {
|
// private getConditions(criteria: UserListingCriteria): any[] {
|
||||||
// const conditions = [];
|
// const conditions = [];
|
||||||
|
|
@ -32,10 +35,13 @@ export class UserService {
|
||||||
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
|
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
|
||||||
const whereConditions: SQL[] = [];
|
const whereConditions: SQL[] = [];
|
||||||
|
|
||||||
if (criteria.city) {
|
if (criteria.city && criteria.searchType === 'exact') {
|
||||||
whereConditions.push(ilike(schema.users.companyLocation, `%${criteria.city}%`));
|
whereConditions.push(ilike(schema.users.companyLocation, `%${criteria.city}%`));
|
||||||
}
|
}
|
||||||
|
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||||
|
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||||
|
whereConditions.push(sql`${getDistanceQuery(schema.users, parseFloat(cityGeo.latitude), parseFloat(cityGeo.longitude))} <= ${criteria.radius}`);
|
||||||
|
}
|
||||||
if (criteria.types && criteria.types.length > 0) {
|
if (criteria.types && criteria.types.length > 0) {
|
||||||
// whereConditions.push(inArray(schema.users.customerSubType, criteria.types));
|
// whereConditions.push(inArray(schema.users.customerSubType, criteria.types));
|
||||||
whereConditions.push(inArray(schema.users.customerSubType, criteria.types as CustomerSubType[]));
|
whereConditions.push(inArray(schema.users.customerSubType, criteria.types as CustomerSubType[]));
|
||||||
|
|
@ -57,10 +63,12 @@ export class UserService {
|
||||||
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}%`})`)));
|
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) {
|
// 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})`)));
|
// whereConditions.push(or(...criteria.states.map(state => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${state})`)));
|
||||||
|
// }
|
||||||
|
if (criteria.state) {
|
||||||
|
whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${criteria.state})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return whereConditions;
|
return whereConditions;
|
||||||
}
|
}
|
||||||
async searchUserListings(criteria: UserListingCriteria) {
|
async searchUserListings(criteria: UserListingCriteria) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
import { businesses, commercials, users } from './drizzle/schema.js';
|
||||||
|
export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
|
||||||
|
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
|
||||||
|
|
||||||
export function convertStringToNullUndefined(value) {
|
export function convertStringToNullUndefined(value) {
|
||||||
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
|
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
|
||||||
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase();
|
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase();
|
||||||
|
|
@ -10,4 +15,16 @@ export function convertStringToNullUndefined(value) {
|
||||||
|
|
||||||
// Gibt den Originalwert zurück, wenn es sich nicht um 'null' oder 'undefined' handelt
|
// Gibt den Originalwert zurück, wenn es sich nicht um 'null' oder 'undefined' handelt
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getDistanceQuery = (schema: typeof businesses | typeof commercials | typeof users, lat: number, lon: number, unit: 'km' | 'miles' = 'miles') => {
|
||||||
|
const radius = unit === 'km' ? EARTH_RADIUS_KM : EARTH_RADIUS_MILES;
|
||||||
|
|
||||||
|
return sql`
|
||||||
|
${radius} * 2 * ASIN(SQRT(
|
||||||
|
POWER(SIN((${lat} - ${schema.latitude}) * PI() / 180 / 2), 2) +
|
||||||
|
COS(${lat} * PI() / 180) * COS(${schema.latitude} * PI() / 180) *
|
||||||
|
POWER(SIN((${lon} - ${schema.longitude}) * PI() / 180 / 2), 2)
|
||||||
|
))
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -172,11 +172,11 @@ export class HeaderComponent {
|
||||||
}
|
}
|
||||||
getNumberOfFiltersSet() {
|
getNumberOfFiltersSet() {
|
||||||
if (this.criteria?.criteriaType === 'broker') {
|
if (this.criteria?.criteriaType === 'broker') {
|
||||||
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page']);
|
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||||
} else if (this.criteria?.criteriaType === 'business') {
|
} else if (this.criteria?.criteriaType === 'business') {
|
||||||
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page']);
|
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||||
} else if (this.criteria?.criteriaType === 'commercialProperty') {
|
} else if (this.criteria?.criteriaType === 'commercialProperty') {
|
||||||
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page']);
|
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<div>
|
<div>
|
||||||
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
|
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
|
||||||
|
|
||||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [(ngModel)]="criteria.state" name="state"> </ng-select>
|
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"> </ng-select>
|
||||||
</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>
|
||||||
|
|
@ -35,13 +35,44 @@
|
||||||
[loading]="cityLoading"
|
[loading]="cityLoading"
|
||||||
typeToSearchText="Please enter 2 or more characters"
|
typeToSearchText="Please enter 2 or more characters"
|
||||||
[typeahead]="cityInput$"
|
[typeahead]="cityInput$"
|
||||||
[(ngModel)]="criteria.city"
|
[ngModel]="criteria.city"
|
||||||
|
(ngModelChange)="setCity($event)"
|
||||||
>
|
>
|
||||||
@for (city of cities$ | async; track city.id) {
|
@for (city of cities$ | async; track city.id) {
|
||||||
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||||
}
|
}
|
||||||
</ng-select>
|
</ng-select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- New section for city search type -->
|
||||||
|
<div *ngIf="criteria.city">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" value="exact" />
|
||||||
|
<span class="ml-2">Exact City</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" value="radius" />
|
||||||
|
<span class="ml-2">Radius Search</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- New section for radius selection -->
|
||||||
|
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-center border border-gray-200 hover:bg-gray-500 hover:text-white"
|
||||||
|
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-gray-900 bg-white'"
|
||||||
|
(click)="criteria.radius = radius"
|
||||||
|
>
|
||||||
|
{{ radius }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
|
@ -113,16 +144,6 @@
|
||||||
placeholder="e.g. Restaurant"
|
placeholder="e.g. Restaurant"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Broker Name / Company Name</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="brokername"
|
|
||||||
[(ngModel)]="criteria.brokerName"
|
|
||||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="e.g. Brokers Invest"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -200,6 +221,16 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Broker Name / Company Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="brokername"
|
||||||
|
[(ngModel)]="criteria.brokerName"
|
||||||
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
|
placeholder="e.g. Brokers Invest"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @if(criteria.criteriaType==='commercialProperty'){
|
} @if(criteria.criteriaType==='commercialProperty'){
|
||||||
|
|
@ -207,7 +238,7 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
|
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
|
||||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [(ngModel)]="criteria.state" name="state"> </ng-select>
|
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"> </ng-select>
|
||||||
</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>
|
||||||
|
|
@ -220,13 +251,44 @@
|
||||||
[loading]="cityLoading"
|
[loading]="cityLoading"
|
||||||
typeToSearchText="Please enter 2 or more characters"
|
typeToSearchText="Please enter 2 or more characters"
|
||||||
[typeahead]="cityInput$"
|
[typeahead]="cityInput$"
|
||||||
[(ngModel)]="criteria.city"
|
[ngModel]="criteria.city"
|
||||||
|
(ngModelChange)="setCity($event)"
|
||||||
>
|
>
|
||||||
@for (city of cities$ | async; track city.id) {
|
@for (city of cities$ | async; track city.id) {
|
||||||
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||||
}
|
}
|
||||||
</ng-select>
|
</ng-select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- New section for city search type -->
|
||||||
|
<div *ngIf="criteria.city">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" value="exact" />
|
||||||
|
<span class="ml-2">Exact City</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" value="radius" />
|
||||||
|
<span class="ml-2">Radius Search</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- New section for radius selection -->
|
||||||
|
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-center border border-gray-200 hover:bg-gray-500 hover:text-white"
|
||||||
|
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-gray-900 bg-white'"
|
||||||
|
(click)="criteria.radius = radius"
|
||||||
|
>
|
||||||
|
{{ radius }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</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>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
|
|
@ -284,7 +346,7 @@
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="states" class="block mb-2 text-sm font-medium text-gray-900">Locations served - States</label>
|
<label for="states" class="block mb-2 text-sm font-medium text-gray-900">Locations served - States</label>
|
||||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [(ngModel)]="criteria.states" name="states" [multiple]="true"> </ng-select>
|
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state" [multiple]="false"> </ng-select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="counties" class="block mb-2 text-sm font-medium text-gray-900">Locations served - Counties</label>
|
<label for="counties" class="block mb-2 text-sm font-medium text-gray-900">Locations served - Counties</label>
|
||||||
|
|
@ -292,7 +354,7 @@
|
||||||
[items]="counties$ | async"
|
[items]="counties$ | async"
|
||||||
bindLabel="name"
|
bindLabel="name"
|
||||||
class="custom"
|
class="custom"
|
||||||
[multiple]="true"
|
[multiple]="false"
|
||||||
[hideSelected]="true"
|
[hideSelected]="true"
|
||||||
[trackByFn]="trackByFn"
|
[trackByFn]="trackByFn"
|
||||||
[minTermLength]="2"
|
[minTermLength]="2"
|
||||||
|
|
@ -317,13 +379,44 @@
|
||||||
[loading]="cityLoading"
|
[loading]="cityLoading"
|
||||||
typeToSearchText="Please enter 2 or more characters"
|
typeToSearchText="Please enter 2 or more characters"
|
||||||
[typeahead]="cityInput$"
|
[typeahead]="cityInput$"
|
||||||
[(ngModel)]="criteria.city"
|
[ngModel]="criteria.city"
|
||||||
|
(ngModelChange)="setCity($event)"
|
||||||
>
|
>
|
||||||
@for (city of cities$ | async; track city.id) {
|
@for (city of cities$ | async; track city.id) {
|
||||||
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||||
}
|
}
|
||||||
</ng-select>
|
</ng-select>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- New section for city search type -->
|
||||||
|
<div *ngIf="criteria.city">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" value="exact" />
|
||||||
|
<span class="ml-2">Exact City</span>
|
||||||
|
</label>
|
||||||
|
<label class="inline-flex items-center">
|
||||||
|
<input type="radio" class="form-radio" name="searchType" [(ngModel)]="criteria.searchType" value="radius" />
|
||||||
|
<span class="ml-2">Radius Search</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- New section for radius selection -->
|
||||||
|
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||||
|
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
@for (radius of [5, 20, 50, 100, 200, 300, 400, 500]; track radius) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-3 py-2 text-xs font-medium text-center border border-gray-200 hover:bg-gray-500 hover:text-white"
|
||||||
|
[ngClass]="criteria.radius === radius ? 'text-white bg-gray-500' : 'text-gray-900 bg-white'"
|
||||||
|
(click)="criteria.radius = radius"
|
||||||
|
>
|
||||||
|
{{ radius }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,24 @@ export class SearchModalComponent {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
setCity(city) {
|
||||||
|
if (city) {
|
||||||
|
this.criteria.city = city.city;
|
||||||
|
this.criteria.state = city.state_code;
|
||||||
|
} else {
|
||||||
|
this.criteria.city = null;
|
||||||
|
this.criteria.radius = null;
|
||||||
|
this.criteria.searchType = 'exact';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setState(state: string) {
|
||||||
|
if (state) {
|
||||||
|
this.criteria.state = state;
|
||||||
|
} else {
|
||||||
|
this.criteria.state = null;
|
||||||
|
this.setCity(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
private setupCriteriaChangeListener() {
|
private setupCriteriaChangeListener() {
|
||||||
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => this.setTotalNumberOfResults());
|
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => this.setTotalNumberOfResults());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@
|
||||||
<p class="text-sm text-gray-600 mb-1">Asking price: {{ listing.price | currency }}</p>
|
<p class="text-sm text-gray-600 mb-1">Asking price: {{ listing.price | currency }}</p>
|
||||||
<p class="text-sm text-gray-600 mb-1">Sales revenue: {{ listing.salesRevenue | currency }}</p>
|
<p class="text-sm text-gray-600 mb-1">Sales revenue: {{ listing.salesRevenue | currency }}</p>
|
||||||
<p class="text-sm text-gray-600 mb-1">Net profit: {{ listing.cashFlow | currency }}</p>
|
<p class="text-sm text-gray-600 mb-1">Net profit: {{ listing.cashFlow | currency }}</p>
|
||||||
<p class="text-sm text-gray-600 mb-1">Location: {{ selectOptions.getState(listing.state) }}</p>
|
<p class="text-sm text-gray-600 mb-1">Location: {{ listing.city }} - {{ selectOptions.getState(listing.state) }}</p>
|
||||||
<p class="text-sm text-gray-600 mb-1">Established: {{ listing.established }}</p>
|
<p class="text-sm text-gray-600 mb-1">Established: {{ listing.established }}</p>
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" alt="Company logo" class="absolute bottom-[70px] right-[30px] h-[35px] w-auto" />
|
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" alt="Company logo" class="absolute bottom-[70px] right-[30px] h-[35px] w-auto" />
|
||||||
<div class="flex-grow"></div>
|
<div class="flex-grow"></div>
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,8 @@ export function createEmptyBusinessListingCriteria(): BusinessListingCriteria {
|
||||||
franchiseResale: false,
|
franchiseResale: false,
|
||||||
title: '',
|
title: '',
|
||||||
brokerName: '',
|
brokerName: '',
|
||||||
|
searchType: 'exact',
|
||||||
|
radius: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,6 +134,8 @@ export function createEmptyCommercialPropertyListingCriteria(): CommercialProper
|
||||||
minPrice: null,
|
minPrice: null,
|
||||||
maxPrice: null,
|
maxPrice: null,
|
||||||
title: '',
|
title: '',
|
||||||
|
searchType: 'exact',
|
||||||
|
radius: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,7 +152,9 @@ export function createEmptyUserListingCriteria(): UserListingCriteria {
|
||||||
lastname: '',
|
lastname: '',
|
||||||
companyName: '',
|
companyName: '',
|
||||||
counties: [],
|
counties: [],
|
||||||
states: [],
|
state: '',
|
||||||
|
searchType: 'exact',
|
||||||
|
radius: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function createLogger(name: string, level: number = INFO, options: any = {}) {
|
export function createLogger(name: string, level: number = INFO, options: any = {}) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue