7 Commits

64 changed files with 361074 additions and 122384 deletions

View File

@@ -698,7 +698,7 @@
"realEstateIncluded": true,
"franchiseResale": false,
"draft": false,
"internals": "",
"internals": null,
"created": "2023-11-18T13:00:00.000Z"
},
{

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,12 +7,53 @@ import { join } from 'path';
import pkg from 'pg';
import { rimraf } from 'rimraf';
import sharp from 'sharp';
import { BusinessListingService } from 'src/listings/business-listing.service.js';
import { CommercialPropertyService } from 'src/listings/commercial-property.service.js';
import { Geo } from 'src/models/server.model.js';
import winston from 'winston';
import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js';
import { createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
import { User, UserData } from '../models/db.model.js';
import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
import { SelectOptionsService } from '../select-options/select-options.service.js';
import { toDrizzleUser } from '../utils.js';
import { convertUserToDrizzleUser } from '../utils.js';
import * as schema from './schema.js';
interface PropertyImportListing {
id: string;
userId: string;
listingsCategory: 'commercialProperty';
title: string;
state: string;
hasImages: boolean;
price: number;
city: string;
description: string;
type: number;
imageOrder: any[];
}
interface BusinessImportListing {
userId: string;
listingsCategory: 'business';
title: string;
description: string;
type: number;
state: string;
city: string;
id: string;
price: number;
salesRevenue: number;
leasedLocation: boolean;
established: number;
employees: number;
reasonForSale: string;
supportAndTraining: string;
cashFlow: number;
brokerLicencing: string;
internalListingNumber: number;
realEstateIncluded: boolean;
franchiseResale: boolean;
draft: boolean;
internals: string;
created: string;
}
const typesOfBusiness: Array<KeyValueStyle> = [
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
@@ -41,13 +82,15 @@ const db = drizzle(client, { schema, logger: true });
const logger = winston.createLogger({
transports: [new winston.transports.Console()],
});
const commService = new CommercialPropertyService(null, db);
const businessService = new BusinessListingService(null, db);
//Delete Content
await db.delete(schema.commercials);
await db.delete(schema.businesses);
await db.delete(schema.users);
let filePath = `./src/assets/geo.json`;
const rawData = readFileSync(filePath, 'utf8');
const geos = JSON.parse(rawData);
const geos = JSON.parse(rawData) as Geo;
const sso = new SelectOptionsService();
//Broker
@@ -68,13 +111,11 @@ deleteFilesOfDir(targetPathProperty);
fs.ensureDirSync(`./pictures/logo`);
fs.ensureDirSync(`./pictures/profile`);
fs.ensureDirSync(`./pictures/property`);
// type UserProfile = Omit<User, 'created' | 'updated' | 'hasCompanyLogo' | 'hasProfile' | 'id'>;
// type NewUser = typeof users.$inferInsert;
//for (const userData of usersData) {
//User
for (let index = 0; index < usersData.length; index++) {
const userData = usersData[index];
const user: User = createDefaultUser('', '', ''); //{ id: undefined, firstname: '', lastname: '', email: '' };
const user: User = createDefaultUser('', '', '');
user.licensedIn = [];
userData.licensedIn.forEach(l => {
console.log(l['value'], l['name']);
@@ -94,28 +135,23 @@ for (let index = 0; index < usersData.length; index++) {
user.companyName = userData.companyName;
user.companyOverview = userData.companyOverview;
user.companyWebsite = userData.companyWebsite;
user.companyLocation = userData.companyLocation;
const [city, state] = user.companyLocation.split('-').map(e => e.trim());
const [city, state] = userData.companyLocation.split('-').map(e => e.trim());
user.companyLocation = {};
user.companyLocation.city = city;
user.companyLocation.state = state;
const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
user.latitude = cityGeo.latitude;
user.longitude = cityGeo.longitude;
user.companyLocation.latitude = cityGeo.latitude;
user.companyLocation.longitude = cityGeo.longitude;
user.offeredServices = userData.offeredServices;
user.gender = userData.gender;
user.customerType = 'professional';
user.customerSubType = 'broker';
user.created = new Date();
user.updated = new Date();
// const createUserProfile = (user: User): UserProfile => {
// const { id, created, updated, hasCompanyLogo, hasProfile, ...userProfile } = user;
// return userProfile;
// };
// const userProfile = createUserProfile(user);
// logger.info(`${index} - ${JSON.stringify(userProfile)}`);
// const embedding = await createEmbedding(JSON.stringify(userProfile));
//sleep(200);
const u = await db
.insert(schema.users)
.values(toDrizzleUser(user))
.values(convertUserToDrizzleUser(user))
.returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname });
generatedUserData.push(u[0]);
i++;
@@ -136,89 +172,91 @@ for (let index = 0; index < usersData.length; index++) {
//Corporate Listings
filePath = `./data/commercials.json`;
data = readFileSync(filePath, 'utf8');
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten
for (let index = 0; index < commercialJsonData.length; index++) {
const commercial = commercialJsonData[index];
const id = commercial.id;
delete commercial.id;
const user = getRandomItem(generatedUserData);
const commercial = createDefaultCommercialPropertyListing();
const id = commercialJsonData[index].id;
delete commercial.id;
commercial.email = user.email;
commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercialJsonData[index].type)).value;
commercial.title = commercialJsonData[index].title;
commercial.description = commercialJsonData[index].description;
try {
const cityGeo = geos.states.find(s => s.state_code === commercialJsonData[index].state).cities.find(c => c.name === commercialJsonData[index].city);
commercial.location = {};
commercial.location.latitude = cityGeo.latitude;
commercial.location.longitude = cityGeo.longitude;
commercial.location.city = commercialJsonData[index].city;
commercial.location.state = commercialJsonData[index].state;
// console.log(JSON.stringify(commercial.location));
} catch (e) {
console.log(`----------------> ERROR ${commercialJsonData[index].state} - ${commercialJsonData[index].city}`);
continue;
}
commercial.price = commercialJsonData[index].price;
commercial.listingsCategory = 'commercialProperty';
commercial.draft = false;
commercial.imageOrder = getFilenames(id);
commercial.imagePath = emailToDirName(user.email);
const insertionDate = getRandomDateWithinLastYear();
commercial.created = insertionDate;
commercial.updated = insertionDate;
commercial.email = user.email;
commercial.draft = false;
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 result = await commService.createListing(commercial); //await db.insert(schema.commercials).values(commercial).returning();
try {
commercial.latitude = cityGeo.latitude;
commercial.longitude = cityGeo.longitude;
} catch (e) {
console.log(`----------------> ERROR ${commercial.state} - ${commercial.city}`);
}
// const reducedCommercial = {
// city: commercial.city,
// description: commercial.description,
// email: commercial.email,
// price: commercial.price,
// state: sso.locations.find(l => l.value === commercial.state)?.name,
// title: commercial.title,
// name: `${user.firstname} ${user.lastname}`,
// };
// const embedding = await createEmbedding(JSON.stringify(reducedCommercial));
// sleep(200);
const result = await db.insert(schema.commercials).values(commercial).returning();
// logger.info(`commercial_${index} inserted`);
try {
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result.imagePath}/${result.serialId}`);
} catch (err) {
console.log(`----- No pictures available for ${id} ------`);
console.log(`----- No pictures available for ${id} ------ ${err}`);
}
}
//Business Listings
filePath = `./data/businesses.json`;
data = readFileSync(filePath, 'utf8');
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
const businessJsonData = JSON.parse(data) as BusinessImportListing[]; // Erwartet ein Array von Objekten
for (let index = 0; index < businessJsonData.length; index++) {
const business = businessJsonData[index];
const business = createDefaultBusinessListing(); //businessJsonData[index];
delete business.id;
business.type = sso.typesOfBusiness.find(e => e.oldValue === String(business.type)).value;
business.created = new Date(business.created);
business.updated = new Date(business.created);
const user = getRandomItem(generatedUserData);
business.email = 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);
business.type = sso.typesOfBusiness.find(e => e.oldValue === String(businessJsonData[index].type)).value;
business.title = businessJsonData[index].title;
business.description = businessJsonData[index].description;
try {
business.latitude = cityGeo.latitude;
business.longitude = cityGeo.longitude;
const cityGeo = geos.states.find(s => s.state_code === businessJsonData[index].state).cities.find(c => c.name === businessJsonData[index].city);
business.location = {};
business.location.latitude = cityGeo.latitude;
business.location.longitude = cityGeo.longitude;
business.location.city = businessJsonData[index].city;
business.location.state = businessJsonData[index].state;
} catch (e) {
console.log(`----------------> ERROR ${business.state} - ${business.city}`);
console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`);
continue;
}
// const embeddingText = JSON.stringify({
// type: typesOfBusiness.find(b => b.value === String(business.type))?.name,
// title: business.title,
// description: business.description,
// email: business.email,
// city: business.city,
// state: sso.locations.find(l => l.value === business.state)?.name,
// price: business.price,
// realEstateIncluded: business.realEstateIncluded,
// leasedLocation: business.leasedLocation,
// franchiseResale: business.franchiseResale,
// salesRevenue: business.salesRevenue,
// cashFlow: business.cashFlow,
// supportAndTraining: business.supportAndTraining,
// employees: business.employees,
// established: business.established,
// reasonForSale: business.reasonForSale,
// name: `${user.firstname} ${user.lastname}`,
// });
// const embedding = await createEmbedding(embeddingText);
sleep(200);
await db.insert(schema.businesses).values(business);
business.price = businessJsonData[index].price;
business.title = businessJsonData[index].title;
business.draft = businessJsonData[index].draft;
business.listingsCategory = 'business';
business.realEstateIncluded = businessJsonData[index].realEstateIncluded;
business.leasedLocation = businessJsonData[index].leasedLocation;
business.franchiseResale = businessJsonData[index].franchiseResale;
business.salesRevenue = businessJsonData[index].salesRevenue;
business.cashFlow = businessJsonData[index].cashFlow;
business.supportAndTraining = businessJsonData[index].supportAndTraining;
business.employees = businessJsonData[index].employees;
business.established = businessJsonData[index].established;
business.internalListingNumber = businessJsonData[index].internalListingNumber;
business.reasonForSale = businessJsonData[index].reasonForSale;
business.brokerLicencing = businessJsonData[index].brokerLicencing;
business.internals = businessJsonData[index].internals;
business.imageName = emailToDirName(user.email);
business.created = new Date(businessJsonData[index].created);
business.updated = new Date(businessJsonData[index].created);
await businessService.createListing(business); //db.insert(schema.businesses).values(business);
}
//End

View File

@@ -30,8 +30,6 @@ CREATE TABLE IF NOT EXISTS "businesses" (
"description" text,
"city" varchar(255),
"state" char(2),
"zipCode" integer,
"county" varchar(255),
"price" double precision,
"favoritesForUser" varchar(30)[],
"draft" boolean,
@@ -51,15 +49,13 @@ CREATE TABLE IF NOT EXISTS "businesses" (
"imageName" varchar(200),
"created" timestamp,
"updated" timestamp,
"visits" integer,
"lastVisit" timestamp,
"latitude" double precision,
"longitude" double precision
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "commercials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"serial_id" serial NOT NULL,
"serialId" serial NOT NULL,
"email" varchar(255),
"type" varchar(255),
"title" varchar(255),
@@ -69,16 +65,11 @@ CREATE TABLE IF NOT EXISTS "commercials" (
"price" double precision,
"favoritesForUser" varchar(30)[],
"listingsCategory" "listingsCategory",
"hideImage" boolean,
"draft" boolean,
"zipCode" integer,
"county" varchar(255),
"imageOrder" varchar(200)[],
"imagePath" varchar(200),
"created" timestamp,
"updated" timestamp,
"visits" integer,
"lastVisit" timestamp,
"latitude" double precision,
"longitude" double precision
);
@@ -93,7 +84,8 @@ CREATE TABLE IF NOT EXISTS "users" (
"companyName" varchar(255),
"companyOverview" text,
"companyWebsite" varchar(255),
"companyLocation" varchar(255),
"city" varchar(255),
"state" char(2),
"offeredServices" text,
"areasServed" jsonb,
"hasProfile" boolean,

View File

@@ -1 +0,0 @@
ALTER TABLE "commercials" RENAME COLUMN "serial_id" TO "serialId";

View File

@@ -1 +0,0 @@
ALTER TABLE "commercials" DROP COLUMN IF EXISTS "hideImage";

View File

@@ -1,5 +1,5 @@
{
"id": "aa3e53ed-4f1b-4e00-84ea-58939189a427",
"id": "a8283ca6-2c10-42bb-a640-ca984544ba30",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@@ -51,18 +51,6 @@
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
@@ -178,18 +166,6 @@
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
@@ -233,8 +209,8 @@
"notNull": true,
"default": "gen_random_uuid()"
},
"serial_id": {
"name": "serial_id",
"serialId": {
"name": "serialId",
"type": "serial",
"primaryKey": false,
"notNull": true
@@ -294,30 +270,12 @@
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
@@ -342,18 +300,6 @@
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
@@ -445,12 +391,18 @@
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",

View File

@@ -1,589 +0,0 @@
{
"id": "ff415931-0de6-4c89-900f-c6fd64830b2e",
"prevId": "aa3e53ed-4f1b-4e00-84ea-58939189a427",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "listingsCategory",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"imageName": {
"name": "imageName",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_email_users_email_fk": {
"name": "businesses_email_users_email_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"serialId": {
"name": "serialId",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "listingsCategory",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_email_users_email_fk": {
"name": "commercials_email_users_email_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gender": {
"name": "gender",
"type": "gender",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerType": {
"name": "customerType",
"type": "customerType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerSubType": {
"name": "customerSubType",
"type": "customerSubType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
}
},
"enums": {
"public.customerSubType": {
"name": "customerSubType",
"schema": "public",
"values": [
"broker",
"cpa",
"attorney",
"titleCompany",
"surveyor",
"appraiser"
]
},
"public.customerType": {
"name": "customerType",
"schema": "public",
"values": [
"buyer",
"professional"
]
},
"public.gender": {
"name": "gender",
"schema": "public",
"values": [
"male",
"female"
]
},
"public.listingsCategory": {
"name": "listingsCategory",
"schema": "public",
"values": [
"commercialProperty",
"business"
]
}
},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -1,583 +0,0 @@
{
"id": "146c197a-0ef7-4b10-84cd-352b88aba859",
"prevId": "ff415931-0de6-4c89-900f-c6fd64830b2e",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "listingsCategory",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"imageName": {
"name": "imageName",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_email_users_email_fk": {
"name": "businesses_email_users_email_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"serialId": {
"name": "serialId",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "listingsCategory",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_email_users_email_fk": {
"name": "commercials_email_users_email_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"email"
],
"columnsTo": [
"email"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gender": {
"name": "gender",
"type": "gender",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerType": {
"name": "customerType",
"type": "customerType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"customerSubType": {
"name": "customerSubType",
"type": "customerSubType",
"typeSchema": "public",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": {
"name": "latitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"longitude": {
"name": "longitude",
"type": "double precision",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_email_unique": {
"name": "users_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
}
}
}
},
"enums": {
"public.customerSubType": {
"name": "customerSubType",
"schema": "public",
"values": [
"broker",
"cpa",
"attorney",
"titleCompany",
"surveyor",
"appraiser"
]
},
"public.customerType": {
"name": "customerType",
"schema": "public",
"values": [
"buyer",
"professional"
]
},
"public.gender": {
"name": "gender",
"schema": "public",
"values": [
"male",
"female"
]
},
"public.listingsCategory": {
"name": "listingsCategory",
"schema": "public",
"values": [
"commercialProperty",
"business"
]
}
},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@@ -5,22 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1721737805677,
"tag": "0000_freezing_vengeance",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1721738173220,
"tag": "0001_steady_phantom_reporter",
"breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1722853523826,
"tag": "0002_chemical_gambit",
"when": 1723045357281,
"tag": "0000_lean_marvex",
"breakpoints": true
}
]

View File

@@ -7,7 +7,7 @@ export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', '
export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
id: uuid('id').primaryKey().defaultRandom().notNull(),
firstname: varchar('firstname', { length: 255 }).notNull(),
lastname: varchar('lastname', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
@@ -16,7 +16,8 @@ export const users = pgTable('users', {
companyName: varchar('companyName', { length: 255 }),
companyOverview: text('companyOverview'),
companyWebsite: varchar('companyWebsite', { length: 255 }),
companyLocation: varchar('companyLocation', { length: 255 }),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
offeredServices: text('offeredServices'),
areasServed: jsonb('areasServed').$type<AreasServed[]>(),
hasProfile: boolean('hasProfile'),
@@ -33,15 +34,15 @@ export const users = pgTable('users', {
});
export const businesses = pgTable('businesses', {
id: uuid('id').primaryKey().defaultRandom(),
id: uuid('id').primaryKey().defaultRandom().notNull(),
email: varchar('email', { length: 255 }).references(() => users.email),
type: varchar('type', { length: 255 }),
title: varchar('title', { length: 255 }),
description: text('description'),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
zipCode: integer('zipCode'),
county: varchar('county', { length: 255 }),
// zipCode: integer('zipCode'),
// county: varchar('county', { length: 255 }),
price: doublePrecision('price'),
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
draft: boolean('draft'),
@@ -61,15 +62,13 @@ export const businesses = pgTable('businesses', {
imageName: varchar('imageName', { length: 200 }),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
latitude: doublePrecision('latitude'),
longitude: doublePrecision('longitude'),
// embedding: vector('embedding', { dimensions: 1536 }),
});
export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(),
id: uuid('id').primaryKey().defaultRandom().notNull(),
serialId: serial('serialId'),
email: varchar('email', { length: 255 }).references(() => users.email),
type: varchar('type', { length: 255 }),
@@ -81,14 +80,12 @@ export const commercials = pgTable('commercials', {
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
listingsCategory: listingsCategoryEnum('listingsCategory'), //listingsCategory: varchar('listingsCategory', { length: 255 }),
draft: boolean('draft'),
zipCode: integer('zipCode'),
county: varchar('county', { length: 255 }),
// zipCode: integer('zipCode'),
// county: varchar('county', { length: 255 }),
imageOrder: varchar('imageOrder', { length: 200 }).array(),
imagePath: varchar('imagePath', { length: 200 }),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
latitude: doublePrecision('latitude'),
longitude: doublePrecision('longitude'),
// embedding: vector('embedding', { dimensions: 1536 }),

View File

@@ -53,16 +53,18 @@ export class GeoService {
result.push({
id: city.id,
city: city.name,
state: state.name,
state_code: state.state_code,
state: state.state_code,
//state_code: state.state_code,
latitude: city.latitude,
longitude: city.longitude,
});
}
});
});
return state ? result.filter(e => e.state_code.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_code: string }> {
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state_code: string }> = [];
findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> {
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = [];
const lowercasePrefix = prefix.toLowerCase();
@@ -74,7 +76,7 @@ export class GeoService {
id: state.id.toString(),
name: state.name,
type: 'state',
state_code: state.state_code,
state: state.state_code,
});
}
@@ -85,7 +87,7 @@ export class GeoService {
id: city.id.toString(),
name: city.name,
type: 'city',
state_code: state.state_code,
state: state.state_code,
});
}
}

View File

@@ -10,15 +10,15 @@ import { FileService } from '../file/file.service.js';
import { GeoService } from '../geo/geo.service.js';
import { BusinessListing, BusinessListingSchema } from '../models/db.model.js';
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
import { getDistanceQuery } from '../utils.js';
import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery } from '../utils.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 geoService: GeoService,
private fileService?: FileService,
private geoService?: GeoService,
) {}
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
@@ -29,7 +29,7 @@ export class BusinessListingService {
}
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}`);
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(businesses.type, criteria.types));
@@ -39,10 +39,6 @@ export class BusinessListingService {
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));
}
@@ -102,7 +98,7 @@ export class BusinessListingService {
if (criteria.brokerName) {
whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${criteria.brokerName}%`)));
}
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
return whereConditions;
}
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
@@ -129,7 +125,7 @@ export class BusinessListingService {
const data = await query;
const totalCount = await this.getBusinessListingsCount(criteria);
const results = data.map(r => r.business);
const results = data.map(r => r.business).map(r => convertDrizzleBusinessToBusiness(r));
return {
results,
totalCount,
@@ -149,33 +145,39 @@ export class BusinessListingService {
const [{ value: totalCount }] = await countQuery;
return totalCount;
}
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
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;
return convertDrizzleBusinessToBusiness(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
const listings = (await this.conn
.select()
.from(businesses)
.where(and(...conditions))) as BusinessListing[];
return listings.map(l => convertDrizzleBusinessToBusiness(l));
}
// #### CREATE ########################################
async createListing(data: BusinessListing): Promise<BusinessListing> {
try {
data.created = new Date();
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
data.updated = new Date();
const validatedBusinessListing = BusinessListingSchema.parse(data);
const [createdListing] = await this.conn.insert(businesses).values(validatedBusinessListing).returning();
return createdListing as BusinessListing;
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
return convertDrizzleBusinessToBusiness(createdListing);
} catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({
@@ -191,10 +193,11 @@ export class BusinessListingService {
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing> {
try {
data.updated = new Date();
data.created = new Date(data.created);
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
const validatedBusinessListing = BusinessListingSchema.parse(data);
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
return updateListing as BusinessListing;
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
return convertDrizzleBusinessToBusiness(updateListing);
} catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({

View File

@@ -1,5 +1,6 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { BusinessListing } from 'src/models/db.model.js';
import { Logger } from 'winston';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
@@ -20,7 +21,7 @@ export class BusinessListingsController {
@UseGuards(OptionalJwtAuthGuard)
@Get('user/:userid')
findByUserId(@Request() req, @Param('userid') userid: string): any {
findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
}

View File

@@ -10,15 +10,15 @@ import { FileService } from '../file/file.service.js';
import { GeoService } from '../geo/geo.service.js';
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js';
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
import { getDistanceQuery } from '../utils.js';
import { convertCommercialToDrizzleCommercial, convertDrizzleCommercialToCommercial, getDistanceQuery } from '../utils.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 geoService: GeoService,
private fileService?: FileService,
private geoService?: GeoService,
) {}
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
const whereConditions: SQL[] = [];
@@ -28,7 +28,7 @@ export class CommercialPropertyService {
}
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}`);
whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(schema.commercials.type, criteria.types));
@@ -38,10 +38,6 @@ export class CommercialPropertyService {
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));
}
@@ -53,14 +49,14 @@ export class CommercialPropertyService {
if (criteria.title) {
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
}
whereConditions.push(and(eq(schema.users.customerType, 'professional')));
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 query = this.conn.select({ commercial: commercials }).from(commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) {
@@ -71,7 +67,8 @@ export class CommercialPropertyService {
// Paginierung
query.limit(length).offset(start);
const results = await query;
const data = await query;
const results = data.map(r => r.commercial).map(r => convertDrizzleCommercialToCommercial(r));
const totalCount = await this.getCommercialPropertiesCount(criteria);
return {
@@ -80,7 +77,7 @@ export class CommercialPropertyService {
};
}
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(schema.commercials);
const countQuery = this.conn.select({ value: count() }).from(schema.commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) {
@@ -99,7 +96,7 @@ export class CommercialPropertyService {
.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;
return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
}
// #### Find by User EMail ########################################
@@ -109,10 +106,11 @@ export class CommercialPropertyService {
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(commercials.draft, true));
}
return (await this.conn
const listings = (await this.conn
.select()
.from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[];
return listings.map(l => convertDrizzleCommercialToCommercial(l)) as CommercialPropertyListing[];
}
// #### Find by imagePath ########################################
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
@@ -120,16 +118,17 @@ export class CommercialPropertyService {
.select()
.from(commercials)
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
return result[0] as CommercialPropertyListing;
return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
}
// #### CREATE ########################################
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
try {
data.created = new Date();
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
data.updated = new Date();
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
const [createdListing] = await this.conn.insert(commercials).values(validatedCommercialPropertyListing).returning();
return createdListing as CommercialPropertyListing;
const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning();
return convertDrizzleCommercialToCommercial(createdListing);
} catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({
@@ -145,7 +144,7 @@ export class CommercialPropertyService {
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
try {
data.updated = new Date();
data.created = new Date(data.created);
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
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)));
@@ -153,8 +152,9 @@ export class CommercialPropertyService {
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 CommercialPropertyListing;
const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning();
return convertDrizzleCommercialToCommercial(updateListing);
} catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({

View File

@@ -56,6 +56,7 @@ const USStates = z.enum([
'CA',
'CO',
'CT',
'DC',
'DE',
'FL',
'GA',
@@ -109,8 +110,29 @@ export const LicensedInSchema = z.object({
registerNo: z.string().nonempty('Registration number is required'),
state: z.string().nonempty('State is required'),
});
const phoneRegex = /^\+1 \(\d{3}\) \d{3}-\d{4}$/;
export const GeoSchema = z.object({
city: z.string(),
state: z.string().refine(val => USStates.safeParse(val).success, {
message: 'Invalid state. Must be a valid 2-letter US state code.',
}),
latitude: z.number().refine(
value => {
return value >= -90 && value <= 90;
},
{
message: 'Latitude muss zwischen -90 und 90 liegen',
},
),
longitude: z.number().refine(
value => {
return value >= -180 && value <= 180;
},
{
message: 'Longitude muss zwischen -180 und 180 liegen',
},
),
});
const phoneRegex = /^\(\d{3}\)\s\d{3}-\d{4}$/;
export const UserSchema = z
.object({
@@ -123,7 +145,7 @@ export const UserSchema = z
companyName: z.string().optional().nullable(),
companyOverview: z.string().optional().nullable(),
companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
companyLocation: z.string().optional().nullable(),
companyLocation: GeoSchema.optional().nullable(),
offeredServices: z.string().optional().nullable(),
areasServed: z.array(AreasServedSchema).optional().nullable(),
hasProfile: z.boolean().optional().nullable(),
@@ -134,8 +156,6 @@ export const UserSchema = z
customerSubType: CustomerSubTypeEnum.optional().nullable(),
created: z.date().optional().nullable(),
updated: z.date().optional().nullable(),
latitude: z.number().optional().nullable(),
longitude: z.number().optional().nullable(),
})
.superRefine((data, ctx) => {
if (data.customerType === 'professional') {
@@ -150,7 +170,7 @@ export const UserSchema = z
if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Phone number is required and must be in US format (+1 (XXX) XXX-XXXX) for professional customers',
message: 'Phone number is required and must be in US format (XXX) XXX-XXXX for professional customers',
path: ['phoneNumber'],
});
}
@@ -209,13 +229,8 @@ export const BusinessListingSchema = z.object({
}),
title: z.string().min(10),
description: z.string().min(10),
city: z.string(),
state: z.string().refine(val => USStates.safeParse(val).success, {
message: 'Invalid state. Must be a valid 2-letter US state code.',
}),
zipCode: z.number().int().positive().optional().nullable(),
county: z.string().optional().nullable(),
price: z.number().positive().max(100000000),
location: GeoSchema,
price: z.number().positive().max(1000000000),
favoritesForUser: z.array(z.string()),
draft: z.boolean(),
listingsCategory: ListingsCategoryEnum,
@@ -234,10 +249,6 @@ export const BusinessListingSchema = z.object({
imageName: z.string().optional().nullable(),
created: z.date(),
updated: z.date(),
visits: z.number().int().positive().optional().nullable(),
lastVisit: z.date().optional().nullable(),
latitude: z.number().optional().nullable(),
longitude: z.number().optional().nullable(),
});
export type BusinessListing = z.infer<typeof BusinessListingSchema>;
@@ -246,30 +257,20 @@ export const CommercialPropertyListingSchema = z
id: z.string().uuid().optional().nullable(),
serialId: z.number().int().positive().optional().nullable(),
email: z.string().email(),
//type: PropertyTypeEnum.optional(),
type: z.string().refine(val => PropertyTypeEnum.safeParse(val).success, {
message: 'Invalid type. Must be one of: ' + PropertyTypeEnum.options.join(', '),
}),
title: z.string().min(10),
description: z.string().min(10),
city: z.string(), // You might want to add a custom validation for valid US cities
state: z.string().refine(val => USStates.safeParse(val).success, {
message: 'Invalid state. Must be a valid 2-letter US state code.',
}), // You might want to add a custom validation for valid US states
price: z.number().positive().max(100000000),
location: GeoSchema,
price: z.number().positive().max(1000000000),
favoritesForUser: z.array(z.string()),
listingsCategory: ListingsCategoryEnum,
draft: z.boolean(),
zipCode: z.number().int().positive().nullable().optional(), // You might want to add a custom validation for valid US zip codes
county: z.string().nullable().optional(), // You might want to add a custom validation for valid US counties
imageOrder: z.array(z.string()),
imagePath: z.string().nullable().optional(),
created: z.date(),
updated: z.date(),
visits: z.number().int().positive().nullable().optional(),
lastVisit: z.date().nullable().optional(),
latitude: z.number().nullable().optional(),
longitude: z.number().nullable().optional(),
})
.strict();

View File

@@ -64,10 +64,9 @@ export interface ListCriteria {
searchType: 'exact' | 'radius';
// radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500';
radius: number;
criteriaType: 'business' | 'commercialProperty' | 'broker';
criteriaType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
}
export interface BusinessListingCriteria extends ListCriteria {
county: string;
minPrice: number;
maxPrice: number;
minRevenue: number;
@@ -83,21 +82,20 @@ export interface BusinessListingCriteria extends ListCriteria {
franchiseResale: boolean;
title: string;
brokerName: string;
criteriaType: 'business';
criteriaType: 'businessListings';
}
export interface CommercialPropertyListingCriteria extends ListCriteria {
county: string;
minPrice: number;
maxPrice: number;
title: string;
criteriaType: 'commercialProperty';
criteriaType: 'commercialPropertyListings';
}
export interface UserListingCriteria extends ListCriteria {
firstname: string;
lastname: string;
companyName: string;
counties: string[];
criteriaType: 'broker';
criteriaType: 'brokerListings';
}
export interface KeycloakUser {
@@ -228,13 +226,15 @@ export interface GeoResult {
id: number;
city: string;
state: string;
state_code: string;
// state_code: string;
latitude: number;
longitude: number;
}
export interface CityAndStateResult {
id: number;
name: string;
type: string;
state_code: string;
state: string;
}
export interface CountyResult {
id: number;
@@ -313,21 +313,14 @@ export function createDefaultCommercialPropertyListing(): CommercialPropertyList
type: null,
title: null,
description: null,
city: null,
state: null,
location: null,
price: null,
favoritesForUser: [],
draft: false,
zipCode: null,
county: null,
imageOrder: [],
imagePath: null,
created: null,
updated: null,
visits: null,
lastVisit: null,
latitude: null,
longitude: null,
listingsCategory: 'commercialProperty',
};
}
@@ -338,8 +331,7 @@ export function createDefaultBusinessListing(): BusinessListing {
type: null,
title: null,
description: null,
city: null,
state: null,
location: null,
price: null,
favoritesForUser: [],
draft: false,
@@ -357,10 +349,6 @@ export function createDefaultBusinessListing(): BusinessListing {
internals: null,
created: null,
updated: null,
visits: null,
lastVisit: null,
latitude: null,
longitude: null,
listingsCategory: 'business',
};
}

View File

@@ -18,8 +18,8 @@ export interface Geo {
nationality: string;
timezones: Timezone[];
translations: Translations;
latitude: string;
longitude: string;
latitude: number;
longitude: number;
emoji: string;
emojiU: string;
states: State[];
@@ -28,16 +28,16 @@ export interface State {
id: number;
name: string;
state_code: string;
latitude: string;
longitude: string;
latitude: number;
longitude: number;
type: string;
cities: City[];
}
export interface City {
id: number;
name: string;
latitude: string;
longitude: string;
latitude: number;
longitude: number;
}
export interface Translations {
kr: string;

View File

@@ -10,7 +10,7 @@ import { FileService } from '../file/file.service.js';
import { GeoService } from '../geo/geo.service.js';
import { User, UserSchema } from '../models/db.model.js';
import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
import { getDistanceQuery, toDrizzleUser } from '../utils.js';
import { convertDrizzleUserToUser, convertUserToDrizzleUser, getDistanceQuery } from '../utils.js';
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
@Injectable()
@@ -24,13 +24,13 @@ export class UserService {
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
const whereConditions: SQL[] = [];
whereConditions.push(eq(schema.users.customerType, 'professional'));
if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(ilike(schema.users.companyLocation, `%${criteria.city}%`));
whereConditions.push(ilike(schema.users.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(schema.users, parseFloat(cityGeo.latitude), parseFloat(cityGeo.longitude))} <= ${criteria.radius}`);
whereConditions.push(sql`${getDistanceQuery(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
// whereConditions.push(inArray(schema.users.customerSubType, criteria.types));
@@ -53,9 +53,6 @@ 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}%`})`)));
}
// 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})`)));
// }
if (criteria.state) {
whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${criteria.state})`);
}
@@ -75,7 +72,8 @@ export class UserService {
// Paginierung
query.limit(length).offset(start);
const results = await query;
const data = await query;
const results = data.map(r => convertDrizzleUserToUser(r));
const totalCount = await this.getUserListingsCount(criteria);
return {
@@ -102,12 +100,13 @@ export class UserService {
.where(sql`email = ${email}`)) as User[];
if (users.length === 0) {
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname) };
return await this.saveUser(user);
const u = await this.saveUser(user);
return convertDrizzleUserToUser(u);
} else {
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user;
return convertDrizzleUserToUser(user);
}
}
async getUserById(id: string) {
@@ -119,7 +118,7 @@ export class UserService {
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user;
return convertDrizzleUserToUser(user);
}
async saveUser(user: User): Promise<User> {
try {
@@ -130,14 +129,13 @@ export class UserService {
user.created = new Date();
}
const validatedUser = UserSchema.parse(user);
const drizzleUser = toDrizzleUser(validatedUser);
const drizzleUser = convertUserToDrizzleUser(validatedUser);
if (user.id) {
const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning();
return updateUser as User;
return convertDrizzleUserToUser(updateUser) as User;
} else {
const drizzleUser = toDrizzleUser(user);
const [newUser] = await this.conn.insert(schema.users).values(drizzleUser).returning();
return newUser as User;
return convertDrizzleUserToUser(newUser) as User;
}
} catch (error) {
if (error instanceof ZodError) {

View File

@@ -1,10 +1,8 @@
import { sql } from 'drizzle-orm';
import { z } from 'zod';
import { businesses, commercials, users } from './drizzle/schema.js';
import { AreasServedSchema, CustomerSubTypeEnum, CustomerTypeEnum, GenderEnum, LicensedInSchema, User } from './models/db.model.js';
import { BusinessListing, CommercialPropertyListing, User } from './models/db.model.js';
export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
export function convertStringToNullUndefined(value) {
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase();
@@ -31,32 +29,97 @@ export const getDistanceQuery = (schema: typeof businesses | typeof commercials
`;
};
export function toDrizzleUser(user: User): {
email: string;
firstname: string;
lastname: string;
phoneNumber?: string;
description?: string;
companyName?: string;
companyOverview?: string;
companyWebsite?: string;
companyLocation?: string;
offeredServices?: string;
areasServed?: (typeof AreasServedSchema._type)[];
hasProfile?: boolean;
hasCompanyLogo?: boolean;
licensedIn?: (typeof LicensedInSchema._type)[];
gender?: z.infer<typeof GenderEnum>;
customerType?: z.infer<typeof CustomerTypeEnum>;
customerSubType?: z.infer<typeof CustomerSubTypeEnum>;
latitude?: number;
longitude?: number;
} {
const { id, created, updated, ...drizzleUser } = user;
return {
...drizzleUser,
email: drizzleUser.email,
firstname: drizzleUser.firstname,
lastname: drizzleUser.lastname,
};
type DrizzleUser = typeof users.$inferSelect;
type DrizzleBusinessListing = typeof businesses.$inferSelect;
type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect;
export function convertBusinessToDrizzleBusiness(businessListing: Partial<BusinessListing>): DrizzleBusinessListing {
return flattenObject(businessListing);
}
export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): BusinessListing {
const o = {
location_city: drizzleBusinessListing.city,
location_state: drizzleBusinessListing.state,
location_latitude: drizzleBusinessListing.latitude,
location_longitude: drizzleBusinessListing.longitude,
...drizzleBusinessListing,
};
delete o.city;
delete o.state;
delete o.latitude;
delete o.longitude;
return unflattenObject(o);
}
export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial<CommercialPropertyListing>): DrizzleCommercialPropertyListing {
return flattenObject(commercialPropertyListing);
}
export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): CommercialPropertyListing {
const o = {
location_city: drizzleCommercialPropertyListing.city,
location_state: drizzleCommercialPropertyListing.state,
location_latitude: drizzleCommercialPropertyListing.latitude,
location_longitude: drizzleCommercialPropertyListing.longitude,
...drizzleCommercialPropertyListing,
};
delete o.city;
delete o.state;
delete o.latitude;
delete o.longitude;
return unflattenObject(o);
}
export function convertUserToDrizzleUser(user: Partial<User>): DrizzleUser {
return flattenObject(user);
}
export function convertDrizzleUserToUser(drizzleUser: Partial<DrizzleUser>): User {
const o = {
companyLocation_city: drizzleUser.city,
companyLocation_state: drizzleUser.state,
companyLocation_latitude: drizzleUser.latitude,
companyLocation_longitude: drizzleUser.longitude,
...drizzleUser,
};
delete o.city;
delete o.state;
delete o.latitude;
delete o.longitude;
return unflattenObject(o);
}
function flattenObject(obj: any, res: any = {}): any {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
if (value instanceof Date) {
res[key] = value;
} else {
flattenObject(value, res);
}
} else {
res[key] = value;
}
}
}
return res;
}
function unflattenObject(obj: any, separator: string = '_'): any {
const result: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const keys = key.split(separator);
keys.reduce((acc, curr, idx) => {
if (idx === keys.length - 1) {
acc[curr] = obj[key];
} else {
if (!acc[curr]) {
acc[curr] = {};
}
}
return acc[curr];
}, result);
}
}
return result;
}

