7 Commits

64 changed files with 361074 additions and 122384 deletions

View File

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

View File

@@ -30,8 +30,6 @@ CREATE TABLE IF NOT EXISTS "businesses" (
"description" text, "description" text,
"city" varchar(255), "city" varchar(255),
"state" char(2), "state" char(2),
"zipCode" integer,
"county" varchar(255),
"price" double precision, "price" double precision,
"favoritesForUser" varchar(30)[], "favoritesForUser" varchar(30)[],
"draft" boolean, "draft" boolean,
@@ -51,15 +49,13 @@ CREATE TABLE IF NOT EXISTS "businesses" (
"imageName" varchar(200), "imageName" varchar(200),
"created" timestamp, "created" timestamp,
"updated" timestamp, "updated" timestamp,
"visits" integer,
"lastVisit" timestamp,
"latitude" double precision, "latitude" double precision,
"longitude" double precision "longitude" double precision
); );
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE IF NOT EXISTS "commercials" ( CREATE TABLE IF NOT EXISTS "commercials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"serial_id" serial NOT NULL, "serialId" serial NOT NULL,
"email" varchar(255), "email" varchar(255),
"type" varchar(255), "type" varchar(255),
"title" varchar(255), "title" varchar(255),
@@ -69,16 +65,11 @@ CREATE TABLE IF NOT EXISTS "commercials" (
"price" double precision, "price" double precision,
"favoritesForUser" varchar(30)[], "favoritesForUser" varchar(30)[],
"listingsCategory" "listingsCategory", "listingsCategory" "listingsCategory",
"hideImage" boolean,
"draft" boolean, "draft" boolean,
"zipCode" integer,
"county" varchar(255),
"imageOrder" varchar(200)[], "imageOrder" varchar(200)[],
"imagePath" varchar(200), "imagePath" varchar(200),
"created" timestamp, "created" timestamp,
"updated" timestamp, "updated" timestamp,
"visits" integer,
"lastVisit" timestamp,
"latitude" double precision, "latitude" double precision,
"longitude" double precision "longitude" double precision
); );
@@ -93,7 +84,8 @@ CREATE TABLE IF NOT EXISTS "users" (
"companyName" varchar(255), "companyName" varchar(255),
"companyOverview" text, "companyOverview" text,
"companyWebsite" varchar(255), "companyWebsite" varchar(255),
"companyLocation" varchar(255), "city" varchar(255),
"state" char(2),
"offeredServices" text, "offeredServices" text,
"areasServed" jsonb, "areasServed" jsonb,
"hasProfile" boolean, "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", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
@@ -51,18 +51,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"price": { "price": {
"name": "price", "name": "price",
"type": "double precision", "type": "double precision",
@@ -178,18 +166,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": { "latitude": {
"name": "latitude", "name": "latitude",
"type": "double precision", "type": "double precision",
@@ -233,8 +209,8 @@
"notNull": true, "notNull": true,
"default": "gen_random_uuid()" "default": "gen_random_uuid()"
}, },
"serial_id": { "serialId": {
"name": "serial_id", "name": "serialId",
"type": "serial", "type": "serial",
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
@@ -294,30 +270,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": { "draft": {
"name": "draft", "name": "draft",
"type": "boolean", "type": "boolean",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": { "imageOrder": {
"name": "imageOrder", "name": "imageOrder",
"type": "varchar(200)[]", "type": "varchar(200)[]",
@@ -342,18 +300,6 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"latitude": { "latitude": {
"name": "latitude", "name": "latitude",
"type": "double precision", "type": "double precision",
@@ -445,12 +391,18 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"companyLocation": { "city": {
"name": "companyLocation", "name": "city",
"type": "varchar(255)", "type": "varchar(255)",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"offeredServices": { "offeredServices": {
"name": "offeredServices", "name": "offeredServices",
"type": "text", "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, "idx": 0,
"version": "7", "version": "7",
"when": 1721737805677, "when": 1723045357281,
"tag": "0000_freezing_vengeance", "tag": "0000_lean_marvex",
"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",
"breakpoints": true "breakpoints": true
} }
] ]

View File

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

View File

@@ -53,16 +53,18 @@ export class GeoService {
result.push({ result.push({
id: city.id, id: city.id,
city: city.name, city: city.name,
state: state.name, state: state.state_code,
state_code: 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 }> { 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_code: string }> = []; const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = [];
const lowercasePrefix = prefix.toLowerCase(); const lowercasePrefix = prefix.toLowerCase();
@@ -74,7 +76,7 @@ export class GeoService {
id: state.id.toString(), id: state.id.toString(),
name: state.name, name: state.name,
type: 'state', type: 'state',
state_code: state.state_code, state: state.state_code,
}); });
} }
@@ -85,7 +87,7 @@ export class GeoService {
id: city.id.toString(), id: city.id.toString(),
name: city.name, name: city.name,
type: 'city', 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 { GeoService } from '../geo/geo.service.js';
import { BusinessListing, BusinessListingSchema } from '../models/db.model.js'; import { BusinessListing, BusinessListingSchema } from '../models/db.model.js';
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js'; import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
import { getDistanceQuery } from '../utils.js'; import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery } from '../utils.js';
@Injectable() @Injectable()
export class BusinessListingService { export class BusinessListingService {
constructor( constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>, @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService: FileService, private fileService?: FileService,
private geoService: GeoService, private geoService?: GeoService,
) {} ) {}
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] { private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
@@ -29,7 +29,7 @@ export class BusinessListingService {
} }
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
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) { if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(businesses.type, criteria.types)); whereConditions.push(inArray(businesses.type, criteria.types));
@@ -39,10 +39,6 @@ export class BusinessListingService {
whereConditions.push(eq(businesses.state, criteria.state)); whereConditions.push(eq(businesses.state, criteria.state));
} }
if (criteria.county) {
whereConditions.push(ilike(businesses.city, `%${criteria.county}%`)); // Assuming county is part of city, adjust if necessary
}
if (criteria.minPrice) { if (criteria.minPrice) {
whereConditions.push(gte(businesses.price, criteria.minPrice)); whereConditions.push(gte(businesses.price, criteria.minPrice));
} }
@@ -102,7 +98,7 @@ export class BusinessListingService {
if (criteria.brokerName) { if (criteria.brokerName) {
whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${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; return whereConditions;
} }
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) { async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
@@ -129,7 +125,7 @@ export class BusinessListingService {
const data = await query; const data = await query;
const totalCount = await this.getBusinessListingsCount(criteria); 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 { return {
results, results,
totalCount, totalCount,
@@ -149,33 +145,39 @@ export class BusinessListingService {
const [{ value: totalCount }] = await countQuery; const [{ value: totalCount }] = await countQuery;
return totalCount; return totalCount;
} }
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> { async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
let result = await this.conn let result = await this.conn
.select() .select()
.from(businesses) .from(businesses)
.where(and(sql`${businesses.id} = ${id}`)); .where(and(sql`${businesses.id} = ${id}`));
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN')); 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[]> { async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
const conditions = []; const conditions = [];
conditions.push(eq(businesses.imageName, emailToDirName(email))); conditions.push(eq(businesses.imageName, emailToDirName(email)));
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) { if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(businesses.draft, true)); conditions.push(ne(businesses.draft, true));
} }
return (await this.conn const listings = (await this.conn
.select() .select()
.from(businesses) .from(businesses)
.where(and(...conditions))) as BusinessListing[]; .where(and(...conditions))) as BusinessListing[];
return listings.map(l => convertDrizzleBusinessToBusiness(l));
} }
// #### CREATE ######################################## // #### CREATE ########################################
async createListing(data: BusinessListing): Promise<BusinessListing> { async createListing(data: BusinessListing): Promise<BusinessListing> {
try { 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(); data.updated = new Date();
const validatedBusinessListing = BusinessListingSchema.parse(data); const validatedBusinessListing = BusinessListingSchema.parse(data);
const [createdListing] = await this.conn.insert(businesses).values(validatedBusinessListing).returning(); const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
return createdListing as BusinessListing; const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
return convertDrizzleBusinessToBusiness(createdListing);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const formattedErrors = error.errors.map(err => ({
@@ -191,10 +193,11 @@ export class BusinessListingService {
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing> { async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing> {
try { try {
data.updated = new Date(); 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 validatedBusinessListing = BusinessListingSchema.parse(data);
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning(); const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
return updateListing as BusinessListing; const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
return convertDrizzleBusinessToBusiness(updateListing);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ 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 { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { BusinessListing } from 'src/models/db.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { BusinessListingCriteria, JwtUser } from '../models/main.model.js'; import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
@@ -20,7 +21,7 @@ export class BusinessListingsController {
@UseGuards(OptionalJwtAuthGuard) @UseGuards(OptionalJwtAuthGuard)
@Get('user/:userid') @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); 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 { GeoService } from '../geo/geo.service.js';
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js'; import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js';
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js'; import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
import { getDistanceQuery } from '../utils.js'; import { convertCommercialToDrizzleCommercial, convertDrizzleCommercialToCommercial, getDistanceQuery } from '../utils.js';
@Injectable() @Injectable()
export class CommercialPropertyService { export class CommercialPropertyService {
constructor( constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>, @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService: FileService, private fileService?: FileService,
private geoService: GeoService, private geoService?: GeoService,
) {} ) {}
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] { private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
const whereConditions: SQL[] = []; const whereConditions: SQL[] = [];
@@ -28,7 +28,7 @@ export class CommercialPropertyService {
} }
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
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) { if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(schema.commercials.type, criteria.types)); whereConditions.push(inArray(schema.commercials.type, criteria.types));
@@ -38,10 +38,6 @@ export class CommercialPropertyService {
whereConditions.push(eq(schema.commercials.state, criteria.state)); whereConditions.push(eq(schema.commercials.state, criteria.state));
} }
if (criteria.county) {
whereConditions.push(ilike(schema.commercials.county, `%${criteria.county}%`));
}
if (criteria.minPrice) { if (criteria.minPrice) {
whereConditions.push(gte(schema.commercials.price, criteria.minPrice)); whereConditions.push(gte(schema.commercials.price, criteria.minPrice));
} }
@@ -53,14 +49,14 @@ export class CommercialPropertyService {
if (criteria.title) { if (criteria.title) {
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${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; return whereConditions;
} }
// #### Find by criteria ######################################## // #### Find by criteria ########################################
async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> { async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0; const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12; const length = criteria.length ? criteria.length : 12;
const 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); const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) { if (whereConditions.length > 0) {
@@ -71,7 +67,8 @@ export class CommercialPropertyService {
// Paginierung // Paginierung
query.limit(length).offset(start); 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); const totalCount = await this.getCommercialPropertiesCount(criteria);
return { return {
@@ -80,7 +77,7 @@ export class CommercialPropertyService {
}; };
} }
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): Promise<number> { 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); const whereConditions = this.getWhereConditions(criteria);
if (whereConditions.length > 0) { if (whereConditions.length > 0) {
@@ -99,7 +96,7 @@ export class CommercialPropertyService {
.from(commercials) .from(commercials)
.where(and(sql`${commercials.id} = ${id}`)); .where(and(sql`${commercials.id} = ${id}`));
result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN')); 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 ######################################## // #### Find by User EMail ########################################
@@ -109,10 +106,11 @@ export class CommercialPropertyService {
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) { if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(ne(commercials.draft, true)); conditions.push(ne(commercials.draft, true));
} }
return (await this.conn const listings = (await this.conn
.select() .select()
.from(commercials) .from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[]; .where(and(...conditions))) as CommercialPropertyListing[];
return listings.map(l => convertDrizzleCommercialToCommercial(l)) as CommercialPropertyListing[];
} }
// #### Find by imagePath ######################################## // #### Find by imagePath ########################################
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> { async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
@@ -120,16 +118,17 @@ export class CommercialPropertyService {
.select() .select()
.from(commercials) .from(commercials)
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`)); .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
return result[0] as CommercialPropertyListing; return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
} }
// #### CREATE ######################################## // #### CREATE ########################################
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> { async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
try { 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(); data.updated = new Date();
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data); const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
const [createdListing] = await this.conn.insert(commercials).values(validatedCommercialPropertyListing).returning(); const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
return createdListing as CommercialPropertyListing; const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning();
return convertDrizzleCommercialToCommercial(createdListing);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const formattedErrors = error.errors.map(err => ({
@@ -145,7 +144,7 @@ export class CommercialPropertyService {
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> { async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
try { try {
data.updated = new Date(); 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 validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId)); 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))); 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(',')}`); this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
data.imageOrder = imageOrder; data.imageOrder = imageOrder;
} }
const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning(); const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
return updateListing as CommercialPropertyListing; const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning();
return convertDrizzleCommercialToCommercial(updateListing);
} catch (error) { } catch (error) {
if (error instanceof ZodError) { if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({ const formattedErrors = error.errors.map(err => ({

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,8 @@
import { sql } from 'drizzle-orm'; import { sql } from 'drizzle-orm';
import { z } from 'zod';
import { businesses, commercials, users } from './drizzle/schema.js'; 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_KM = 6371; // Erdradius in Kilometern
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
export function convertStringToNullUndefined(value) { export function convertStringToNullUndefined(value) {
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung // Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase(); const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase();
@@ -31,32 +29,97 @@ export const getDistanceQuery = (schema: typeof businesses | typeof commercials
`; `;
}; };
export function toDrizzleUser(user: User): { type DrizzleUser = typeof users.$inferSelect;
email: string; type DrizzleBusinessListing = typeof businesses.$inferSelect;
firstname: string; type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect;
lastname: string; export function convertBusinessToDrizzleBusiness(businessListing: Partial<BusinessListing>): DrizzleBusinessListing {
phoneNumber?: string; return flattenObject(businessListing);
description?: string; }
companyName?: string; export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): BusinessListing {
companyOverview?: string; const o = {
companyWebsite?: string; location_city: drizzleBusinessListing.city,
companyLocation?: string; location_state: drizzleBusinessListing.state,
offeredServices?: string; location_latitude: drizzleBusinessListing.latitude,
areasServed?: (typeof AreasServedSchema._type)[]; location_longitude: drizzleBusinessListing.longitude,
hasProfile?: boolean; ...drizzleBusinessListing,
hasCompanyLogo?: boolean; };
licensedIn?: (typeof LicensedInSchema._type)[]; delete o.city;
gender?: z.infer<typeof GenderEnum>; delete o.state;
customerType?: z.infer<typeof CustomerTypeEnum>; delete o.latitude;
customerSubType?: z.infer<typeof CustomerSubTypeEnum>; delete o.longitude;
latitude?: number; return unflattenObject(o);
longitude?: number; }
} { export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial<CommercialPropertyListing>): DrizzleCommercialPropertyListing {
const { id, created, updated, ...drizzleUser } = user; return flattenObject(commercialPropertyListing);
return { }
...drizzleUser, export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): CommercialPropertyListing {
email: drizzleUser.email, const o = {
firstname: drizzleUser.firstname, location_city: drizzleCommercialPropertyListing.city,
lastname: drizzleUser.lastname, 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, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^18.0.6", "@angular/animations": "^18.1.3",
"@angular/cdk": "^18.0.6", "@angular/cdk": "^18.0.6",
"@angular/common": "^18.0.6", "@angular/common": "^18.1.3",
"@angular/compiler": "^18.0.6", "@angular/compiler": "^18.1.3",
"@angular/core": "^18.0.6", "@angular/core": "^18.1.3",
"@angular/forms": "^18.0.6", "@angular/forms": "^18.1.3",
"@angular/platform-browser": "^18.0.6", "@angular/platform-browser": "^18.1.3",
"@angular/platform-browser-dynamic": "^18.0.6", "@angular/platform-browser-dynamic": "^18.1.3",
"@angular/platform-server": "^18.0.6", "@angular/platform-server": "^18.1.3",
"@angular/router": "^18.0.6", "@angular/router": "^18.1.3",
"@fortawesome/angular-fontawesome": "^0.15.0", "@fortawesome/angular-fontawesome": "^0.15.0",
"@fortawesome/fontawesome-free": "^6.5.2", "@fortawesome/fontawesome-free": "^6.5.2",
"@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/fontawesome-svg-core": "^6.5.2",
@@ -43,6 +43,7 @@
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"ngx-currency": "^18.0.0", "ngx-currency": "^18.0.0",
"ngx-image-cropper": "^8.0.0", "ngx-image-cropper": "^8.0.0",
"ngx-mask": "^18.0.0",
"ngx-quill": "^26.0.5", "ngx-quill": "^26.0.5",
"on-change": "^5.0.1", "on-change": "^5.0.1",
"rxjs": "~7.8.1", "rxjs": "~7.8.1",
@@ -52,9 +53,9 @@
"zone.js": "~0.14.7" "zone.js": "~0.14.7"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^18.0.7", "@angular-devkit/build-angular": "^18.1.3",
"@angular/cli": "^18.0.7", "@angular/cli": "^18.1.3",
"@angular/compiler-cli": "^18.0.6", "@angular/compiler-cli": "^18.1.3",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jasmine": "~5.1.4", "@types/jasmine": "~5.1.4",
"@types/node": "^20.14.9", "@types/node": "^20.14.9",

View File

@@ -11,9 +11,45 @@
@if (loadingService.isLoading$ | async) { @if (loadingService.isLoading$ | async) {
<div class="spinner-overlay"> <div class="spinner-overlay">
<div class="spinner-container"> <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> </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-message-container></app-message-container>
<app-search-modal></app-search-modal> <app-search-modal></app-search-modal>
<app-confirmation></app-confirmation>

View File

@@ -1,25 +1,25 @@
.progress-spinner { // .progress-spinner {
position: fixed; // position: fixed;
z-index: 999; // z-index: 999;
top: 0; // top: 0;
left: 0; // left: 0;
bottom: 0; // bottom: 0;
right: 0; // right: 0;
display: flex; // display: flex;
flex-direction: column; // flex-direction: column;
align-items: center; // align-items: center;
} // }
.progress-spinner:before { // .progress-spinner:before {
content: ''; // content: '';
display: block; // display: block;
position: fixed; // position: fixed;
top: 0; // top: 0;
left: 0; // left: 0;
width: 100%; // width: 100%;
height: 100%; // height: 100%;
background-color: rgba(0, 0, 0, 0.3); // background-color: rgba(0, 0, 0, 0.3);
} // }
.spinner-text { .spinner-text {
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */ margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
font-size: 20px; /* Schriftgröße nach Bedarf 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 { filter } from 'rxjs/operators';
import build from '../build'; 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 { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component'; import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component'; import { MessageContainerComponent } from './components/message/message-container.component';
@@ -15,7 +17,7 @@ import { UserService } from './services/user.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent], imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
providers: [], providers: [],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
@@ -25,7 +27,14 @@ export class AppComponent {
title = 'bizmatch'; title = 'bizmatch';
actualRoute = ''; 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(() => { this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
let currentRoute = this.activatedRoute.root; let currentRoute = this.activatedRoute.root;
while (currentRoute.children[0] !== undefined) { while (currentRoute.children[0] !== undefined) {
@@ -49,13 +58,6 @@ export class AppComponent {
} }
showVersionDialog() { showVersionDialog() {
// this.confirmationService.confirm({ this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' });
// target: event.target as EventTarget,
// message: `App Version: ${this.build.timestamp}`,
// header: 'Version Info',
// icon: 'pi pi-info-circle',
// accept: () => {},
// reject: () => {},
// });
} }
} }

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"> <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" /> <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> </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 <button
(click)="confirmationService.accept()" (click)="confirmationService.accept()"
type="button" type="button"
@@ -39,6 +41,7 @@ import { ConfirmationService } from './confirmation.service';
> >
No, cancel No, cancel
</button> </button>
}
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -1,7 +1,7 @@
import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop'; import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop';
import { _getShadowRoot } from '@angular/cdk/platform'; import { _getShadowRoot } from '@angular/cdk/platform';
import { CommonModule } from '@angular/common'; 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 { CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
@Component({ @Component({
@@ -14,12 +14,11 @@ import { environment } from '../../../environments/environment';
export class DragDropMixedComponent { export class DragDropMixedComponent {
@ViewChild('_container') _container!: ElementRef<HTMLDivElement>; @ViewChild('_container') _container!: ElementRef<HTMLDivElement>;
@ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>; @ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>;
@Input() ts: number;
listing = input<CommercialPropertyListing>(); listing = input<CommercialPropertyListing>();
imageOrderChanged = output<string[]>(); imageOrderChanged = output<string[]>();
imageToDelete = output<string>(); imageToDelete = output<string>();
env = environment; env = environment;
ts = new Date().getTime();
items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9]; items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
private _cachedItems: 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; private _containerStyle: CSSStyleDeclaration | null = null;
public isAnimationActive = false; public isAnimationActive = false;
constructor(private cdr: ChangeDetectorRef) {}
ngOnChanges() { ngOnChanges() {
this.items = this.listing()?.imageOrder; this.items = this.listing()?.imageOrder;
this._cachedItems = this.items.slice(); this._cachedItems = this.items.slice();
} }
ngAfterViewInit() {
// Führen Sie einen zusätzlichen Change Detection-Zyklus durch
this.cdr.detectChanges();
}
getImageUrl(image: string): string { getImageUrl(image: string): string {
return `${this.env.imageBaseUrl}/pictures/property/${this.listing().imagePath}/${this.listing().serialId}/${image}?_ts=${this.ts}`; 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') }" [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" 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" aria-current="page"
(click)="closeMenus()" (click)="closeMenusAndSetCriteria('businessListings')"
>Businesses</a >Businesses</a
> >
</li> </li>
@@ -216,7 +216,7 @@
routerLink="/commercialPropertyListings" routerLink="/commercialPropertyListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/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" 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 >Properties</a
> >
</li> </li>
@@ -226,7 +226,7 @@
routerLink="/brokerListings" routerLink="/brokerListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/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" 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 >Professionals</a
> >
</li> </li>
@@ -247,141 +247,3 @@
</div> </div>
} }
</nav> </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 { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { Collapse, Dropdown, initFlowbite } from 'flowbite'; import { Collapse, Dropdown, initFlowbite } from 'flowbite';
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { filter, Observable, Subject, Subscription } from 'rxjs'; import { filter, Observable, Subject, Subscription } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model'; import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model'; import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
@@ -15,7 +14,7 @@ import { CriteriaChangeService } from '../../services/criteria-change.service';
import { SearchService } from '../../services/search.service'; import { SearchService } from '../../services/search.service';
import { SharedService } from '../../services/shared.service'; import { SharedService } from '../../services/shared.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import { 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 { DropdownComponent } from '../dropdown/dropdown.component';
import { ModalService } from '../search-modal/modal.service'; import { ModalService } from '../search-modal/modal.service';
@Component({ @Component({
@@ -80,43 +79,43 @@ export class HeaderComponent {
private checkCurrentRoute(url: string): void { private checkCurrentRoute(url: string): void {
this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/' this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/'
const specialRoutes = [, '', '']; const specialRoutes = [, '', ''];
if ('businessListings' === this.baseRoute) { this.criteria = getCriteriaProxy(this.baseRoute, this);
//this.criteria = onChange(getCriteriaStateObject('business'), getSessionStorageHandlerWrapper('business')); this.searchService.search(this.criteria);
//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;
}
} }
private createEnhancedProxy(obj: any) { // getCriteriaProxy(path:string):BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria{
const component = this; // 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) { // const sessionStorageHandler = function (path, value, previous, applyData) {
let criteriaType = ''; // let criteriaType = '';
if ('/businessListings' === window.location.pathname) { // if ('/businessListings' === window.location.pathname) {
criteriaType = 'business'; // criteriaType = 'business';
} else if ('/commercialPropertyListings' === window.location.pathname) { // } else if ('/commercialPropertyListings' === window.location.pathname) {
criteriaType = 'commercialProperty'; // criteriaType = 'commercialProperty';
} else if ('/brokerListings' === window.location.pathname) { // } else if ('/brokerListings' === window.location.pathname) {
criteriaType = 'broker'; // criteriaType = 'broker';
} // }
sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this)); // sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this));
}; // };
return onChange(obj, function (path, value, previous, applyData) { // return onChange(obj, function (path, value, previous, applyData) {
// Call the original sessionStorageHandler // // Call the original sessionStorageHandler
sessionStorageHandler.call(this, path, value, previous, applyData); // sessionStorageHandler.call(this, path, value, previous, applyData);
// Notify about the criteria change using the component's context // // Notify about the criteria change using the component's context
component.criteriaChangeService.notifyCriteriaChange(); // component.criteriaChangeService.notifyCriteriaChange();
}); // });
} // }
ngAfterViewInit() {} ngAfterViewInit() {}
@@ -161,9 +160,12 @@ export class HeaderComponent {
collapse.collapse(); collapse.collapse();
} }
} }
closeMenus() { closeMenusAndSetCriteria(path: string) {
this.closeDropdown(); this.closeDropdown();
this.closeMobileMenu(); this.closeMobileMenu();
const criteria = getCriteriaProxy(path, this);
criteria.page = 1;
criteria.start = 0;
} }
ngOnDestroy() { ngOnDestroy() {
@@ -171,11 +173,11 @@ export class HeaderComponent {
this.destroy$.complete(); this.destroy$.complete();
} }
getNumberOfFiltersSet() { getNumberOfFiltersSet() {
if (this.criteria?.criteriaType === 'broker') { if (this.criteria?.criteriaType === 'brokerListings') {
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']); 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']); 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']); return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
} else { } else {
return 0; return 0;

View File

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

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-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> <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> </div>
@if(criteria.criteriaType==='business'){ @if(criteria.criteriaType==='businessListings'){
<div class="grid grid-cols-1 md:grid-cols-2 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
@@ -39,7 +39,7 @@
(ngModelChange)="setCity($event)" (ngModelChange)="setCity($event)"
> >
@for (city of cities$ | async; track city.id) { @for (city of cities$ | async; track city.id) {
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option> <ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option>
} }
</ng-select> </ng-select>
</div> </div>
@@ -233,7 +233,7 @@
</div> </div>
</div> </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="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4"> <div class="space-y-4">
<div> <div>
@@ -341,7 +341,7 @@
</div> </div>
</div> </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="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-4"> <div class="space-y-4">
<div> <div>

View File

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

View File

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

View File

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

View File

@@ -101,8 +101,11 @@
<div class="bg-white shadow-md rounded-lg overflow-hidden"> <div class="bg-white shadow-md rounded-lg overflow-hidden">
<div class="p-6 relative"> <div class="p-6 relative">
<h1 class="text-3xl font-bold mb-4">{{ listing?.title }}</h1> <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()"> <button
<fa-icon [icon]="faTimes" size="2x"></fa-icon> (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> </button>
<div class="lg:hidden"> <div class="lg:hidden">
@if (listing && listing.imageOrder && listing.imageOrder.length > 0) { @if (listing && listing.imageOrder && listing.imageOrder.length > 0) {

View File

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

View File

@@ -138,7 +138,7 @@
@if(user){ @if(user){
<div class="bg-white shadow-md rounded-lg overflow-hidden"> <div class="bg-white shadow-md rounded-lg overflow-hidden">
<!-- Header --> <!-- 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"> <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" /> --> <!-- <img src="https://placehold.co/80x80" alt="Profile picture of Avery Brown smiling" class="w-20 h-20 rounded-full" /> -->
@if(user.hasProfile){ @if(user.hasProfile){
@@ -167,7 +167,12 @@
} }
<!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> --> <!-- <img src="https://placehold.co/45x60" class="w-11 h-14" /> -->
</div> </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> </div>
<!-- Description --> <!-- Description -->
@@ -194,7 +199,7 @@
</div> </div>
<div class="flex flex-col sm:flex-row sm:items-center"> <div class="flex flex-col sm:flex-row sm:items-center">
<span class="font-semibold w-40 p-2">Company Location</span> <span class="font-semibold w-40 p-2">Company Location</span>
<span class="p-2 flex-grow">{{ user.companyLocation }}</span> <span class="p-2 flex-grow">{{ user.companyLocation.city }} - {{ user.companyLocation.state }}</span>
</div> </div>
</div> </div>

View File

@@ -114,7 +114,7 @@
groupBy="type" groupBy="type"
> >
@for (city of cities$ | async; track city.id) { @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> </ng-select>
</div> </div>

View File

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

View File

@@ -53,10 +53,10 @@ export class BrokerListingsComponent {
private route: ActivatedRoute, private route: ActivatedRoute,
private searchService: SearchService, private searchService: SearchService,
) { ) {
this.criteria = getCriteriaStateObject('broker'); this.criteria = getCriteriaStateObject('brokerListings');
this.init(); this.init();
this.searchService.currentCriteria.subscribe(criteria => { this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'broker') { if (criteria && criteria.criteriaType === 'brokerListings') {
this.criteria = criteria as UserListingCriteria; this.criteria = criteria as UserListingCriteria;
this.search(); this.search();
} }
@@ -74,6 +74,7 @@ export class BrokerListingsComponent {
this.users = usersReponse.results; this.users = usersReponse.results;
this.totalRecords = usersReponse.totalCount; 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.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.markForCheck();
this.cdRef.detectChanges(); 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">Asking price: {{ listing.price | currency }}</p>
<p class="text-sm text-gray-600 mb-1">Sales revenue: {{ listing.salesRevenue | currency }}</p> <p class="text-sm text-gray-600 mb-1">Sales revenue: {{ listing.salesRevenue | currency }}</p>
<p class="text-sm text-gray-600 mb-1">Net profit: {{ listing.cashFlow | currency }}</p> <p class="text-sm text-gray-600 mb-1">Net profit: {{ listing.cashFlow | currency }}</p>
<p class="text-sm text-gray-600 mb-1">Location: {{ listing.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> <p class="text-sm text-gray-600 mb-1">Established: {{ listing.established }}</p>
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" alt="Company logo" class="absolute bottom-[70px] right-[30px] h-[35px] w-auto" /> <img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" alt="Company logo" class="absolute bottom-[70px] right-[30px] h-[35px] w-auto" />
<div class="flex-grow"></div> <div class="flex-grow"></div>

View File

@@ -49,29 +49,28 @@ export class BusinessListingsComponent {
private route: ActivatedRoute, private route: ActivatedRoute,
private searchService: SearchService, private searchService: SearchService,
) { ) {
this.criteria = getCriteriaStateObject('business'); this.criteria = getCriteriaStateObject('businessListings');
this.init(); this.init();
this.searchService.currentCriteria.subscribe(criteria => { this.searchService.currentCriteria.subscribe(criteria => {
if (criteria && criteria.criteriaType === 'business') { if (criteria && criteria.criteriaType === 'businessListings') {
this.criteria = criteria as BusinessListingCriteria; this.criteria = criteria as BusinessListingCriteria;
this.search(); this.search();
} }
}); });
} }
async ngOnInit() {} async ngOnInit() {
this.search();
}
async init() { async init() {
this.reset(); 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() { async search() {
//this.listings = await this.listingsService.getListingsByPrompt(this.criteria);
const listingReponse = await this.listingsService.getListings(this.criteria, 'business'); const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
this.listings = listingReponse.results; this.listings = listingReponse.results;
this.totalRecords = listingReponse.totalCount; 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.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.markForCheck();
this.cdRef.detectChanges(); 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="container mx-auto px-4 py-8">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Property Card 1 -->
@for (listing of listings; track listing.id) { @for (listing of listings; track listing.id) {
<div class="bg-white rounded-lg shadow-md overflow-hidden"> <div class="bg-white rounded-lg shadow-md overflow-hidden">
@if (listing.imageOrder?.length>0){ @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 { } @else {
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="w-full h-48 object-cover" /> <img src="assets/images/placeholder_properties.jpg" alt="Image" class="w-full h-48 object-cover" />
} }
<div class="p-4"> <div class="p-4">
<div class="flex items-center justify-between mb-2"> <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> <span class="text-gray-600 text-sm"><i [class]="selectOptions.getIconTypeOfCommercials(listing.type)" class="mr-1"></i> {{ selectOptions.getCommercialProperty(listing.type) }}</span>
</div> </div>
<h3 class="text-lg font-semibold mb-2">{{ listing.title }}</h3> <h3 class="text-lg font-semibold mb-2">{{ listing.title }}</h3>
<p class="text-gray-600 mb-2">{{ listing.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> <p class="text-xl font-bold mb-4">{{ listing.price | currency }}</p>
<button [routerLink]="['/details-commercial-property-listing', listing.id]" class="bg-green-500 text-white px-4 py-2 rounded-full w-full hover:bg-green-600 transition duration-300"> <button [routerLink]="['/details-commercial-property-listing', listing.id]" class="bg-green-500 text-white px-4 py-2 rounded-full w-full hover:bg-green-600 transition duration-300">
View Full Listing <i class="fas fa-arrow-right ml-1"></i> View Full Listing <i class="fas fa-arrow-right ml-1"></i>

View File

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

View File

@@ -71,13 +71,14 @@
<option *ngFor="let type of customerTypes" [value]="type">{{ type | titlecase }}</option> <option *ngFor="let type of customerTypes" [value]="type">{{ type | titlecase }}</option>
</select> </select>
</div> --> </div> -->
@if (!isAdmin()){ @if (isAdmin() && !id){
<app-validated-select label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
}@else{
<div> <div>
<label for="customerType" class="block text-sm font-medium text-gray-700">User Type</label> <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> <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> </div>
}@else{
<app-validated-select label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
} @if (isProfessional){ } @if (isProfessional){
<!-- <div> <!-- <div>
<label for="customerSubType" class="block text-sm font-medium text-gray-700">Professional Type</label> <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> <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" /> <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> --> </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 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>
<!-- <div> <!-- <div>

View File

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

View File

@@ -52,8 +52,10 @@
</div> </div>
</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-ng-select label="State" name="state" [(ngModel)]="listing.state" [items]="selectOptions?.states"></app-validated-ng-select> <!-- <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.city"></app-validated-input> <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>
<!-- <div class="flex mb-4 space-x-4"> <!-- <div class="flex mb-4 space-x-4">
@@ -83,8 +85,8 @@
</div> </div>
</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-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="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>
<!-- <div class="mb-4"> <!-- <div class="mb-4">
@@ -99,9 +101,9 @@
currencyMask currencyMask
/> />
</div> --> </div> -->
<div> <!-- <div>
<app-validated-price label="Cash Flow" name="cashFlow" [(ngModel)]="listing.cashFlow"></app-validated-price>
</div> </div> -->
<!-- <div class="flex mb-4 space-x-4"> <!-- <div class="flex mb-4 space-x-4">
<div class="w-1/2"> <div class="w-1/2">

View File

@@ -17,6 +17,7 @@ import { AutoCompleteCompleteEvent, ImageProperty, createDefaultBusinessListing,
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { MessageService } from '../../../components/message/message.service'; 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 { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component'; import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component';
import { ValidatedPriceComponent } from '../../../components/validated-price/validated-price.component'; import { ValidatedPriceComponent } from '../../../components/validated-price/validated-price.component';
@@ -45,6 +46,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
ValidatedNgSelectComponent, ValidatedNgSelectComponent,
ValidatedPriceComponent, ValidatedPriceComponent,
ValidatedTextareaComponent, ValidatedTextareaComponent,
ValidatedCityComponent,
], ],
providers: [], providers: [],
templateUrl: './edit-business-listing.component.html', templateUrl: './edit-business-listing.component.html',
@@ -137,7 +139,7 @@ export class EditBusinessListingComponent {
suggestions: string[] | undefined; suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) { 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); 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> <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> <ng-select [items]="typesOfCommercialProperty" bindLabel="name" bindValue="value" [(ngModel)]="listing.type" name="type"> </ng-select>
</div> --> </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-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>
<!-- <div class="flex mb-4 space-x-4"> <!-- <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" /> <input type="text" id="city" [(ngModel)]="listing.city" name="city" class="w-full p-2 border border-gray-300 rounded-md" />
</div> </div>
</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-ng-select label="State" name="state" [(ngModel)]="listing.state" [items]="selectOptions?.states"></app-validated-ng-select> <!-- <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.city"></app-validated-input> <app-validated-input label="City" name="city" [(ngModel)]="listing.location.city"></app-validated-input> -->
</div>
<!-- </div> -->
<!-- <div class="flex mb-4 space-x-4"> <!-- <div class="flex mb-4 space-x-4">
<div class="w-1/2"> <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" /> <input type="text" id="county" [(ngModel)]="listing.county" name="county" class="w-full p-2 border border-gray-300 rounded-md" />
</div> </div>
</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="Zip Code" name="zipCode" [(ngModel)]="listing.zipCode"></app-validated-input>
<app-validated-input label="County" name="county" [(ngModel)]="listing.county"></app-validated-input> <app-validated-input label="County" name="county" [(ngModel)]="listing.county"></app-validated-input>
</div> </div> -->
<!-- <div class="mb-4"> <!-- <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> <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>
</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> -->
</div> </div>
@if (mode!=='create'){ @if (mode!=='create'){

View File

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

View File

@@ -39,7 +39,7 @@ export class EmailUsComponent {
this.keycloakUser = map2User(token); this.keycloakUser = map2User(token);
if (this.keycloakUser) { if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email); 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() { ngOnDestroy() {

View File

@@ -17,7 +17,7 @@
<tr *ngFor="let listing of myListings" class="border-b"> <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.title }}</td>
<td class="py-2 px-4">{{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</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"> <td class="py-2 px-4">
@if(listing.listingsCategory==='business'){ @if(listing.listingsCategory==='business'){
<button class="bg-green-500 text-white p-2 rounded-full mr-2" [routerLink]="['/editBusinessListing', listing.id]"> <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"> <div *ngFor="let listing of myListings" class="bg-white shadow-md rounded-lg p-4 mb-4">
<h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2> <h2 class="text-xl font-semibold mb-2">{{ listing.title }}</h2>
<p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p> <p class="text-gray-600 mb-2">Category: {{ listing.listingsCategory === 'commercialProperty' ? 'Commercial Property' : 'Business' }}</p>
<p class="text-gray-600 mb-4">Located in: {{ listing.state }}</p> <p class="text-gray-600 mb-4">Located in: {{ listing.location.city }} - {{ listing.location.state }}</p>
<div class="flex justify-end"> <div class="flex justify-end">
<button class="bg-green-500 text-white p-2 rounded-full mr-2"> <button class="bg-green-500 text-white p-2 rounded-full mr-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">

View File

@@ -54,7 +54,7 @@ export class MyListingComponent {
} }
async confirm(listing: ListingType) { 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) { if (confirmed) {
// this.messageService.showMessage('Listing has been deleted'); // this.messageService.showMessage('Listing has been deleted');
this.deleteListing(listing); this.deleteListing(listing);

View File

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

View File

@@ -1,13 +1,15 @@
import { Injectable } from '@angular/core'; 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({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class LoadingService { export class LoadingService {
public loading$ = new BehaviorSubject<string[]>([]); private loading$ = new BehaviorSubject<string[]>([]);
private loadingTextSubject = new BehaviorSubject<string | null>(null); 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(); loadingText$: Observable<string | null> = this.loadingTextSubject.asObservable();
public isLoading$ = this.loading$.asObservable().pipe( public isLoading$ = this.loading$.asObservable().pipe(
@@ -17,13 +19,15 @@ export class LoadingService {
shareReplay(1), shareReplay(1),
); );
public startLoading(type: string, request?: string): void { public startLoading(type: string, url?: string): void {
if (!this.loading$.value.includes(type)) { if (this.shouldShowLoading(url)) {
this.loading$.next(this.loading$.value.concat(type)); if (!this.loading$.value.includes(type)) {
if (type === 'uploadImage' || request?.includes('uploadImage') || request?.includes('uploadPropertyPicture') || request?.includes('uploadProfile') || request?.includes('uploadCompanyLogo')) { this.loading$.next(this.loading$.value.concat(type));
this.loadingTextSubject.next("Please wait - we're processing your image..."); if (this.isImageUpload(type, url)) {
} else { this.loadingTextSubject.next("Please wait - we're processing your image...");
this.loadingTextSubject.next(null); } else {
this.loadingTextSubject.next(null);
}
} }
} }
} }
@@ -34,4 +38,13 @@ export class LoadingService {
this.loadingTextSubject.next(null); 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: '', city: '',
types: [], types: [],
prompt: '', prompt: '',
criteriaType: 'business', criteriaType: 'businessListings',
county: '',
minPrice: null, minPrice: null,
maxPrice: null, maxPrice: null,
minRevenue: null, minRevenue: null,
@@ -128,8 +127,7 @@ export function createEmptyCommercialPropertyListingCriteria(): CommercialProper
city: '', city: '',
types: [], types: [],
prompt: '', prompt: '',
criteriaType: 'commercialProperty', criteriaType: 'commercialPropertyListings',
county: '',
minPrice: null, minPrice: null,
maxPrice: null, maxPrice: null,
title: '', title: '',
@@ -146,7 +144,7 @@ export function createEmptyUserListingCriteria(): UserListingCriteria {
city: '', city: '',
types: [], types: [],
prompt: '', prompt: '',
criteriaType: 'broker', criteriaType: 'brokerListings',
firstname: '', firstname: '',
lastname: '', lastname: '',
companyName: '', 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; let initialState;
if (criteriaType === 'business') { if (criteriaType === 'businessListings') {
initialState = createEmptyBusinessListingCriteria(); initialState = createEmptyBusinessListingCriteria();
} else if (criteriaType === 'commercialProperty') { } else if (criteriaType === 'commercialPropertyListings') {
initialState = createEmptyCommercialPropertyListingCriteria(); initialState = createEmptyCommercialPropertyListingCriteria();
} else { } else {
initialState = createEmptyUserListingCriteria(); initialState = createEmptyUserListingCriteria();
} }
const storedState = sessionStorage.getItem(`${criteriaType}_criteria`); const storedState = sessionStorage.getItem(`${criteriaType}`);
return storedState ? JSON.parse(storedState) : initialState; return storedState ? JSON.parse(storedState) : initialState;
} }
@@ -244,6 +242,7 @@ export function getDialogWidth(dimensions): string {
} }
import { initFlowbite } from 'flowbite'; import { initFlowbite } from 'flowbite';
import onChange from 'on-change';
import { Subject, concatMap, delay, of } from 'rxjs'; import { Subject, concatMap, delay, of } from 'rxjs';
const flowbiteQueue = new Subject<() => void>(); const flowbiteQueue = new Subject<() => void>();
@@ -372,3 +371,41 @@ function arraysEqual(arr1: any[] | null | undefined, arr2: any[] | null | undefi
} }
return true; 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();
});
}