Überarbeitung

This commit is contained in:
Andreas Knuth 2024-08-09 17:59:49 +02:00
parent 6d1c50d5df
commit 1e1d5cea57
27 changed files with 135 additions and 112 deletions

View File

@ -60,3 +60,8 @@ pictures_base
src/*.js src/*.js
bun.lockb bun.lockb
#drizzle migrations
src/drizzle/migrations
importlog.txt

View File

@ -137,7 +137,7 @@ for (let index = 0; index < usersData.length; index++) {
user.companyWebsite = userData.companyWebsite; user.companyWebsite = userData.companyWebsite;
const [city, state] = userData.companyLocation.split('-').map(e => e.trim()); const [city, state] = userData.companyLocation.split('-').map(e => e.trim());
user.companyLocation = {}; user.companyLocation = {};
user.companyLocation.city = city; user.companyLocation.name = city;
user.companyLocation.state = state; user.companyLocation.state = state;
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);
user.companyLocation.latitude = cityGeo.latitude; user.companyLocation.latitude = cityGeo.latitude;
@ -188,7 +188,7 @@ for (let index = 0; index < commercialJsonData.length; index++) {
commercial.location = {}; commercial.location = {};
commercial.location.latitude = cityGeo.latitude; commercial.location.latitude = cityGeo.latitude;
commercial.location.longitude = cityGeo.longitude; commercial.location.longitude = cityGeo.longitude;
commercial.location.city = commercialJsonData[index].city; commercial.location.name = commercialJsonData[index].city;
commercial.location.state = commercialJsonData[index].state; commercial.location.state = commercialJsonData[index].state;
// console.log(JSON.stringify(commercial.location)); // console.log(JSON.stringify(commercial.location));
} catch (e) { } catch (e) {
@ -229,7 +229,7 @@ for (let index = 0; index < businessJsonData.length; index++) {
business.location = {}; business.location = {};
business.location.latitude = cityGeo.latitude; business.location.latitude = cityGeo.latitude;
business.location.longitude = cityGeo.longitude; business.location.longitude = cityGeo.longitude;
business.location.city = businessJsonData[index].city; business.location.name = businessJsonData[index].city;
business.location.state = businessJsonData[index].state; business.location.state = businessJsonData[index].state;
} catch (e) { } catch (e) {
console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`); console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`);

View File

@ -1,7 +1,7 @@
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 { CountyResult, GeoResult } from 'src/models/main.model.js'; import { CityAndStateResult, CountyResult, GeoResult } from 'src/models/main.model.js';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { City, CountyData, Geo, State } from '../models/server.model.js'; import { City, CountyData, Geo, State } from '../models/server.model.js';
@ -52,7 +52,7 @@ export class GeoService {
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) { if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
result.push({ result.push({
id: city.id, id: city.id,
city: city.name, name: city.name,
state: state.state_code, state: state.state_code,
//state_code: state.state_code, //state_code: state.state_code,
latitude: city.latitude, latitude: city.latitude,
@ -63,8 +63,8 @@ export class GeoService {
}); });
return state ? result.filter(e => e.state.toLowerCase() === state.toLowerCase()) : result; return state ? result.filter(e => e.state.toLowerCase() === state.toLowerCase()) : result;
} }
findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> { findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<CityAndStateResult> {
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = []; const results: Array<CityAndStateResult> = [];
const lowercasePrefix = prefix.toLowerCase(); const lowercasePrefix = prefix.toLowerCase();
@ -73,10 +73,9 @@ export class GeoService {
for (const state of this.geo.states) { for (const state of this.geo.states) {
if (state.name.toLowerCase().startsWith(lowercasePrefix)) { if (state.name.toLowerCase().startsWith(lowercasePrefix)) {
results.push({ results.push({
id: state.id.toString(), id: state.id,
name: state.name,
type: 'state', type: 'state',
state: state.state_code, content: state,
}); });
} }
@ -84,10 +83,9 @@ export class GeoService {
for (const city of state.cities) { for (const city of state.cities) {
if (city.name.toLowerCase().startsWith(lowercasePrefix)) { if (city.name.toLowerCase().startsWith(lowercasePrefix)) {
results.push({ results.push({
id: city.id.toString(), id: city.id,
name: city.name,
type: 'city', type: 'city',
state: state.state_code, content: { state: state.state_code, ...city },
}); });
} }
} }
@ -97,7 +95,7 @@ export class GeoService {
return results.sort((a, b) => { return results.sort((a, b) => {
if (a.type === 'state' && b.type === 'city') return -1; if (a.type === 'state' && b.type === 'city') return -1;
if (a.type === 'city' && b.type === 'state') return 1; if (a.type === 'city' && b.type === 'state') return 1;
return a.name.localeCompare(b.name); return a.content.name.localeCompare(b.content.name);
}); });
} }
getCityWithCoords(state: string, city: string): City { getCityWithCoords(state: string, city: string): City {

View File

@ -25,10 +25,10 @@ export class BusinessListingService {
const whereConditions: SQL[] = []; const whereConditions: SQL[] = [];
if (criteria.city && criteria.searchType === 'exact') { if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`)); whereConditions.push(ilike(businesses.city, `%${criteria.city.name}%`));
} }
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
} }
if (criteria.types && criteria.types.length > 0) { if (criteria.types && criteria.types.length > 0) {
@ -180,11 +180,13 @@ export class BusinessListingService {
return convertDrizzleBusinessToBusiness(createdListing); return convertDrizzleBusinessToBusiness(createdListing);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const filteredErrors = error.errors
field: err.path.join('.'), .map(item => ({
message: err.message, ...item,
})); field: item.path[0],
throw new BadRequestException(formattedErrors); }))
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }
@ -200,11 +202,13 @@ export class BusinessListingService {
return convertDrizzleBusinessToBusiness(updateListing); return convertDrizzleBusinessToBusiness(updateListing);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const filteredErrors = error.errors
field: err.path.join('.'), .map(item => ({
message: err.message, ...item,
})); field: item.path[0],
throw new BadRequestException(formattedErrors); }))
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }

View File

@ -24,10 +24,10 @@ export class CommercialPropertyService {
const whereConditions: SQL[] = []; const whereConditions: SQL[] = [];
if (criteria.city && criteria.searchType === 'exact') { if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`)); whereConditions.push(ilike(schema.commercials.city, `%${criteria.city.name}%`));
} }
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
} }
if (criteria.types && criteria.types.length > 0) { if (criteria.types && criteria.types.length > 0) {
@ -131,11 +131,13 @@ export class CommercialPropertyService {
return convertDrizzleCommercialToCommercial(createdListing); return convertDrizzleCommercialToCommercial(createdListing);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const filteredErrors = error.errors
field: err.path.join('.'), .map(item => ({
message: err.message, ...item,
})); field: item.path[0],
throw new BadRequestException(formattedErrors); }))
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }
@ -157,11 +159,13 @@ export class CommercialPropertyService {
return convertDrizzleCommercialToCommercial(updateListing); return convertDrizzleCommercialToCommercial(updateListing);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const filteredErrors = error.errors
field: err.path.join('.'), .map(item => ({
message: err.message, ...item,
})); field: item.path[0],
throw new BadRequestException(formattedErrors); }))
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }

View File

@ -111,7 +111,7 @@ export const LicensedInSchema = z.object({
state: z.string().nonempty('State is required'), state: z.string().nonempty('State is required'),
}); });
export const GeoSchema = z.object({ export const GeoSchema = z.object({
city: z.string(), name: z.string(),
state: z.string().refine(val => USStates.safeParse(val).success, { state: z.string().refine(val => USStates.safeParse(val).success, {
message: 'Invalid state. Must be a valid 2-letter US state code.', message: 'Invalid state. Must be a valid 2-letter US state code.',
}), }),

View File

@ -1,4 +1,5 @@
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js'; import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
import { State } from './server.model.js';
export interface StatesResult { export interface StatesResult {
state: string; state: string;
@ -59,7 +60,7 @@ export interface ListCriteria {
page: number; page: number;
types: string[]; types: string[];
state: string; state: string;
city: string; city: GeoResult;
prompt: string; prompt: string;
searchType: 'exact' | 'radius'; searchType: 'exact' | 'radius';
// radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'; // radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500';
@ -224,18 +225,23 @@ export interface UploadParams {
} }
export interface GeoResult { export interface GeoResult {
id: number; id: number;
city: string; name: string;
state: string; state: string;
// state_code: string;
latitude: number; latitude: number;
longitude: number; longitude: number;
} }
export interface CityAndStateResult { interface CityResult {
id: number; id: number;
name: string; type: 'city';
type: string; content: GeoResult;
state: string;
} }
interface StateResult {
id: number;
type: 'state';
content: State;
}
export type CityAndStateResult = CityResult | StateResult;
export interface CountyResult { export interface CountyResult {
id: number; id: number;
name: string; name: string;

View File

@ -26,10 +26,10 @@ export class UserService {
const whereConditions: SQL[] = []; const whereConditions: SQL[] = [];
whereConditions.push(eq(schema.users.customerType, 'professional')); whereConditions.push(eq(schema.users.customerType, 'professional'));
if (criteria.city && criteria.searchType === 'exact') { if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(ilike(schema.users.city, `%${criteria.city}%`)); whereConditions.push(ilike(schema.users.city, `%${criteria.city.name}%`));
} }
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
whereConditions.push(sql`${getDistanceQuery(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); whereConditions.push(sql`${getDistanceQuery(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
} }
if (criteria.types && criteria.types.length > 0) { if (criteria.types && criteria.types.length > 0) {
@ -139,11 +139,13 @@ export class UserService {
} }
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const filteredErrors = error.errors
field: err.path.join('.'), .map(item => ({
message: err.message, ...item,
})); field: item.path[0],
throw new BadRequestException(formattedErrors); }))
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
throw new BadRequestException(filteredErrors);
} }
throw error; throw error;
} }

View File

@ -33,11 +33,14 @@ type DrizzleUser = typeof users.$inferSelect;
type DrizzleBusinessListing = typeof businesses.$inferSelect; type DrizzleBusinessListing = typeof businesses.$inferSelect;
type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect; type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect;
export function convertBusinessToDrizzleBusiness(businessListing: Partial<BusinessListing>): DrizzleBusinessListing { export function convertBusinessToDrizzleBusiness(businessListing: Partial<BusinessListing>): DrizzleBusinessListing {
return flattenObject(businessListing); const drizzleBusinessListing = flattenObject(businessListing);
drizzleBusinessListing.city = drizzleBusinessListing.name;
delete drizzleBusinessListing.name;
return drizzleBusinessListing;
} }
export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): BusinessListing { export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): BusinessListing {
const o = { const o = {
location_city: drizzleBusinessListing.city, location_name: drizzleBusinessListing.city,
location_state: drizzleBusinessListing.state, location_state: drizzleBusinessListing.state,
location_latitude: drizzleBusinessListing.latitude, location_latitude: drizzleBusinessListing.latitude,
location_longitude: drizzleBusinessListing.longitude, location_longitude: drizzleBusinessListing.longitude,
@ -50,11 +53,14 @@ export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial
return unflattenObject(o); return unflattenObject(o);
} }
export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial<CommercialPropertyListing>): DrizzleCommercialPropertyListing { export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial<CommercialPropertyListing>): DrizzleCommercialPropertyListing {
return flattenObject(commercialPropertyListing); const drizzleCommercialPropertyListing = flattenObject(commercialPropertyListing);
drizzleCommercialPropertyListing.city = drizzleCommercialPropertyListing.name;
delete drizzleCommercialPropertyListing.name;
return drizzleCommercialPropertyListing;
} }
export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): CommercialPropertyListing { export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): CommercialPropertyListing {
const o = { const o = {
location_city: drizzleCommercialPropertyListing.city, location_name: drizzleCommercialPropertyListing.city,
location_state: drizzleCommercialPropertyListing.state, location_state: drizzleCommercialPropertyListing.state,
location_latitude: drizzleCommercialPropertyListing.latitude, location_latitude: drizzleCommercialPropertyListing.latitude,
location_longitude: drizzleCommercialPropertyListing.longitude, location_longitude: drizzleCommercialPropertyListing.longitude,
@ -67,12 +73,15 @@ export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyLi
return unflattenObject(o); return unflattenObject(o);
} }
export function convertUserToDrizzleUser(user: Partial<User>): DrizzleUser { export function convertUserToDrizzleUser(user: Partial<User>): DrizzleUser {
return flattenObject(user); const drizzleUser = flattenObject(user);
drizzleUser.city = drizzleUser.name;
delete drizzleUser.name;
return drizzleUser;
} }
export function convertDrizzleUserToUser(drizzleUser: Partial<DrizzleUser>): User { export function convertDrizzleUserToUser(drizzleUser: Partial<DrizzleUser>): User {
const o = { const o = {
companyLocation_city: drizzleUser.city, companyLocation_name: drizzleUser.city,
companyLocation_state: drizzleUser.state, companyLocation_state: drizzleUser.state,
companyLocation_latitude: drizzleUser.latitude, companyLocation_latitude: drizzleUser.latitude,
companyLocation_longitude: drizzleUser.longitude, companyLocation_longitude: drizzleUser.longitude,

View File

@ -2,7 +2,13 @@
<div class="relative w-full max-w-4xl max-h-full"> <div class="relative w-full max-w-4xl max-h-full">
<div class="relative bg-white rounded-lg shadow"> <div class="relative bg-white rounded-lg shadow">
<div class="flex items-start justify-between p-4 border-b rounded-t"> <div class="flex items-start justify-between p-4 border-b rounded-t">
@if(criteria.criteriaType==='businessListings'){
<h3 class="text-xl font-semibold text-gray-900">Business Listing Search</h3> <h3 class="text-xl font-semibold text-gray-900">Business Listing Search</h3>
} @else if (criteria.criteriaType==='commercialPropertyListings'){
<h3 class="text-xl font-semibold text-gray-900">Property Listing Search</h3>
} @else {
<h3 class="text-xl font-semibold text-gray-900">Professional Listing Search</h3>
}
<button (click)="modalService.reject()" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center"> <button (click)="modalService.reject()" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14"> <svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" /> <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
@ -20,10 +26,12 @@
<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" (ngModelChange)="setState($event)" 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>
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
</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>
<ng-select <ng-select
@ -42,7 +50,7 @@
<ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option> <ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option>
} }
</ng-select> </ng-select>
</div> </div> -->
<!-- New section for city search type --> <!-- New section for city search type -->
<div *ngIf="criteria.city"> <div *ngIf="criteria.city">
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label> <label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
@ -241,7 +249,7 @@
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" 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>
<ng-select <ng-select
class="custom" class="custom"
[multiple]="false" [multiple]="false"
@ -257,7 +265,8 @@
@for (city of cities$ | async; track city.id) { @for (city of cities$ | async; track city.id) {
<ng-option [value]="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> -->
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
</div> </div>
<!-- New section for city search type --> <!-- New section for city search type -->
<div *ngIf="criteria.city"> <div *ngIf="criteria.city">
@ -363,13 +372,10 @@
[typeahead]="countyInput$" [typeahead]="countyInput$"
[(ngModel)]="criteria.counties" [(ngModel)]="criteria.counties"
> >
<!-- @for (county of counties$ | async; track county.id) {
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
} -->
</ng-select> </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>
<ng-select <ng-select
class="custom" class="custom"
[multiple]="false" [multiple]="false"
@ -383,9 +389,10 @@
(ngModelChange)="setCity($event)" (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 }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option> <ng-option [value]="city">{{ city.name }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
} }
</ng-select> </ng-select> -->
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
</div> </div>
<!-- New section for city search type --> <!-- New section for city search type -->
<div *ngIf="criteria.city"> <div *ngIf="criteria.city">

View File

@ -10,12 +10,13 @@ import { ListingsService } from '../../services/listings.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { SharedModule } from '../../shared/shared/shared.module'; import { SharedModule } from '../../shared/shared/shared.module';
import { ValidatedCityComponent } from '../validated-city/validated-city.component';
import { ModalService } from './modal.service'; import { ModalService } from './modal.service';
@UntilDestroy() @UntilDestroy()
@Component({ @Component({
selector: 'app-search-modal', selector: 'app-search-modal',
standalone: true, standalone: true,
imports: [SharedModule, AsyncPipe, NgIf, NgSelectModule], imports: [SharedModule, AsyncPipe, NgIf, NgSelectModule, ValidatedCityComponent],
templateUrl: './search-modal.component.html', templateUrl: './search-modal.component.html',
styleUrl: './search-modal.component.scss', styleUrl: './search-modal.component.scss',
}) })
@ -98,7 +99,7 @@ export class SearchModalComponent {
} }
setCity(city) { setCity(city) {
if (city) { if (city) {
this.criteria.city = city.city; this.criteria.city = city;
this.criteria.state = city.state; this.criteria.state = city.state;
} else { } else {
this.criteria.city = null; this.criteria.city = null;

View File

@ -1,5 +1,5 @@
<div> <div>
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit" <label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit {{ labelClasses }}"
>{{ label }} @if(validationMessage){ >{{ label }} @if(validationMessage){
<div <div
attr.data-tooltip-target="tooltip-{{ name }}" attr.data-tooltip-target="tooltip-{{ name }}"
@ -19,11 +19,11 @@
[loading]="cityLoading" [loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters" typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$" [typeahead]="cityInput$"
ngModel="{{ value?.city }} {{ value ? '-' : '' }} {{ value?.state }}" ngModel="{{ value?.name }} {{ value ? '-' : '' }} {{ value?.state }}"
(ngModelChange)="onInputChange($event)" (ngModelChange)="onInputChange($event)"
> >
@for (city of cities$ | async; track city.id) { @for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option> <ng-option [value]="city">{{ city.name }} - {{ city.state }}</ng-option>
} }
</ng-select> </ng-select>
</div> </div>

View File

@ -1,7 +1,7 @@
:host ::ng-deep .ng-select.custom .ng-select-container { :host ::ng-deep .ng-select.custom .ng-select-container {
// --tw-bg-opacity: 1; // --tw-bg-opacity: 1;
// background-color: rgb(249 250 251 / var(--tw-bg-opacity)); // background-color: rgb(249 250 251 / var(--tw-bg-opacity));
height: 42px; // height: 42px;
border-radius: 0.5rem; border-radius: 0.5rem;
.ng-value-container .ng-input { .ng-value-container .ng-input {
top: 10px; top: 10px;

View File

@ -27,6 +27,8 @@ import { ValidationMessagesService } from '../validation-messages.service';
}) })
export class ValidatedCityComponent extends BaseInputComponent { export class ValidatedCityComponent extends BaseInputComponent {
@Input() items; @Input() items;
@Input() labelClasses: string;
@Input() state: string;
cities$: Observable<GeoResult[]>; cities$: Observable<GeoResult[]>;
cityInput$ = new Subject<string>(); cityInput$ = new Subject<string>();
countyInput$ = new Subject<string>(); countyInput$ = new Subject<string>();
@ -50,7 +52,7 @@ export class ValidatedCityComponent extends BaseInputComponent {
distinctUntilChanged(), distinctUntilChanged(),
tap(() => (this.cityLoading = true)), tap(() => (this.cityLoading = true)),
switchMap(term => switchMap(term =>
this.geoService.findCitiesStartingWith(term).pipe( this.geoService.findCitiesStartingWith(term, this.state).pipe(
catchError(() => of([])), // empty list on error catchError(() => of([])), // empty list on error
// map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names // map(cities => cities.map(city => city.city)), // transform the list of objects to a list of city names
tap(() => (this.cityLoading = false)), tap(() => (this.cityLoading = false)),

View File

@ -121,7 +121,7 @@ export class DetailsBusinessListingComponent {
} }
return [ return [
{ label: 'Category', value: this.selectOptions.getBusiness(this.listing.type) }, { label: 'Category', value: this.selectOptions.getBusiness(this.listing.type) },
{ label: 'Located in', value: `${this.listing.location.city}, ${this.selectOptions.getState(this.listing.location.state)}` }, { label: 'Located in', value: `${this.listing.location.name}, ${this.selectOptions.getState(this.listing.location.state)}` },
{ label: 'Asking Price', value: `$${this.listing.price?.toLocaleString()}` }, { label: 'Asking Price', value: `$${this.listing.price?.toLocaleString()}` },
{ label: 'Sales revenue', value: `$${this.listing.salesRevenue?.toLocaleString()}` }, { label: 'Sales revenue', value: `$${this.listing.salesRevenue?.toLocaleString()}` },
{ label: 'Cash flow', value: `$${this.listing.cashFlow?.toLocaleString()}` }, { label: 'Cash flow', value: `$${this.listing.cashFlow?.toLocaleString()}` },

View File

@ -97,7 +97,7 @@ export class DetailsCommercialPropertyListingComponent {
this.propertyDetails = [ this.propertyDetails = [
{ label: 'Property Category', value: this.selectOptions.getCommercialProperty(this.listing.type) }, { label: 'Property Category', value: this.selectOptions.getCommercialProperty(this.listing.type) },
{ label: 'Located in', value: this.selectOptions.getState(this.listing.location.state) }, { label: 'Located in', value: this.selectOptions.getState(this.listing.location.state) },
{ label: 'City', value: this.listing.location.city }, { label: 'City', value: this.listing.location.name },
{ label: 'Asking Price:', value: `$${this.listing.price?.toLocaleString()}` }, { label: 'Asking Price:', value: `$${this.listing.price?.toLocaleString()}` },
]; ];
//this.initFlowbite(); //this.initFlowbite();

View File

@ -199,7 +199,7 @@
</div> </div>
<div class="flex flex-col sm:flex-row sm:items-center"> <div class="flex flex-col sm:flex-row sm:items-center">
<span class="font-semibold w-40 p-2">Company Location</span> <span class="font-semibold w-40 p-2">Company Location</span>
<span class="p-2 flex-grow">{{ user.companyLocation.city }} - {{ user.companyLocation.state }}</span> <span class="p-2 flex-grow">{{ user.companyLocation.name }} - {{ user.companyLocation.state }}</span>
</div> </div>
</div> </div>

View File

@ -113,8 +113,8 @@
placeholder="Enter City or State ..." placeholder="Enter City or State ..."
groupBy="type" groupBy="type"
> >
@for (city of cities$ | async; track city.id) { @for (city of cities$ | async; track city.id) { @let state = city.type==='city'?city.content.state:''; @let separator = city.type==='city'?' - ':'';
<ng-option [value]="city">{{ city.name }} - {{ city.state }}</ng-option> <ng-option [value]="city">{{ city.content.name }}{{ separator }}{{ state }}</ng-option>
} }
</ng-select> </ng-select>
</div> </div>

View File

@ -64,6 +64,7 @@ export class HomeComponent {
} }
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') { async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
this.activeTabAction = tabname; this.activeTabAction = tabname;
this.cityOrState = null;
if ('business' === tabname) { if ('business' === tabname) {
this.criteria = createEnhancedProxy(getCriteriaStateObject('businessListings'), this); this.criteria = createEnhancedProxy(getCriteriaStateObject('businessListings'), this);
} else if ('commercialProperty' === tabname) { } else if ('commercialProperty' === tabname) {
@ -74,22 +75,7 @@ export class HomeComponent {
this.criteria = undefined; this.criteria = undefined;
} }
} }
// private createEnhancedProxy(obj: any) {
// const component = this;
// const sessionStorageHandler = function (path, value, previous, applyData) {
// let criteriaType = this.criteriaType;
// sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this));
// };
// return onChange(obj, function (path, value, previous, applyData) {
// // Call the original sessionStorageHandler
// sessionStorageHandler.call(this, path, value, previous, applyData);
// // Notify about the criteria change using the component's context
// component.criteriaChangeService.notifyCriteriaChange();
// });
// }
search() { search() {
this.router.navigate([`${this.activeTabAction}Listings`]); this.router.navigate([`${this.activeTabAction}Listings`]);
} }
@ -126,7 +112,6 @@ export class HomeComponent {
async openModal() { async openModal() {
const accepted = await this.modalService.showModal(this.criteria); const accepted = await this.modalService.showModal(this.criteria);
if (accepted) { if (accepted) {
//this.searchService.search(this.criteria);
this.router.navigate([`${this.activeTabAction}Listings`]); this.router.navigate([`${this.activeTabAction}Listings`]);
} }
} }
@ -153,10 +138,10 @@ export class HomeComponent {
setCityOrState(cityOrState: CityAndStateResult) { setCityOrState(cityOrState: CityAndStateResult) {
if (cityOrState) { if (cityOrState) {
if (cityOrState.type === 'state') { if (cityOrState.type === 'state') {
this.criteria.state = cityOrState.state; this.criteria.state = cityOrState.content.state_code;
} else { } else {
this.criteria.city = cityOrState.name; this.criteria.city = cityOrState.content as GeoResult;
this.criteria.state = cityOrState.state; this.criteria.state = cityOrState.content.state;
this.criteria.searchType = 'radius'; this.criteria.searchType = 'radius';
this.criteria.radius = 20; this.criteria.radius = 20;
} }

View File

@ -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: {{ listing.location.city }} - {{ listing.location.state }}</p> <p class="text-sm text-gray-600 mb-1">Location: {{ listing.location.name }} - {{ listing.location.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>

View File

@ -13,7 +13,7 @@
<span class="text-gray-600 text-sm"><i [class]="selectOptions.getIconTypeOfCommercials(listing.type)" class="mr-1"></i> {{ selectOptions.getCommercialProperty(listing.type) }}</span> <span class="text-gray-600 text-sm"><i [class]="selectOptions.getIconTypeOfCommercials(listing.type)" class="mr-1"></i> {{ selectOptions.getCommercialProperty(listing.type) }}</span>
</div> </div>
<h3 class="text-lg font-semibold mb-2">{{ listing.title }}</h3> <h3 class="text-lg font-semibold mb-2">{{ listing.title }}</h3>
<p class="text-gray-600 mb-2">{{ listing.location.city }}</p> <p class="text-gray-600 mb-2">{{ listing.location.name }}</p>
<p class="text-xl font-bold mb-4">{{ listing.price | currency }}</p> <p class="text-xl font-bold mb-4">{{ listing.price | currency }}</p>
<button [routerLink]="['/details-commercial-property-listing', listing.id]" class="bg-green-500 text-white px-4 py-2 rounded-full w-full hover:bg-green-600 transition duration-300"> <button [routerLink]="['/details-commercial-property-listing', listing.id]" class="bg-green-500 text-white px-4 py-2 rounded-full w-full hover:bg-green-600 transition duration-300">
View Full Listing <i class="fas fa-arrow-right ml-1"></i> View Full Listing <i class="fas fa-arrow-right ml-1"></i>

View File

@ -100,7 +100,7 @@
<input type="text" id="description" name="description" [(ngModel)]="user.description" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" /> <input type="text" id="description" name="description" [(ngModel)]="user.description" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
</div> --> </div> -->
<app-validated-input label="Company Name" name="companyName" [(ngModel)]="user.companyName"></app-validated-input> <app-validated-input label="Company Name" name="companyName" [(ngModel)]="user.companyName"></app-validated-input>
<app-validated-input label="Describe yourself" name="description" [(ngModel)]="user.description"></app-validated-input> <app-validated-input label="Describe Yourself" name="description" [(ngModel)]="user.description"></app-validated-input>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4">

View File

@ -169,7 +169,7 @@ export class AccountComponent {
async search(event: AutoCompleteCompleteEvent) { async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query)); const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => `${r.city} - ${r.state}`).slice(0, 5); this.suggestions = result.map(r => `${r.name} - ${r.state}`).slice(0, 5);
} }
addLicence() { addLicence() {
this.user.licensedIn.push({ registerNo: '', state: '' }); this.user.licensedIn.push({ registerNo: '', state: '' });

View File

@ -140,7 +140,7 @@ export class EditBusinessListingComponent {
async search(event: AutoCompleteCompleteEvent) { async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query)); const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => r.city).slice(0, 5); this.suggestions = result.map(r => r.name).slice(0, 5);
} }
changeListingCategory(value: 'business' | 'commercialProperty') { changeListingCategory(value: 'business' | 'commercialProperty') {

View File

@ -177,7 +177,7 @@ export class EditCommercialPropertyListingComponent {
async search(event: AutoCompleteCompleteEvent) { async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query)); const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => r.city).slice(0, 5); this.suggestions = result.map(r => r.name).slice(0, 5);
} }
openFileDialog() { openFileDialog() {
this.fileInput.nativeElement.click(); this.fileInput.nativeElement.click();

View File

@ -52,7 +52,7 @@
<div *ngFor="let listing of myListings" class="bg-white shadow-md rounded-lg p-4 mb-4"> <div *ngFor="let listing of myListings" class="bg-white shadow-md rounded-lg p-4 mb-4">
<h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2> <h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2>
<p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p> <p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p>
<p class="text-gray-600 mb-4">Located in: {{ listing.location.city }} - {{ listing.location.state }}</p> <p class="text-gray-600 mb-4">Located in: {{ listing.location.name }} - {{ listing.location.state }}</p>
<div class="flex justify-end"> <div class="flex justify-end">
<button class="bg-green-500 text-white p-2 rounded-full mr-2"> <button class="bg-green-500 text-white p-2 rounded-full mr-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">

View File

@ -93,8 +93,8 @@ export function createEmptyBusinessListingCriteria(): BusinessListingCriteria {
start: 0, start: 0,
length: 0, length: 0,
page: 0, page: 0,
state: '', state: null,
city: '', city: null,
types: [], types: [],
prompt: '', prompt: '',
criteriaType: 'businessListings', criteriaType: 'businessListings',
@ -123,8 +123,8 @@ export function createEmptyCommercialPropertyListingCriteria(): CommercialProper
start: 0, start: 0,
length: 0, length: 0,
page: 0, page: 0,
state: '', state: null,
city: '', city: null,
types: [], types: [],
prompt: '', prompt: '',
criteriaType: 'commercialPropertyListings', criteriaType: 'commercialPropertyListings',
@ -141,7 +141,7 @@ export function createEmptyUserListingCriteria(): UserListingCriteria {
start: 0, start: 0,
length: 0, length: 0,
page: 0, page: 0,
city: '', city: null,
types: [], types: [],
prompt: '', prompt: '',
criteriaType: 'brokerListings', criteriaType: 'brokerListings',