View File

@@ -13,16 +13,16 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^18.0.6",
"@angular/animations": "^18.1.3",
"@angular/cdk": "^18.0.6",
"@angular/common": "^18.0.6",
"@angular/compiler": "^18.0.6",
"@angular/core": "^18.0.6",
"@angular/forms": "^18.0.6",
"@angular/platform-browser": "^18.0.6",
"@angular/platform-browser-dynamic": "^18.0.6",
"@angular/platform-server": "^18.0.6",
"@angular/router": "^18.0.6",
"@angular/common": "^18.1.3",
"@angular/compiler": "^18.1.3",
"@angular/core": "^18.1.3",
"@angular/forms": "^18.1.3",
"@angular/platform-browser": "^18.1.3",
"@angular/platform-browser-dynamic": "^18.1.3",
"@angular/platform-server": "^18.1.3",
"@angular/router": "^18.1.3",
"@fortawesome/angular-fontawesome": "^0.15.0",
"@fortawesome/fontawesome-free": "^6.5.2",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
@@ -43,6 +43,7 @@
"memoize-one": "^6.0.0",
"ngx-currency": "^18.0.0",
"ngx-image-cropper": "^8.0.0",
"ngx-mask": "^18.0.0",
"ngx-quill": "^26.0.5",
"on-change": "^5.0.1",
"rxjs": "~7.8.1",
@@ -52,9 +53,9 @@
"zone.js": "~0.14.7"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.0.7",
"@angular/cli": "^18.0.7",
"@angular/compiler-cli": "^18.0.6",
"@angular-devkit/build-angular": "^18.1.3",
"@angular/cli": "^18.1.3",
"@angular/compiler-cli": "^18.1.3",
"@types/express": "^4.17.21",
"@types/jasmine": "~5.1.4",
"@types/node": "^20.14.9",

View File

@@ -11,9 +11,45 @@
@if (loadingService.isLoading$ | async) {
<div class="spinner-overlay">
<div class="spinner-container">
<div class="spinner-text" *ngIf="loadingService.loadingText$ | async as loadingText">{{ loadingText }}</div>
@let loadingText = (loadingService.loadingText$ | async); @if(loadingText){
<div class="spinner-text">{{ loadingText }}</div>
}
<div role="status">
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
<!-- <span class="sr-only">Loading ...</span> -->
</div>
</div>
</div>
}
<!-- <div *ngIf="loadingService.isLoading$ | async" class="spinner-overlay">
<div class="spinner-container">
<ng-container *ngIf="loadingService.loadingText$ | async as loadingText">
<div *ngIf="loadingText" class="spinner-text">{{ loadingText }}</div>
</ng-container>
<div role="status">
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</div> -->
<app-message-container></app-message-container>
<app-search-modal></app-search-modal>
<app-confirmation></app-confirmation>

View File

@@ -1,25 +1,25 @@
.progress-spinner {
position: fixed;
z-index: 999;
top: 0;
left: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
align-items: center;
}
// .progress-spinner {
// position: fixed;
// z-index: 999;
// top: 0;
// left: 0;
// bottom: 0;
// right: 0;
// display: flex;
// flex-direction: column;
// align-items: center;
// }
.progress-spinner:before {
content: '';
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
}
// .progress-spinner:before {
// content: '';
// display: block;
// position: fixed;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// background-color: rgba(0, 0, 0, 0.3);
// }
.spinner-text {
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */

View File

@@ -5,6 +5,8 @@ import { KeycloakService } from 'keycloak-angular';
import { filter } from 'rxjs/operators';
import build from '../build';
import { ConfirmationComponent } from './components/confirmation/confirmation.component';
import { ConfirmationService } from './components/confirmation/confirmation.service';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component';
@@ -15,7 +17,7 @@ import { UserService } from './services/user.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent],
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
providers: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
@@ -25,7 +27,14 @@ export class AppComponent {
title = 'bizmatch';
actualRoute = '';
public constructor(public loadingService: LoadingService, private router: Router, private activatedRoute: ActivatedRoute, private keycloakService: KeycloakService, private userService: UserService) {
public constructor(
public loadingService: LoadingService,
private router: Router,
private activatedRoute: ActivatedRoute,
private keycloakService: KeycloakService,
private userService: UserService,
private confirmationService: ConfirmationService,
) {
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
let currentRoute = this.activatedRoute.root;
while (currentRoute.children[0] !== undefined) {
@@ -49,13 +58,6 @@ export class AppComponent {
}
showVersionDialog() {
// this.confirmationService.confirm({
// target: event.target as EventTarget,
// message: `App Version: ${this.build.timestamp}`,
// header: 'Version Info',
// icon: 'pi pi-info-circle',
// accept: () => {},
// reject: () => {},
// });
this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' });
}
}

View File

@@ -24,7 +24,9 @@ import { ConfirmationService } from './confirmation.service';
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmationService.message$ | async }}</h3>
@let confirmation = (confirmationService.confirmation$ | async);
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation.message }}</h3>
@if(confirmation.buttons==='both'){
<button
(click)="confirmationService.accept()"
type="button"
@@ -39,6 +41,7 @@ import { ConfirmationService } from './confirmation.service';
>
No, cancel
</button>
}
</div>
</div>
</div>

View File

@@ -1,19 +1,25 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface Confirmation {
message: string;
buttons?: 'both' | 'none';
button_accept_label?: string;
button_reject_label?: string;
}
@Injectable({
providedIn: 'root',
})
export class ConfirmationService {
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
private messageSubject = new BehaviorSubject<string>('');
private confirmationSubject = new BehaviorSubject<Confirmation>({ message: '' });
private resolvePromise!: (value: boolean) => void;
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
message$: Observable<string> = this.messageSubject.asObservable();
confirmation$: Observable<Confirmation> = this.confirmationSubject.asObservable();
showConfirmation(message: string): Promise<boolean> {
this.messageSubject.next(message);
showConfirmation(confirmation: Confirmation): Promise<boolean> {
confirmation.buttons = confirmation.buttons ? confirmation.buttons : 'both';
this.confirmationSubject.next(confirmation);
this.modalVisibleSubject.next(true);
return new Promise<boolean>(resolve => {
this.resolvePromise = resolve;

View File

@@ -1,7 +1,7 @@
import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop';
import { _getShadowRoot } from '@angular/cdk/platform';
import { CommonModule } from '@angular/common';
import { Component, ElementRef, input, output, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, Input, input, output, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model';
import { environment } from '../../../environments/environment';
@Component({
@@ -14,12 +14,11 @@ import { environment } from '../../../environments/environment';
export class DragDropMixedComponent {
@ViewChild('_container') _container!: ElementRef<HTMLDivElement>;
@ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>;
@Input() ts: number;
listing = input<CommercialPropertyListing>();
imageOrderChanged = output<string[]>();
imageToDelete = output<string>();
env = environment;
ts = new Date().getTime();
items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
private _cachedItems: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
@@ -34,12 +33,15 @@ export class DragDropMixedComponent {
};
private _containerStyle: CSSStyleDeclaration | null = null;
public isAnimationActive = false;
constructor(private cdr: ChangeDetectorRef) {}
ngOnChanges() {
this.items = this.listing()?.imageOrder;
this._cachedItems = this.items.slice();
}
ngAfterViewInit() {
// Führen Sie einen zusätzlichen Change Detection-Zyklus durch
this.cdr.detectChanges();
}
getImageUrl(image: string): string {
return `${this.env.imageBaseUrl}/pictures/property/${this.listing().imagePath}/${this.listing().serialId}/${image}?_ts=${this.ts}`;
}

View File

@@ -206,7 +206,7 @@
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/businessListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
aria-current="page"
(click)="closeMenus()"
(click)="closeMenusAndSetCriteria('businessListings')"
>Businesses</a
>
</li>
@@ -216,7 +216,7 @@
routerLink="/commercialPropertyListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/commercialPropertyListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
(click)="closeMenus()"
(click)="closeMenusAndSetCriteria('commercialPropertyListings')"
>Properties</a
>
</li>
@@ -226,7 +226,7 @@
routerLink="/brokerListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/brokerListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
(click)="closeMenus()"
(click)="closeMenusAndSetCriteria('brokerListings')"
>Professionals</a
>
</li>
@@ -247,141 +247,3 @@
</div>
}
</nav>
<!-- ############################### -->
<!-- Filter Dropdown -->
<!-- ############################### -->
<!-- <app-dropdown [triggerEl]="triggerButton" [triggerType]="'click'">
<div id="filterDropdown" class="z-[50] w-80 p-3 bg-slate-200 rounded-lg shadow-lg dark:bg-gray-700">
<div class="mb-4">
<label for="price-range" class="block text-sm font-medium text-gray-900 dark:text-white">Price Range</label>
<div class="flex items-center space-x-4">
<input
type="number"
id="price-from"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="From"
value="300"
/>
<input
type="number"
id="price-to"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="To"
value="3500"
/>
</div>
</div>
<div class="mb-4">
<label for="sales-range" class="block text-sm font-medium text-gray-900 dark:text-white">Sales Revenue</label>
<div class="flex items-center space-x-4">
<input
type="number"
id="sales-from"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="From"
value="1"
/>
<input
type="number"
id="sales-to"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="To"
value="100"
/>
</div>
</div>
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Category</label>
<div class="flex flex-wrap gap-2">
<button
class="px-3 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
>
Gaming
</button>
<button
class="px-3 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
>
Electronics
</button>
<button
class="px-3 py-1 text-sm font-medium text-white bg-blue-700 border border-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Phone
</button>
<button
class="px-3 py-1 text-sm font-medium text-white bg-blue-700 border border-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
TV/Monitor
</button>
<button
class="px-3 py-1 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-blue-500 dark:focus:text-white"
>
Laptop
</button>
<button
class="px-3 py-1 text-sm font-medium text-white bg-blue-700 border border-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Watch
</button>
</div>
</div>
<div class="mb-4">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">State</label>
<ul class="w-48 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<li class="w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600">
<div class="flex items-center ps-3">
<input
id="state-all"
type="radio"
value="all"
name="state"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
checked
/>
<label for="state-all" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">All</label>
</div>
</li>
<li class="w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600">
<div class="flex items-center ps-3">
<input
id="state-new"
type="radio"
value="new"
name="state"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label for="state-new" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">New</label>
</div>
</li>
<li class="w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600">
<div class="flex items-center ps-3">
<input
id="state-refurbished"
type="radio"
value="refurbished"
name="state"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
/>
<label for="state-refurbished" class="w-full py-3 ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Refurbished</label>
</div>
</li>
</ul>
</div>
<div class="flex justify-between">
<button
type="button"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Show 32 Results
</button>
<button
type="button"
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
>
Reset
</button>
</div>
</div>
</app-dropdown> -->

View File

@@ -6,7 +6,6 @@ import { NavigationEnd, Router, RouterModule } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { filter, Observable, Subject, Subscription } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
@@ -15,7 +14,7 @@ import { CriteriaChangeService } from '../../services/criteria-change.service';
import { SearchService } from '../../services/search.service';
import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service';
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaStateObject, map2User } from '../../utils/utils';
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils';
import { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service';
@Component({
@@ -80,43 +79,43 @@ export class HeaderComponent {
private checkCurrentRoute(url: string): void {
this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/'
const specialRoutes = [, '', ''];
if ('businessListings' === this.baseRoute) {
//this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandlerWrapper('business'));
//this.criteria = onChange(getCriteriaStateObject('business'), this.getSessionStorageHandler);
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('business'));
} else if ('commercialPropertyListings' === this.baseRoute) {
// this.criteria = onChange(getCriteriaStateObject('commercialProperty'), getSessionStorageHandlerWrapper('commercialProperty'));
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('commercialProperty'));
} else if ('brokerListings' === this.baseRoute) {
// this.criteria = onChange(getCriteriaStateObject('broker'), getSessionStorageHandlerWrapper('broker'));
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('broker'));
} else {
this.criteria = undefined;
}
this.criteria = getCriteriaProxy(this.baseRoute, this);
this.searchService.search(this.criteria);
}
private createEnhancedProxy(obj: any) {
const component = this;
// getCriteriaProxy(path:string):BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria{
// if ('businessListings' === path) {
// return this.createEnhancedProxy(getCriteriaStateObject('business'));
// } else if ('commercialPropertyListings' === path) {
// return this.createEnhancedProxy(getCriteriaStateObject('commercialProperty'));
// } else if ('brokerListings' === path) {
// return this.createEnhancedProxy(getCriteriaStateObject('broker'));
// } else {
// return undefined;
// }
// }
// private createEnhancedProxy(obj: any) {
// const component = this;
const sessionStorageHandler = function (path, value, previous, applyData) {
let criteriaType = '';
if ('/businessListings' === window.location.pathname) {
criteriaType = 'business';
} else if ('/commercialPropertyListings' === window.location.pathname) {
criteriaType = 'commercialProperty';
} else if ('/brokerListings' === window.location.pathname) {
criteriaType = 'broker';
}
sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this));
};
// const sessionStorageHandler = function (path, value, previous, applyData) {
// let criteriaType = '';
// if ('/businessListings' === window.location.pathname) {
// criteriaType = 'business';
// } else if ('/commercialPropertyListings' === window.location.pathname) {
// criteriaType = 'commercialProperty';
// } else if ('/brokerListings' === window.location.pathname) {
// criteriaType = 'broker';
// }
// 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);
// 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();
});
}
// // Notify about the criteria change using the component's context
// component.criteriaChangeService.notifyCriteriaChange();
// });
// }
ngAfterViewInit() {}
@@ -161,9 +160,12 @@ export class HeaderComponent {
collapse.collapse();
}
}
closeMenus() {
closeMenusAndSetCriteria(path: string) {
this.closeDropdown();
this.closeMobileMenu();
const criteria = getCriteriaProxy(path, this);
criteria.page = 1;
criteria.start = 0;
}
ngOnDestroy() {
@@ -171,11 +173,11 @@ export class HeaderComponent {
this.destroy$.complete();
}
getNumberOfFiltersSet() {
if (this.criteria?.criteriaType === 'broker') {
if (this.criteria?.criteriaType === 'brokerListings') {
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else if (this.criteria?.criteriaType === 'business') {
} else if (this.criteria?.criteriaType === 'businessListings') {
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else if (this.criteria?.criteriaType === 'commercialProperty') {
} else if (this.criteria?.criteriaType === 'commercialPropertyListings') {
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else {
return 0;

View File

@@ -48,23 +48,15 @@ export class ImageCropAndUploadComponent {
this.imageChangedEvent = null;
this.croppedImage = null;
this.showModal = false;
this.fileInput.nativeElement.value = '';
this.uploadFinished.emit({ success: false, type: this.uploadParams.type });
}
uploadImage() {
async uploadImage() {
if (this.croppedImage) {
this.imageService.uploadImage(this.croppedImage, this.uploadParams.type, this.uploadParams.imagePath, this.uploadParams.serialId).subscribe(
response => {
console.log('Upload successful', response);
this.closeModal();
this.uploadFinished.emit({ success: true, type: this.uploadParams.type });
},
error => {
console.error('Upload failed', error);
this.closeModal();
this.uploadFinished.emit({ success: false, type: this.uploadParams.type });
},
);
await this.imageService.uploadImage(this.croppedImage, this.uploadParams.type, this.uploadParams.imagePath, this.uploadParams.serialId);
this.closeModal();
this.uploadFinished.emit({ success: true, type: this.uploadParams.type });
}
}

View File

@@ -15,7 +15,7 @@
<button class="text-blue-600 font-medium border-b-2 border-blue-600 pb-2">Classic Search</button>
<button class="text-gray-500">AI Search <span class="bg-gray-200 text-xs font-semibold px-2 py-1 rounded">BETA</span></button>
</div>
@if(criteria.criteriaType==='business'){
@if(criteria.criteriaType==='businessListings'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
@@ -39,7 +39,7 @@
(ngModelChange)="setCity($event)"
>
@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 }} - {{ city.state }}</ng-option>
}
</ng-select>
</div>
@@ -233,7 +233,7 @@
</div>
</div>
</div>
} @if(criteria.criteriaType==='commercialProperty'){
} @if(criteria.criteriaType==='commercialPropertyListings'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div>
@@ -341,7 +341,7 @@
</div>
</div>
</div>
} @if(criteria.criteriaType==='broker'){
} @if(criteria.criteriaType==='brokerListings'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4">
<div>

View File

@@ -1,6 +1,7 @@
import { AsyncPipe, NgIf } from '@angular/common';
import { Component } from '@angular/core';
import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { catchError, concat, debounceTime, distinctUntilChanged, map, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, CountyResult, GeoResult, KeyValue, KeyValueStyle, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { CriteriaChangeService } from '../../services/criteria-change.service';
@@ -10,7 +11,7 @@ import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { SharedModule } from '../../shared/shared/shared.module';
import { ModalService } from './modal.service';
@UntilDestroy()
@Component({
selector: 'app-search-modal',
standalone: true,
@@ -38,10 +39,16 @@ export class SearchModalComponent {
) {}
ngOnInit() {
this.setupCriteriaChangeListener();
this.modalService.message$.subscribe(msg => {
this.modalService.message$.pipe(untilDestroyed(this)).subscribe(msg => {
this.criteria = msg;
this.setTotalNumberOfResults();
});
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
if (val) {
this.criteria.page = 1;
this.criteria.start = 0;
}
});
this.loadCities();
this.loadCounties();
}
@@ -92,7 +99,7 @@ export class SearchModalComponent {
setCity(city) {
if (city) {
this.criteria.city = city.city;
this.criteria.state = city.state_code;
this.criteria.state = city.state;
} else {
this.criteria.city = null;
this.criteria.radius = null;
@@ -131,9 +138,9 @@ export class SearchModalComponent {
setTotalNumberOfResults() {
if (this.criteria) {
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
if (this.criteria.criteriaType === 'business' || this.criteria.criteriaType === 'commercialProperty') {
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria, this.criteria.criteriaType);
} else if (this.criteria.criteriaType === 'broker') {
if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') {
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria, this.criteria.criteriaType === 'businessListings' ? 'business' : 'commercialProperty');
} else if (this.criteria.criteriaType === 'brokerListings') {
this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria);
} else {
this.numberOfResults$ = of();

View File

@@ -0,0 +1,29 @@
<div>
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit"
>{{ label }} @if(validationMessage){
<div
attr.data-tooltip-target="tooltip-{{ name }}"
class="absolute inline-flex items-center justify-center w-6 h-6 text-xs font-bold text-white bg-red-500 border-2 border-white rounded-full -top-2 dark:border-gray-900 hover:cursor-pointer"
>
!
</div>
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage"></app-tooltip>
}
</label>
<ng-select
class="custom"
[multiple]="false"
[hideSelected]="true"
[trackByFn]="trackByFn"
[minTermLength]="2"
[loading]="cityLoading"
typeToSearchText="Please enter 2 or more characters"
[typeahead]="cityInput$"
ngModel="{{ value?.city }} {{ value ? '-' : '' }} {{ value?.state }}"
(ngModelChange)="onInputChange($event)"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option>
}
</ng-select>
</div>

View File

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

View File

@@ -0,0 +1,68 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import { catchError, concat, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs';
import { GeoResult } from '../../../../../bizmatch-server/src/models/main.model';
import { City } from '../../../../../bizmatch-server/src/models/server.model';
import { GeoService } from '../../services/geo.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@Component({
selector: 'app-validated-city',
standalone: true,
imports: [CommonModule, FormsModule, NgSelectModule, TooltipComponent],
templateUrl: './validated-city.component.html',
styleUrl: './validated-city.component.scss',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedCityComponent),
multi: true,
},
],
})
export class ValidatedCityComponent extends BaseInputComponent {
@Input() items;
cities$: Observable<GeoResult[]>;
cityInput$ = new Subject<string>();
countyInput$ = new Subject<string>();
cityLoading = false;
constructor(validationMessagesService: ValidationMessagesService, private geoService: GeoService, public selectOptions: SelectOptionsService) {
super(validationMessagesService);
}
override ngOnInit() {
super.ngOnInit();
this.loadCities();
}
onInputChange(event: City): void {
this.value = event; //{ ...event, longitude: parseFloat(event.longitude), latitude: parseFloat(event.latitude) };
this.onChange(this.value);
}
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;
}
compareFn = (item, selected) => {
return item.id === selected.id;
};
}

View File

@@ -19,5 +19,7 @@
(blur)="onTouched()"
[attr.name]="name"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
[mask]="mask"
[dropSpecialCharacters]="false"
/>
</div>

View File

@@ -1,6 +1,7 @@
import { CommonModule } from '@angular/common';
import { Component, forwardRef, Input } from '@angular/core';
import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask';
import { BaseInputComponent } from '../base-input/base-input.component';
import { TooltipComponent } from '../tooltip/tooltip.component';
import { ValidationMessagesService } from '../validation-messages.service';
@@ -9,17 +10,19 @@ import { ValidationMessagesService } from '../validation-messages.service';
selector: 'app-validated-input',
templateUrl: './validated-input.component.html',
standalone: true,
imports: [CommonModule, FormsModule, TooltipComponent],
imports: [CommonModule, FormsModule, TooltipComponent, NgxMaskDirective, NgxMaskPipe],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ValidatedInputComponent),
multi: true,
},
provideNgxMask(),
],
})
export class ValidatedInputComponent extends BaseInputComponent {
@Input() kind: 'text' | 'number' | 'email' | 'tel' = 'text';
@Input() mask: string;
constructor(validationMessagesService: ValidationMessagesService) {
super(validationMessagesService);
}

View File

@@ -2,10 +2,9 @@ import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { lastValueFrom } from 'rxjs';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { MessageService } from '../../../components/message/message.service';
import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
@@ -17,7 +16,7 @@ import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../../../utils/utils';
import { map2User } from '../../../utils/utils';
@Component({
selector: 'app-details-business-listing',
standalone: true,
@@ -47,7 +46,6 @@ export class DetailsBusinessListingComponent {
];
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
listing: BusinessListing;
criteria: BusinessListingCriteria;
mailinfo: MailInfo;
environment = environment;
keycloakUser: KeycloakUser;
@@ -76,7 +74,6 @@ export class DetailsBusinessListingComponent {
}
});
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandler.bind('business'));
}
async ngOnInit() {
@@ -84,7 +81,7 @@ export class DetailsBusinessListingComponent {
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation.state };
}
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
this.listingUser = await this.userService.getByMail(this.listing.email);
@@ -124,7 +121,7 @@ export class DetailsBusinessListingComponent {
}
return [
{ label: 'Category', value: this.selectOptions.getBusiness(this.listing.type) },
{ label: 'Located in', value: `${this.listing.city}, ${this.selectOptions.getState(this.listing.state)}` },
{ label: 'Located in', value: `${this.listing.location.city}, ${this.selectOptions.getState(this.listing.location.state)}` },
{ label: 'Asking Price', value: `$${this.listing.price?.toLocaleString()}` },
{ label: 'Sales revenue', value: `$${this.listing.salesRevenue?.toLocaleString()}` },
{ label: 'Cash flow', value: `$${this.listing.cashFlow?.toLocaleString()}` },

View File

@@ -101,8 +101,11 @@
<div class="bg-white shadow-md rounded-lg overflow-hidden">
<div class="p-6 relative">
<h1 class="text-3xl font-bold mb-4">{{ listing?.title }}</h1>
<button class="absolute top-4 right-4 text-gray-500 hover:text-gray-700" (click)="historyService.goBack()">
<fa-icon [icon]="faTimes" size="2x"></fa-icon>
<button
(click)="historyService.goBack()"
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
>
<i class="fas fa-times"></i>
</button>
<div class="lg:hidden">
@if (listing && listing.imageOrder && listing.imageOrder.length > 0) {

View File

@@ -86,7 +86,7 @@ export class DetailsCommercialPropertyListingComponent {
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation.state };
}
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
this.listingUser = await this.userService.getByMail(this.listing.email);
@@ -96,10 +96,8 @@ export class DetailsCommercialPropertyListingComponent {
});
this.propertyDetails = [
{ label: 'Property Category', value: this.selectOptions.getCommercialProperty(this.listing.type) },
{ label: 'Located in', value: this.selectOptions.getState(this.listing.state) },
{ label: 'City', value: this.listing.city },
{ label: 'Zip Code', value: this.listing.zipCode },
{ label: 'County', value: this.listing.county },
{ label: 'Located in', value: this.selectOptions.getState(this.listing.location.state) },
{ label: 'City', value: this.listing.location.city },
{ label: 'Asking Price:', value: `$${this.listing.price?.toLocaleString()}` },
];
//this.initFlowbite();

View File

@@ -138,7 +138,7 @@
@if(user){
<div class="bg-white shadow-md rounded-lg overflow-hidden">
<!-- Header -->
<div class="flex items-center justify-between p-4 border-b">
<div class="flex items-center justify-between p-4 border-b relative">
<div class="flex items-center space-x-4">
<!-- <img src="https://placehold.co/80x80" alt="Profile picture of Avery Brown smiling" class="w-20 h-20 rounded-full" /> -->
@if(user.hasProfile){
@@ -167,7 +167,12 @@
}
<!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> -->
</div>
<button class="text-red-500 text-2xl" (click)="historyService.goBack()">&times;</button>
<button
(click)="historyService.goBack()"
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
>
<i class="fas fa-times"></i>
</button>
</div>
<!-- Description -->
@@ -194,7 +199,7 @@
</div>
<div class="flex flex-col sm:flex-row sm:items-center">
<span class="font-semibold w-40 p-2">Company Location</span>
<span class="p-2 flex-grow">{{ user.companyLocation }}</span>
<span class="p-2 flex-grow">{{ user.companyLocation.city }} - {{ user.companyLocation.state }}</span>
</div>
</div>

View File

@@ -114,7 +114,7 @@
groupBy="type"
>
@for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.name }} - {{ city.state_code }}</ng-option>
<ng-option [value]="city">{{ city.name }} - {{ city.state }}</ng-option>
}
</ng-select>
</div>

View File

@@ -5,7 +5,6 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { NgSelectModule } from '@ng-select/ng-select';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { catchError, concat, debounceTime, distinctUntilChanged, Observable, of, Subject, Subscription, switchMap, tap } from 'rxjs';
import { BusinessListingCriteria, CityAndStateResult, CommercialPropertyListingCriteria, GeoResult, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { ModalService } from '../../components/search-modal/modal.service';
@@ -15,7 +14,7 @@ import { ListingsService } from '../../services/listings.service';
import { SearchService } from '../../services/search.service';
import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaStateObject, map2User } from '../../utils/utils';
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, createEnhancedProxy, getCriteriaStateObject, map2User } from '../../utils/utils';
@UntilDestroy()
@Component({
selector: 'app-home',
@@ -55,10 +54,10 @@ export class HomeComponent {
) {}
async ngOnInit() {
const token = await this.keycloakService.getToken();
sessionStorage.removeItem('business_criteria');
sessionStorage.removeItem('commercialProperty_criteria');
sessionStorage.removeItem('broker_criteria');
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('business'));
sessionStorage.removeItem('businessListings');
sessionStorage.removeItem('commercialPropertyListings');
sessionStorage.removeItem('brokerListings');
this.criteria = createEnhancedProxy(getCriteriaStateObject('businessListings'), this);
this.user = map2User(token);
this.loadCities();
this.setupCriteriaChangeListener();
@@ -66,31 +65,31 @@ export class HomeComponent {
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
this.activeTabAction = tabname;
if ('business' === tabname) {
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('business'));
this.criteria = createEnhancedProxy(getCriteriaStateObject('businessListings'), this);
} else if ('commercialProperty' === tabname) {
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('commercialProperty'));
this.criteria = createEnhancedProxy(getCriteriaStateObject('commercialPropertyListings'), this);
} else if ('broker' === tabname) {
this.criteria = this.createEnhancedProxy(getCriteriaStateObject('broker'));
this.criteria = createEnhancedProxy(getCriteriaStateObject('brokerListings'), this);
} else {
this.criteria = undefined;
}
}
private createEnhancedProxy(obj: any) {
const component = this;
// 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));
};
// 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);
// 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();
});
}
// // Notify about the criteria change using the component's context
// component.criteriaChangeService.notifyCriteriaChange();
// });
// }
search() {
this.router.navigate([`${this.activeTabAction}Listings`]);
}
@@ -154,32 +153,33 @@ export class HomeComponent {
setCityOrState(cityOrState: CityAndStateResult) {
if (cityOrState) {
if (cityOrState.type === 'state') {
this.criteria.state = cityOrState.state_code;
this.criteria.state = cityOrState.state;
} else {
this.criteria.city = cityOrState.name;
this.criteria.state = cityOrState.state_code;
this.criteria.state = cityOrState.state;
this.criteria.searchType = 'radius';
this.criteria.radius = 20;
}
} else {
this.criteria.state = null;
this.criteria.city = null;
this.criteria.radius = null;
this.criteria.searchType = 'exact';
}
}
getTypes() {
if (this.criteria.criteriaType === 'business') {
if (this.criteria.criteriaType === 'businessListings') {
return this.selectOptions.typesOfBusiness;
} else if (this.criteria.criteriaType === 'commercialProperty') {
} else if (this.criteria.criteriaType === 'commercialPropertyListings') {
return this.selectOptions.typesOfCommercialProperty;
} else {
return this.selectOptions.customerSubTypes;
}
}
getPlaceholderLabel() {
if (this.criteria.criteriaType === 'business') {
if (this.criteria.criteriaType === 'businessListings') {
return 'Business Type';
} else if (this.criteria.criteriaType === 'commercialProperty') {
} else if (this.criteria.criteriaType === 'commercialPropertyListings') {
return 'Property Type';
} else {
return 'Professional Type';
@@ -188,9 +188,9 @@ export class HomeComponent {
setTotalNumberOfResults() {
if (this.criteria) {
console.log(`Getting total number of results for ${this.criteria.criteriaType}`);
if (this.criteria.criteriaType === 'business' || this.criteria.criteriaType === 'commercialProperty') {
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria, this.criteria.criteriaType);
} else if (this.criteria.criteriaType === 'broker') {
if (this.criteria.criteriaType === 'businessListings' || this.criteria.criteriaType === 'commercialPropertyListings') {
this.numberOfResults$ = this.listingService.getNumberOfListings(this.criteria, this.criteria.criteriaType === 'businessListings' ? 'business' : 'commercialProperty');
} else if (this.criteria.criteriaType === 'brokerListings') {
this.numberOfResults$ = this.userService.getNumberOfBroker(this.criteria);
} else {
this.numberOfResults$ = of();
@@ -198,11 +198,11 @@ export class HomeComponent {
}
}
getNumberOfFiltersSet() {
if (this.criteria?.criteriaType === 'broker') {
if (this.criteria?.criteriaType === 'brokerListings') {
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else if (this.criteria?.criteriaType === 'business') {
} else if (this.criteria?.criteriaType === 'businessListings') {
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else if (this.criteria?.criteriaType === 'commercialProperty') {
} else if (this.criteria?.criteriaType === 'commercialPropertyListings') {
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else {
return 0;

View File

@@ -53,10 +53,10 @@ export class BrokerListingsComponent {
private route: ActivatedRoute,
private searchService: SearchService,
) {
this.criteria = getCriteriaStateObject('broker');
this.criteria = getCriteriaStateObject('brokerListings');
this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'broker') {
if (criteria && criteria.criteriaType === 'brokerListings') {
this.criteria = criteria as UserListingCriteria;
this.search();
}
@@ -74,6 +74,7 @@ export class BrokerListingsComponent {
this.users = usersReponse.results;
this.totalRecords = usersReponse.totalCount;
this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1;
this.page = this.criteria.page ? this.criteria.page : 1;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}

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">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">Location: {{ listing.city }} - {{ selectOptions.getState(listing.state) }}</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">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" />
<div class="flex-grow"></div>

View File

@@ -49,29 +49,28 @@ export class BusinessListingsComponent {
private route: ActivatedRoute,
private searchService: SearchService,
) {
this.criteria = getCriteriaStateObject('business');
this.criteria = getCriteriaStateObject('businessListings');
this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'business') {
if (criteria && criteria.criteriaType === 'businessListings') {
this.criteria = criteria as BusinessListingCriteria;
this.search();
}
});
}
async ngOnInit() {}
async ngOnInit() {
this.search();
}
async init() {
this.reset();
const statesResult = await this.listingsService.getAllStates('business');
this.states = statesResult.map(ls => ({ name: this.selectOptions.getState(ls.state as string), value: ls.state, count: ls.count }));
this.search();
}
async search() {
//this.listings = await this.listingsService.getListingsByPrompt(this.criteria);
const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
this.listings = listingReponse.results;
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.page = this.criteria.page ? this.criteria.page : 1;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}

View File

@@ -1,74 +1,19 @@
<!--
<div class="surface-200 h-full">
<div class="wrapper">
<div class="grid">
@for (listing of listings; track listing.id) {
<div class="col-12 xl:col-4 flex">
<div class="surface-card p-2 flex flex-column flex-grow-1 justify-content-between" style="border-radius: 10px">
<article class="flex flex-column md:flex-row w-full gap-3 p-3 surface-card">
<div class="relative">
@if (listing.imageOrder?.length>0){
<img
src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}"
alt="Image"
class="border-round w-full h-full md:w-12rem md:h-9rem"
/>
} @else {
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
}
<p class="absolute px-2 py-1 border-round-lg text-sm font-normal text-white mt-0 mb-0" style="background-color: rgba(255, 255, 255, 0.3); backdrop-filter: invert(30%); top: 3%; left: 3%">
{{ selectOptions.getState(listing.state) }}
</p>
</div>
<div class="flex flex-column w-full gap-3">
<div class="flex w-full justify-content-between align-items-center flex-wrap gap-3">
<p class="font-semibold text-lg mt-0 mb-0">{{ listing.title }}</p>
</div>
<p class="font-normal text-lg text-600 mt-0 mb-0">{{ listing.city }}</p>
<div class="flex flex-wrap justify-content-between xl:h-2rem mt-auto">
<p class="text-base flex align-items-center text-900 mt-0 mb-1">
<i class="pi pi-list mr-2"></i>
<span class="font-medium">{{ selectOptions.getCommercialProperty(listing.type) }}</span>
</p>
</div>
<p class="font-semibold text-3xl text-900 mt-0 mb-2">{{ listing.price | currency }}</p>
</div>
</article>
<div class="px-4 py-3 text-left">
<button
pButton
pRipple
icon="pi pi-arrow-right"
iconPos="right"
label="View Full Listing"
class="p-button-rounded p-button-success"
[routerLink]="['/details-commercial-property-listing', listing.id]"
></button>
</div>
</div>
</div>
}
</div>
</div>
</div> -->
<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">
<!-- Property Card 1 -->
@for (listing of listings; track listing.id) {
<div class="bg-white rounded-lg shadow-md overflow-hidden">
@if (listing.imageOrder?.length>0){
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ getTS() }}" alt="Image" class="w-full h-48 object-cover" />
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}" alt="Image" class="w-full h-48 object-cover" />
} @else {
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="w-full h-48 object-cover" />
}
<div class="p-4">
<div class="flex items-center justify-between mb-2">
<span class="bg-gray-200 text-gray-700 text-xs font-semibold px-2 py-1 rounded">{{ selectOptions.getState(listing.state) }}</span>
<span class="bg-gray-200 text-gray-700 text-xs font-semibold px-2 py-1 rounded">{{ selectOptions.getState(listing.location.state) }}</span>
<span class="text-gray-600 text-sm"><i [class]="selectOptions.getIconTypeOfCommercials(listing.type)" class="mr-1"></i> {{ selectOptions.getCommercialProperty(listing.type) }}</span>
</div>
<h3 class="text-lg font-semibold mb-2">{{ listing.title }}</h3>
<p class="text-gray-600 mb-2">{{ listing.city }}</p>
<p class="text-gray-600 mb-2">{{ listing.location.city }}</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">
View Full Listing <i class="fas fa-arrow-right ml-1"></i>

View File

@@ -37,6 +37,7 @@ export class CommercialPropertyListingsComponent {
env = environment;
page = 1;
pageCount = 1;
ts = new Date().getTime();
constructor(
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,
@@ -47,10 +48,10 @@ export class CommercialPropertyListingsComponent {
private route: ActivatedRoute,
private searchService: SearchService,
) {
this.criteria = getCriteriaStateObject('commercialProperty');
this.criteria = getCriteriaStateObject('commercialPropertyListings');
this.init();
this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'commercialProperty') {
if (criteria && criteria.criteriaType === 'commercialPropertyListings') {
this.criteria = criteria as CommercialPropertyListingCriteria;
this.search();
}
@@ -62,16 +63,12 @@ export class CommercialPropertyListingsComponent {
this.states = statesResult.map(ls => ({ name: this.selectOptions.getState(ls.state as string), value: ls.state, count: ls.count }));
this.search();
}
refine() {
this.criteria.start = 0;
this.criteria.page = 0;
this.search();
}
async search() {
const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty');
this.listings = (<ResponseCommercialPropertyListingArray>listingReponse).results;
this.totalRecords = (<ResponseCommercialPropertyListingArray>listingReponse).totalCount;
this.pageCount = this.totalRecords % LISTINGS_PER_PAGE === 0 ? this.totalRecords / LISTINGS_PER_PAGE : Math.floor(this.totalRecords / LISTINGS_PER_PAGE) + 1;
this.page = this.criteria.page ? this.criteria.page : 1;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}

View File

@@ -71,13 +71,14 @@
<option *ngFor="let type of customerTypes" [value]="type">{{ type | titlecase }}</option>
</select>
</div> -->
@if (!isAdmin()){
<app-validated-select label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
}@else{
@if (isAdmin() && !id){
<div>
<label for="customerType" class="block text-sm font-medium text-gray-700">User Type</label>
<span class="bg-blue-100 text-blue-800 text-sm font-medium me-2 px-2.5 py-0.5 rounded dark:bg-blue-900 dark:text-blue-300">ADMIN</span>
</div>
}@else{
<app-validated-select label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
} @if (isProfessional){
<!-- <div>
<label for="customerSubType" class="block text-sm font-medium text-gray-700">Professional Type</label>
@@ -115,9 +116,10 @@
<label for="companyLocation" class="block text-sm font-medium text-gray-700">Company Location</label>
<input type="text" id="companyLocation" name="companyLocation" [(ngModel)]="user.companyLocation" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
</div> -->
<app-validated-input label="Your Phone Number" name="phoneNumber" [(ngModel)]="user.phoneNumber"></app-validated-input>
<app-validated-input label="Your Phone Number" name="phoneNumber" [(ngModel)]="user.phoneNumber" mask="(000) 000-0000"></app-validated-input>
<app-validated-input label="Company Website" name="companyWebsite" [(ngModel)]="user.companyWebsite"></app-validated-input>
<app-validated-input label="Company Location" name="companyLocation" [(ngModel)]="user.companyLocation"></app-validated-input>
<!-- <app-validated-input label="Company Location" name="companyLocation" [(ngModel)]="user.companyLocation"></app-validated-input> -->
<app-validated-city label="Company Location" name="companyLocation" [(ngModel)]="user.companyLocation"></app-validated-city>
</div>
<!-- <div>

View File

@@ -18,6 +18,7 @@ import { ImageCropAndUploadComponent, UploadReponse } from '../../../components/
import { MessageComponent } from '../../../components/message/message.component';
import { MessageService } from '../../../components/message/message.service';
import { TooltipComponent } from '../../../components/tooltip/tooltip.component';
import { ValidatedCityComponent } from '../../../components/validated-city/validated-city.component';
import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedQuillComponent } from '../../../components/validated-quill/validated-quill.component';
import { ValidatedSelectComponent } from '../../../components/validated-select/validated-select.component';
@@ -47,6 +48,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
ValidatedInputComponent,
ValidatedSelectComponent,
ValidatedQuillComponent,
ValidatedCityComponent,
TooltipComponent,
],
providers: [TitleCasePipe],
@@ -54,7 +56,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
styleUrl: './account.component.scss',
})
export class AccountComponent {
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User;
subscriptions: Array<Subscription>;
userSubscriptions: Array<Subscription> = [];
@@ -167,7 +169,7 @@ export class AccountComponent {
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => `${r.city} - ${r.state_code}`).slice(0, 5);
this.suggestions = result.map(r => `${r.city} - ${r.state}`).slice(0, 5);
}
addLicence() {
this.user.licensedIn.push({ registerNo: '', state: '' });
@@ -204,7 +206,7 @@ export class AccountComponent {
}
}
async deleteConfirm(type: 'profile' | 'logo') {
const confirmed = await this.confirmationService.showConfirmation(`Do you want to delete your ${type === 'logo' ? 'Logo' : 'Profile'} image`);
const confirmed = await this.confirmationService.showConfirmation({ message: `Do you want to delete your ${type === 'logo' ? 'Logo' : 'Profile'} image` });
if (confirmed) {
if (type === 'profile') {
this.user.hasProfile = false;

View File

@@ -52,8 +52,10 @@
</div>
</div> -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-ng-select label="State" name="state" [(ngModel)]="listing.state" [items]="selectOptions?.states"></app-validated-ng-select>
<app-validated-input label="City" name="city" [(ngModel)]="listing.city"></app-validated-input>
<!-- <app-validated-ng-select label="State" name="state" [(ngModel)]="listing.location.state" [items]="selectOptions?.states"></app-validated-ng-select>
<app-validated-input label="City" name="city" [(ngModel)]="listing.location.city"></app-validated-input> -->
<app-validated-city label="Location" name="location" [(ngModel)]="listing.location"></app-validated-city>
<app-validated-price label="Price" name="price" [(ngModel)]="listing.price"></app-validated-price>
</div>
<!-- <div class="flex mb-4 space-x-4">
@@ -83,8 +85,8 @@
</div>
</div> -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-price label="Price" name="price" [(ngModel)]="listing.price"></app-validated-price>
<app-validated-price label="Sales Revenue" name="salesRevenue" [(ngModel)]="listing.salesRevenue"></app-validated-price>
<app-validated-price label="Cash Flow" name="cashFlow" [(ngModel)]="listing.cashFlow"></app-validated-price>
</div>
<!-- <div class="mb-4">
@@ -99,9 +101,9 @@
currencyMask
/>
</div> -->
<div>
<app-validated-price label="Cash Flow" name="cashFlow" [(ngModel)]="listing.cashFlow"></app-validated-price>
</div>
<!-- <div>
</div> -->
<!-- <div class="flex mb-4 space-x-4">
<div class="w-1/2">

View File

@@ -17,6 +17,7 @@ import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing,
import { environment } from '../../../../environments/environment';
import { MessageService } from '../../../components/message/message.service';
import { ValidatedCityComponent } from '../../../components/validated-city/validated-city.component';
import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component';
import { ValidatedPriceComponent } from '../../../components/validated-price/validated-price.component';
@@ -45,6 +46,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
ValidatedNgSelectComponent,
ValidatedPriceComponent,
ValidatedTextareaComponent,
ValidatedCityComponent,
],
providers: [],
templateUrl: './edit-business-listing.component.html',
@@ -137,7 +139,7 @@ export class EditBusinessListingComponent {
suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => r.city).slice(0, 5);
}

View File

@@ -35,8 +35,9 @@
<label for="type" class="block text-sm font-bold text-gray-700 mb-1">Property Category</label>
<ng-select [items]="typesOfCommercialProperty" bindLabel="name" bindValue="value" [(ngModel)]="listing.type" name="type"> </ng-select>
</div> -->
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-ng-select label="Property Category" name="type" [(ngModel)]="listing.type" [items]="typesOfCommercialProperty"></app-validated-ng-select>
<app-validated-city label="Location" name="location" [(ngModel)]="listing.location"></app-validated-city>
</div>
<!-- <div class="flex mb-4 space-x-4">
@@ -49,10 +50,11 @@
<input type="text" id="city" [(ngModel)]="listing.city" name="city" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
</div> -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-ng-select label="State" name="state" [(ngModel)]="listing.state" [items]="selectOptions?.states"></app-validated-ng-select>
<app-validated-input label="City" name="city" [(ngModel)]="listing.city"></app-validated-input>
</div>
<!-- <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> -->
<!-- <app-validated-ng-select label="State" name="state" [(ngModel)]="listing.location.state" [items]="selectOptions?.states"></app-validated-ng-select>
<app-validated-input label="City" name="city" [(ngModel)]="listing.location.city"></app-validated-input> -->
<!-- </div> -->
<!-- <div class="flex mb-4 space-x-4">
<div class="w-1/2">
@@ -64,10 +66,10 @@
<input type="text" id="county" [(ngModel)]="listing.county" name="county" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
</div> -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-input label="Zip Code" name="zipCode" [(ngModel)]="listing.zipCode"></app-validated-input>
<app-validated-input label="County" name="county" [(ngModel)]="listing.county"></app-validated-input>
</div>
</div> -->
<!-- <div class="mb-4">
<label for="internals" class="block text-sm font-bold text-gray-700 mb-1">Internal Notes (Will not be shown on the listing, for your records only.)</label>
@@ -127,7 +129,7 @@
</div>
</div>
} -->
<app-drag-drop-mixed [listing]="listing" (imageOrderChanged)="imageOrderChanged($event)" (imageToDelete)="deleteConfirm($event)"></app-drag-drop-mixed>
<app-drag-drop-mixed [listing]="listing" [ts]="ts" (imageOrderChanged)="imageOrderChanged($event)" (imageToDelete)="deleteConfirm($event)"></app-drag-drop-mixed>
<!-- </div> -->
</div>
@if (mode!=='create'){

View File

@@ -21,6 +21,7 @@ import { ConfirmationComponent } from '../../../components/confirmation/confirma
import { ConfirmationService } from '../../../components/confirmation/confirmation.service';
import { DragDropMixedComponent } from '../../../components/drag-drop-mixed/drag-drop-mixed.component';
import { MessageService } from '../../../components/message/message.service';
import { ValidatedCityComponent } from '../../../components/validated-city/validated-city.component';
import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component';
import { ValidatedPriceComponent } from '../../../components/validated-price/validated-price.component';
@@ -50,6 +51,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
ValidatedQuillComponent,
ValidatedNgSelectComponent,
ValidatedPriceComponent,
ValidatedCityComponent,
],
providers: [],
templateUrl: './edit-commercial-property-listing.component.html',
@@ -102,7 +104,6 @@ export class EditCommercialPropertyListingComponent {
showModal = false;
imageChangedEvent: any = '';
croppedImage: Blob | null = null;
constructor(
public selectOptions: SelectOptionsService,
private router: Router,
@@ -175,7 +176,7 @@ export class EditCommercialPropertyListingComponent {
}
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => r.city).slice(0, 5);
}
openFileDialog() {
@@ -197,28 +198,24 @@ export class EditCommercialPropertyListingComponent {
this.showModal = false;
}
uploadImage() {
async uploadImage() {
if (this.croppedImage) {
this.imageService.uploadImage(this.croppedImage, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
async () => {
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
this.closeModal();
},
error => {
console.error('Upload failed', error);
},
);
await this.imageService.uploadImage(this.croppedImage, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId);
this.ts = new Date().getTime();
this.closeModal();
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
}
}
async deleteConfirm(imageName: string) {
const confirmed = await this.confirmationService.showConfirmation('Are you sure you want to delete this image?');
const confirmed = await this.confirmationService.showConfirmation({ message: 'Are you sure you want to delete this image?' });
if (confirmed) {
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
await this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName);
await this.listingsService.save(this.listing, 'commercialProperty');
this.listing = (await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'))) as CommercialPropertyListing;
this.messageService.addMessage({ severity: 'success', text: 'Image has been deleted', duration: 3000 });
this.ts = new Date().getTime();
} else {
console.log('deny');
}

View File

@@ -39,7 +39,7 @@ export class EmailUsComponent {
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation.state };
}
}
ngOnDestroy() {

View File

@@ -17,7 +17,7 @@
<tr *ngFor="let listing of myListings" class="border-b">
<td class="py-2 px-4">{{ listing.title }}</td>
<td class="py-2 px-4">{{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</td>
<td class="py-2 px-4">{{ listing.state }}</td>
<td class="py-2 px-4">{{ listing.location.state }}</td>
<td class="py-2 px-4">
@if(listing.listingsCategory==='business'){
<button class="bg-green-500 text-white p-2 rounded-full mr-2" [routerLink]="['/editBusinessListing', listing.id]">
@@ -52,7 +52,7 @@
<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>
<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.state }}</p>
<p class="text-gray-600 mb-4">Located in: {{ listing.location.city }} - {{ listing.location.state }}</p>
<div class="flex justify-end">
<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">

View File

@@ -54,7 +54,7 @@ export class MyListingComponent {
}
async confirm(listing: ListingType) {
const confirmed = await this.confirmationService.showConfirmation(`Are you sure you want to delete this listing?`);
const confirmed = await this.confirmationService.showConfirmation({ message: `Are you sure you want to delete this listing?` });
if (confirmed) {
// this.messageService.showMessage('Listing has been deleted');
this.deleteListing(listing);

View File

@@ -12,7 +12,7 @@ export class ImageService {
constructor(private http: HttpClient) {}
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string, serialId?: number) {
async uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string, serialId?: number) {
let uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
if (type === 'uploadPropertyPicture') {
uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}/${serialId}`;
@@ -20,9 +20,10 @@ export class ImageService {
const formData = new FormData();
formData.append('file', imageBlob, 'image.png');
return this.http.post(uploadUrl, formData, {
observe: 'events',
});
// return this.http.post(uploadUrl, formData, {
// observe: 'events',
// });
return await lastValueFrom(this.http.post(uploadUrl, formData));
}
async deleteListingImage(imagePath: string, serial: number, name?: string) {

View File

@@ -1,13 +1,15 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, shareReplay } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class LoadingService {
public loading$ = new BehaviorSubject<string[]>([]);
private loading$ = new BehaviorSubject<string[]>([]);
private loadingTextSubject = new BehaviorSubject<string | null>(null);
private excludedUrls: string[] = ['/findTotal', '/geo']; // Liste der URLs, für die kein Ladeindikator angezeigt werden soll
loadingText$: Observable<string | null> = this.loadingTextSubject.asObservable();
public isLoading$ = this.loading$.asObservable().pipe(
@@ -17,13 +19,15 @@ export class LoadingService {
shareReplay(1),
);
public startLoading(type: string, request?: string): void {
if (!this.loading$.value.includes(type)) {
this.loading$.next(this.loading$.value.concat(type));
if (type === 'uploadImage' || request?.includes('uploadImage') || request?.includes('uploadPropertyPicture') || request?.includes('uploadProfile') || request?.includes('uploadCompanyLogo')) {
this.loadingTextSubject.next("Please wait - we're processing your image...");
} else {
this.loadingTextSubject.next(null);
public startLoading(type: string, url?: string): void {
if (this.shouldShowLoading(url)) {
if (!this.loading$.value.includes(type)) {
this.loading$.next(this.loading$.value.concat(type));
if (this.isImageUpload(type, url)) {
this.loadingTextSubject.next("Please wait - we're processing your image...");
} else {
this.loadingTextSubject.next(null);
}
}
}
}
@@ -34,4 +38,13 @@ export class LoadingService {
this.loadingTextSubject.next(null);
}
}
private shouldShowLoading(url?: string): boolean {
if (!url) return true;
return !this.excludedUrls.some(excludedUrl => url.includes(excludedUrl));
}
private isImageUpload(type: string, url?: string): boolean {
return type === 'uploadImage' || url?.includes('uploadImage') || url?.includes('uploadPropertyPicture') || url?.includes('uploadProfile') || url?.includes('uploadCompanyLogo');
}
}

View File

@@ -97,8 +97,7 @@ export function createEmptyBusinessListingCriteria(): BusinessListingCriteria {
city: '',
types: [],
prompt: '',
criteriaType: 'business',
county: '',
criteriaType: 'businessListings',
minPrice: null,
maxPrice: null,
minRevenue: null,
@@ -128,8 +127,7 @@ export function createEmptyCommercialPropertyListingCriteria(): CommercialProper
city: '',
types: [],
prompt: '',
criteriaType: 'commercialProperty',
county: '',
criteriaType: 'commercialPropertyListings',
minPrice: null,
maxPrice: null,
title: '',
@@ -146,7 +144,7 @@ export function createEmptyUserListingCriteria(): UserListingCriteria {
city: '',
types: [],
prompt: '',
criteriaType: 'broker',
criteriaType: 'brokerListings',
firstname: '',
lastname: '',
companyName: '',
@@ -186,16 +184,16 @@ export const getSessionStorageHandlerWrapper = param => {
};
};
export function getCriteriaStateObject(criteriaType: 'business' | 'commercialProperty' | 'broker') {
export function getCriteriaStateObject(criteriaType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings') {
let initialState;
if (criteriaType === 'business') {
if (criteriaType === 'businessListings') {
initialState = createEmptyBusinessListingCriteria();
} else if (criteriaType === 'commercialProperty') {
} else if (criteriaType === 'commercialPropertyListings') {
initialState = createEmptyCommercialPropertyListingCriteria();
} else {
initialState = createEmptyUserListingCriteria();
}
const storedState = sessionStorage.getItem(`${criteriaType}_criteria`);
const storedState = sessionStorage.getItem(`${criteriaType}`);
return storedState ? JSON.parse(storedState) : initialState;
}
@@ -244,6 +242,7 @@ export function getDialogWidth(dimensions): string {
}
import { initFlowbite } from 'flowbite';
import onChange from 'on-change';
import { Subject, concatMap, delay, of } from 'rxjs';
const flowbiteQueue = new Subject<() => void>();
@@ -372,3 +371,41 @@ function arraysEqual(arr1: any[] | null | undefined, arr2: any[] | null | undefi
}
return true;
}
// -----------------------------
// Criteria Proxy
// -----------------------------
export function getCriteriaProxy(path: string, component: any): BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria {
if ('businessListings' === path) {
return createEnhancedProxy(getCriteriaStateObject('businessListings'), component);
} else if ('commercialPropertyListings' === path) {
return createEnhancedProxy(getCriteriaStateObject('commercialPropertyListings'), component);
} else if ('brokerListings' === path) {
return createEnhancedProxy(getCriteriaStateObject('brokerListings'), component);
} else {
return undefined;
}
}
export function createEnhancedProxy(obj: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria, component: any) {
// const component = this;
const sessionStorageHandler = function (path, value, previous, applyData) {
// let criteriaType = '';
// if ('/businessListings' === window.location.pathname) {
// criteriaType = 'business';
// } else if ('/commercialPropertyListings' === window.location.pathname) {
// criteriaType = 'commercialProperty';
// } else if ('/brokerListings' === window.location.pathname) {
// criteriaType = 'broker';
// }
sessionStorage.setItem(`${obj.criteriaType}`, 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();
});
}