diff --git a/bizmatch-server/src/drizzle/import.ts b/bizmatch-server/src/drizzle/import.ts index 16279b6..71e7d93 100644 --- a/bizmatch-server/src/drizzle/import.ts +++ b/bizmatch-server/src/drizzle/import.ts @@ -2,19 +2,18 @@ import 'dotenv/config'; import { drizzle } from 'drizzle-orm/node-postgres'; import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs'; import fs from 'fs-extra'; -import OpenAI from 'openai'; import { join } from 'path'; -import pkg from 'pg'; +import { Pool } from 'pg'; import { rimraf } from 'rimraf'; import sharp from 'sharp'; import { BusinessListingService } from 'src/listings/business-listing.service'; import { CommercialPropertyService } from 'src/listings/commercial-property.service'; import { Geo } from 'src/models/server.model'; +import { UserService } from 'src/user/user.service'; import winston from 'winston'; import { User, UserData } from '../models/db.model'; -import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model'; +import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName } from '../models/main.model'; import { SelectOptionsService } from '../select-options/select-options.service'; -import { convertUserToDrizzleUser } from '../utils'; import * as schema from './schema'; interface PropertyImportListing { id: string; @@ -54,224 +53,226 @@ interface BusinessImportListing { internals: string; created: string; } -const typesOfBusiness: Array = [ - { 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: 'Real Estate', value: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' }, - { name: 'Uncategorized', value: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' }, - { name: 'Retail', value: '5', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' }, - { name: 'Oilfield SVE and MFG.', value: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' }, - { name: 'Service', value: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' }, - { name: 'Advertising', value: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' }, - { name: 'Agriculture', value: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' }, - { name: 'Franchise', value: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' }, - { name: 'Professional', value: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' }, - { name: 'Manufacturing', value: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' }, - { name: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' }, -]; -const { Pool } = pkg; +// const typesOfBusiness: Array = [ +// { 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: 'Real Estate', value: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' }, +// { name: 'Uncategorized', value: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' }, +// { name: 'Retail', value: '5', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' }, +// { name: 'Oilfield SVE and MFG.', value: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' }, +// { name: 'Service', value: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' }, +// { name: 'Advertising', value: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' }, +// { name: 'Agriculture', value: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' }, +// { name: 'Franchise', value: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' }, +// { name: 'Professional', value: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' }, +// { name: 'Manufacturing', value: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' }, +// { name: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' }, +// ]; +// const { Pool } = pkg; -const openai = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen -}); - -const connectionString = process.env.DATABASE_URL; -// const pool = new Pool({connectionString}) -const client = new Pool({ connectionString }); -const db = drizzle(client, { schema, logger: true }); -const logger = winston.createLogger({ - transports: [new winston.transports.Console()], -}); -const commService = new CommercialPropertyService(null, db); -const businessService = new BusinessListingService(null, db); -//Delete Content -await db.delete(schema.commercials); -await db.delete(schema.businesses); -await db.delete(schema.users); -let filePath = `./src/assets/geo.json`; -const rawData = readFileSync(filePath, 'utf8'); -const geos = JSON.parse(rawData) as Geo; - -const sso = new SelectOptionsService(); -//Broker -filePath = `./data/broker.json`; -let data: string = readFileSync(filePath, 'utf8'); -const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten -const generatedUserData = []; -console.log(usersData.length); -let i = 0, - male = 0, - female = 0; -const targetPathProfile = `./pictures/profile`; -deleteFilesOfDir(targetPathProfile); -const targetPathLogo = `./pictures/logo`; -deleteFilesOfDir(targetPathLogo); -const targetPathProperty = `./pictures/property`; -deleteFilesOfDir(targetPathProperty); -fs.ensureDirSync(`./pictures/logo`); -fs.ensureDirSync(`./pictures/profile`); -fs.ensureDirSync(`./pictures/property`); - -//User -for (let index = 0; index < usersData.length; index++) { - const userData = usersData[index]; - const user: User = createDefaultUser('', '', '', null); - user.licensedIn = []; - userData.licensedIn.forEach(l => { - console.log(l['value'], l['name']); - user.licensedIn.push({ registerNo: l['value'], state: l['name'] }); +// const openai = new OpenAI({ +// apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen +// }); +(async () => { + const connectionString = process.env.DATABASE_URL; + // const pool = new Pool({connectionString}) + const client = new Pool({ connectionString }); + const db = drizzle(client, { schema, logger: true }); + const logger = winston.createLogger({ + transports: [new winston.transports.Console()], }); - user.areasServed = []; - user.areasServed = userData.areasServed.map(l => { - return { county: l.split(',')[0].trim(), state: l.split(',')[1].trim() }; - }); - user.hasCompanyLogo = true; - user.hasProfile = true; - user.firstname = userData.firstname; - user.lastname = userData.lastname; - user.email = userData.email; - user.phoneNumber = userData.phoneNumber; - user.description = userData.description; - user.companyName = userData.companyName; - user.companyOverview = userData.companyOverview; - user.companyWebsite = userData.companyWebsite; - const [city, state] = userData.companyLocation.split('-').map(e => e.trim()); - user.companyLocation = {}; - user.companyLocation.name = city; - user.companyLocation.state = state; - const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city); - user.companyLocation.latitude = cityGeo.latitude; - user.companyLocation.longitude = cityGeo.longitude; - user.offeredServices = userData.offeredServices; - user.gender = userData.gender; - user.customerType = 'professional'; - user.customerSubType = 'broker'; - user.created = new Date(); - user.updated = new Date(); + const commService = new CommercialPropertyService(null, db); + const businessService = new BusinessListingService(null, db); + const userService = new UserService(null, db, null, null); + //Delete Content + await db.delete(schema.commercials); + await db.delete(schema.businesses); + await db.delete(schema.users); + let filePath = `./src/assets/geo.json`; + const rawData = readFileSync(filePath, 'utf8'); + const geos = JSON.parse(rawData) as Geo; - const u = await db - .insert(schema.users) - .values(convertUserToDrizzleUser(user)) - .returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname }); - generatedUserData.push(u[0]); - i++; - logger.info(`user_${index} inserted`); - if (u[0].gender === 'male') { - male++; - const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`); - await storeProfilePicture(data, emailToDirName(u[0].email)); - } else { - female++; - const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`); - await storeProfilePicture(data, emailToDirName(u[0].email)); + const sso = new SelectOptionsService(); + //Broker + filePath = `./data/broker.json`; + let data: string = readFileSync(filePath, 'utf8'); + const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten + const generatedUserData = []; + console.log(usersData.length); + let i = 0, + male = 0, + female = 0; + const targetPathProfile = `./pictures/profile`; + deleteFilesOfDir(targetPathProfile); + const targetPathLogo = `./pictures/logo`; + deleteFilesOfDir(targetPathLogo); + const targetPathProperty = `./pictures/property`; + deleteFilesOfDir(targetPathProperty); + fs.ensureDirSync(`./pictures/logo`); + fs.ensureDirSync(`./pictures/profile`); + fs.ensureDirSync(`./pictures/property`); + + //User + for (let index = 0; index < usersData.length; index++) { + const userData = usersData[index]; + const user: User = createDefaultUser('', '', '', null); + user.licensedIn = []; + userData.licensedIn.forEach(l => { + console.log(l['value'], l['name']); + user.licensedIn.push({ registerNo: l['value'], state: l['name'] }); + }); + user.areasServed = []; + user.areasServed = userData.areasServed.map(l => { + return { county: l.split(',')[0].trim(), state: l.split(',')[1].trim() }; + }); + user.hasCompanyLogo = true; + user.hasProfile = true; + user.firstname = userData.firstname; + user.lastname = userData.lastname; + user.email = userData.email; + user.phoneNumber = userData.phoneNumber; + user.description = userData.description; + user.companyName = userData.companyName; + user.companyOverview = userData.companyOverview; + user.companyWebsite = userData.companyWebsite; + const [city, state] = userData.companyLocation.split('-').map(e => e.trim()); + user.location = {}; + user.location.name = city; + user.location.state = state; + const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city); + user.location.latitude = cityGeo.latitude; + user.location.longitude = cityGeo.longitude; + user.offeredServices = userData.offeredServices; + user.gender = userData.gender; + user.customerType = 'professional'; + user.customerSubType = 'broker'; + user.created = new Date(); + user.updated = new Date(); + + // const u = await db + // .insert(schema.users) + // .values(convertUserToDrizzleUser(user)) + // .returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname }); + const u = await userService.saveUser(user); + generatedUserData.push(u); + i++; + logger.info(`user_${index} inserted`); + if (u.gender === 'male') { + male++; + const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`); + await storeProfilePicture(data, emailToDirName(u.email)); + } else { + female++; + const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`); + await storeProfilePicture(data, emailToDirName(u.email)); + } + const data = readFileSync(`./pictures_base/logo/${i}.jpg`); + await storeCompanyLogo(data, emailToDirName(u.email)); } - const data = readFileSync(`./pictures_base/logo/${i}.jpg`); - await storeCompanyLogo(data, emailToDirName(u[0].email)); -} -//Corporate Listings -filePath = `./data/commercials.json`; -data = readFileSync(filePath, 'utf8'); -const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten -for (let index = 0; index < commercialJsonData.length; index++) { - const user = getRandomItem(generatedUserData); - const commercial = createDefaultCommercialPropertyListing(); - const id = commercialJsonData[index].id; - delete commercial.id; + //Corporate Listings + filePath = `./data/commercials.json`; + data = readFileSync(filePath, 'utf8'); + const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten + for (let index = 0; index < commercialJsonData.length; index++) { + 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.name = 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.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.name = commercialJsonData[index].city; + commercial.location.state = commercialJsonData[index].state; + // console.log(JSON.stringify(commercial.location)); + } catch (e) { + console.log(`----------------> ERROR ${commercialJsonData[index].state} - ${commercialJsonData[index].city}`); + continue; + } + commercial.price = commercialJsonData[index].price; + commercial.listingsCategory = 'commercialProperty'; + commercial.draft = false; + commercial.imageOrder = getFilenames(id); + commercial.imagePath = emailToDirName(user.email); + const insertionDate = getRandomDateWithinLastYear(); + commercial.created = insertionDate; + commercial.updated = insertionDate; + + const result = await commService.createListing(commercial); //await db.insert(schema.commercials).values(commercial).returning(); + try { + fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result.imagePath}/${result.serialId}`); + } catch (err) { + console.log(`----- No pictures available for ${id} ------ ${err}`); + } } - commercial.price = commercialJsonData[index].price; - commercial.listingsCategory = 'commercialProperty'; - commercial.draft = false; - commercial.imageOrder = getFilenames(id); - commercial.imagePath = emailToDirName(user.email); - const insertionDate = getRandomDateWithinLastYear(); - commercial.created = insertionDate; - commercial.updated = insertionDate; - const result = await commService.createListing(commercial); //await db.insert(schema.commercials).values(commercial).returning(); - try { - fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result.imagePath}/${result.serialId}`); - } catch (err) { - console.log(`----- No pictures available for ${id} ------ ${err}`); + //Business Listings + filePath = `./data/businesses.json`; + data = readFileSync(filePath, 'utf8'); + const businessJsonData = JSON.parse(data) as BusinessImportListing[]; // Erwartet ein Array von Objekten + for (let index = 0; index < businessJsonData.length; index++) { + const business = createDefaultBusinessListing(); //businessJsonData[index]; + delete business.id; + const user = getRandomItem(generatedUserData); + business.email = user.email; + business.type = sso.typesOfBusiness.find(e => e.oldValue === String(businessJsonData[index].type)).value; + business.title = businessJsonData[index].title; + business.description = businessJsonData[index].description; + try { + const cityGeo = geos.states.find(s => s.state_code === businessJsonData[index].state).cities.find(c => c.name === businessJsonData[index].city); + business.location = {}; + business.location.latitude = cityGeo.latitude; + business.location.longitude = cityGeo.longitude; + business.location.name = businessJsonData[index].city; + business.location.state = businessJsonData[index].state; + } catch (e) { + console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`); + continue; + } + business.price = businessJsonData[index].price; + business.title = businessJsonData[index].title; + business.draft = businessJsonData[index].draft; + business.listingsCategory = 'business'; + business.realEstateIncluded = businessJsonData[index].realEstateIncluded; + business.leasedLocation = businessJsonData[index].leasedLocation; + business.franchiseResale = businessJsonData[index].franchiseResale; + + business.salesRevenue = businessJsonData[index].salesRevenue; + business.cashFlow = businessJsonData[index].cashFlow; + business.supportAndTraining = businessJsonData[index].supportAndTraining; + business.employees = businessJsonData[index].employees; + business.established = businessJsonData[index].established; + business.internalListingNumber = businessJsonData[index].internalListingNumber; + business.reasonForSale = businessJsonData[index].reasonForSale; + business.brokerLicencing = businessJsonData[index].brokerLicencing; + business.internals = businessJsonData[index].internals; + business.imageName = emailToDirName(user.email); + business.created = new Date(businessJsonData[index].created); + business.updated = new Date(businessJsonData[index].created); + + await businessService.createListing(business); //db.insert(schema.businesses).values(business); } -} -//Business Listings -filePath = `./data/businesses.json`; -data = readFileSync(filePath, 'utf8'); -const businessJsonData = JSON.parse(data) as BusinessImportListing[]; // Erwartet ein Array von Objekten -for (let index = 0; index < businessJsonData.length; index++) { - const business = createDefaultBusinessListing(); //businessJsonData[index]; - delete business.id; - const user = getRandomItem(generatedUserData); - business.email = user.email; - business.type = sso.typesOfBusiness.find(e => e.oldValue === String(businessJsonData[index].type)).value; - business.title = businessJsonData[index].title; - business.description = businessJsonData[index].description; - try { - const cityGeo = geos.states.find(s => s.state_code === businessJsonData[index].state).cities.find(c => c.name === businessJsonData[index].city); - business.location = {}; - business.location.latitude = cityGeo.latitude; - business.location.longitude = cityGeo.longitude; - business.location.name = businessJsonData[index].city; - business.location.state = businessJsonData[index].state; - } catch (e) { - console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`); - continue; - } - business.price = businessJsonData[index].price; - business.title = businessJsonData[index].title; - business.draft = businessJsonData[index].draft; - business.listingsCategory = 'business'; - business.realEstateIncluded = businessJsonData[index].realEstateIncluded; - business.leasedLocation = businessJsonData[index].leasedLocation; - business.franchiseResale = businessJsonData[index].franchiseResale; - - business.salesRevenue = businessJsonData[index].salesRevenue; - business.cashFlow = businessJsonData[index].cashFlow; - business.supportAndTraining = businessJsonData[index].supportAndTraining; - business.employees = businessJsonData[index].employees; - business.established = businessJsonData[index].established; - business.internalListingNumber = businessJsonData[index].internalListingNumber; - business.reasonForSale = businessJsonData[index].reasonForSale; - business.brokerLicencing = businessJsonData[index].brokerLicencing; - business.internals = businessJsonData[index].internals; - business.imageName = emailToDirName(user.email); - business.created = new Date(businessJsonData[index].created); - business.updated = new Date(businessJsonData[index].created); - - await businessService.createListing(business); //db.insert(schema.businesses).values(business); -} - -//End -await client.end(); - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} -async function createEmbedding(text: string): Promise { - const response = await openai.embeddings.create({ - model: 'text-embedding-3-small', - input: text, - }); - return response.data[0].embedding; -} + //End + await client.end(); +})(); +// function sleep(ms) { +// return new Promise(resolve => setTimeout(resolve, ms)); +// } +// async function createEmbedding(text: string): Promise { +// const response = await openai.embeddings.create({ +// model: 'text-embedding-3-small', +// input: text, +// }); +// return response.data[0].embedding; +// } function getRandomItem(arr: T[]): T { if (arr.length === 0) { @@ -283,7 +284,7 @@ function getRandomItem(arr: T[]): T { } function getFilenames(id: string): string[] { try { - let filePath = `./pictures_base/property/${id}`; + const filePath = `./pictures_base/property/${id}`; return readdirSync(filePath); } catch (e) { return []; @@ -300,7 +301,7 @@ function getRandomDateWithinLastYear(): Date { return randomDate; } async function storeProfilePicture(buffer: Buffer, userId: string) { - let quality = 50; + const quality = 50; const output = await sharp(buffer) .resize({ width: 300 }) .avif({ quality }) // Verwende AVIF @@ -310,7 +311,7 @@ async function storeProfilePicture(buffer: Buffer, userId: string) { } async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) { - let quality = 50; + const quality = 50; const output = await sharp(buffer) .resize({ width: 300 }) .avif({ quality }) // Verwende AVIF diff --git a/bizmatch-server/src/drizzle/schema.ts b/bizmatch-server/src/drizzle/schema.ts index b90185a..d5b63ff 100644 --- a/bizmatch-server/src/drizzle/schema.ts +++ b/bizmatch-server/src/drizzle/schema.ts @@ -1,4 +1,5 @@ -import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; +import { boolean, doublePrecision, index, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'; import { AreasServed, LicensedIn } from '../models/db.model'; export const PG_CONNECTION = 'PG_CONNECTION'; export const genderEnum = pgEnum('gender', ['male', 'female']); @@ -7,89 +8,142 @@ export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', ' export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']); export const subscriptionTypeEnum = pgEnum('subscriptionType', ['free', 'professional', 'broker']); -export const users = pgTable('users', { +export const users = pgTable( + 'users', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + firstname: varchar('firstname', { length: 255 }).notNull(), + lastname: varchar('lastname', { length: 255 }).notNull(), + email: varchar('email', { length: 255 }).notNull().unique(), + phoneNumber: varchar('phoneNumber', { length: 255 }), + description: text('description'), + companyName: varchar('companyName', { length: 255 }), + companyOverview: text('companyOverview'), + companyWebsite: varchar('companyWebsite', { length: 255 }), + offeredServices: text('offeredServices'), + areasServed: jsonb('areasServed').$type(), + hasProfile: boolean('hasProfile'), + hasCompanyLogo: boolean('hasCompanyLogo'), + licensedIn: jsonb('licensedIn').$type(), + gender: genderEnum('gender'), + customerType: customerTypeEnum('customerType'), + customerSubType: customerSubTypeEnum('customerSubType'), + created: timestamp('created'), + updated: timestamp('updated'), + subscriptionId: text('subscriptionId'), + subscriptionPlan: subscriptionTypeEnum('subscriptionPlan'), + location: jsonb('location'), + // city: varchar('city', { length: 255 }), + // state: char('state', { length: 2 }), + // latitude: doublePrecision('latitude'), + // longitude: doublePrecision('longitude'), + }, + table => ({ + locationUserCityStateIdx: index('idx_user_location_city_state').on( + sql`((${table.location}->>'name')::varchar), ((${table.location}->>'state')::varchar), ((${table.location}->>'latitude')::float), ((${table.location}->>'longitude')::float)`, + ), + }), +); +export const businesses = pgTable( + 'businesses', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + email: varchar('email', { length: 255 }).references(() => users.email), + type: varchar('type', { length: 255 }), + title: varchar('title', { length: 255 }), + description: text('description'), + price: doublePrecision('price'), + favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(), + draft: boolean('draft'), + listingsCategory: listingsCategoryEnum('listingsCategory'), //varchar('listingsCategory', { length: 255 }), + realEstateIncluded: boolean('realEstateIncluded'), + leasedLocation: boolean('leasedLocation'), + franchiseResale: boolean('franchiseResale'), + salesRevenue: doublePrecision('salesRevenue'), + cashFlow: doublePrecision('cashFlow'), + supportAndTraining: text('supportAndTraining'), + employees: integer('employees'), + established: integer('established'), + internalListingNumber: integer('internalListingNumber'), + reasonForSale: varchar('reasonForSale', { length: 255 }), + brokerLicencing: varchar('brokerLicencing', { length: 255 }), + internals: text('internals'), + imageName: varchar('imageName', { length: 200 }), + created: timestamp('created'), + updated: timestamp('updated'), + location: jsonb('location'), + // city: varchar('city', { length: 255 }), + // state: char('state', { length: 2 }), + // zipCode: integer('zipCode'), + // county: varchar('county', { length: 255 }), + // street: varchar('street', { length: 255 }), + // housenumber: varchar('housenumber', { length: 10 }), + // latitude: doublePrecision('latitude'), + // longitude: doublePrecision('longitude'), + }, + table => ({ + locationBusinessCityStateIdx: index('idx_business_location_city_state').on( + sql`((${table.location}->>'name')::varchar), ((${table.location}->>'state')::varchar), ((${table.location}->>'latitude')::float), ((${table.location}->>'longitude')::float)`, + ), + }), +); +export const commercials = pgTable( + 'commercials', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + serialId: serial('serialId'), + email: varchar('email', { length: 255 }).references(() => users.email), + type: varchar('type', { length: 255 }), + title: varchar('title', { length: 255 }), + description: text('description'), + price: doublePrecision('price'), + favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(), + listingsCategory: listingsCategoryEnum('listingsCategory'), //listingsCategory: varchar('listingsCategory', { length: 255 }), + draft: boolean('draft'), + imageOrder: varchar('imageOrder', { length: 200 }).array(), + imagePath: varchar('imagePath', { length: 200 }), + created: timestamp('created'), + updated: timestamp('updated'), + location: jsonb('location'), + // city: varchar('city', { length: 255 }), + // state: char('state', { length: 2 }), + // zipCode: integer('zipCode'), + // county: varchar('county', { length: 255 }), + // street: varchar('street', { length: 255 }), + // housenumber: varchar('housenumber', { length: 10 }), + // latitude: doublePrecision('latitude'), + // longitude: doublePrecision('longitude'), + }, + table => ({ + locationCommercialsCityStateIdx: index('idx_commercials_location_city_state').on( + sql`((${table.location}->>'name')::varchar), ((${table.location}->>'state')::varchar), ((${table.location}->>'latitude')::float), ((${table.location}->>'longitude')::float)`, + ), + }), +); +// export const geo = pgTable('geo', { +// id: uuid('id').primaryKey().defaultRandom().notNull(), +// country: varchar('country', { length: 255 }).default('us'), +// state: char('state', { length: 2 }), +// city: varchar('city', { length: 255 }), +// zipCode: integer('zipCode'), +// county: varchar('county', { length: 255 }), +// street: varchar('street', { length: 255 }), +// housenumber: varchar('housenumber', { length: 10 }), +// latitude: doublePrecision('latitude'), +// longitude: doublePrecision('longitude'), +// }); +export const listingEvents = pgTable('listing_events', { id: uuid('id').primaryKey().defaultRandom().notNull(), - firstname: varchar('firstname', { length: 255 }).notNull(), - lastname: varchar('lastname', { length: 255 }).notNull(), - email: varchar('email', { length: 255 }).notNull().unique(), - phoneNumber: varchar('phoneNumber', { length: 255 }), - description: text('description'), - companyName: varchar('companyName', { length: 255 }), - companyOverview: text('companyOverview'), - companyWebsite: varchar('companyWebsite', { length: 255 }), - city: varchar('city', { length: 255 }), - state: char('state', { length: 2 }), - offeredServices: text('offeredServices'), - areasServed: jsonb('areasServed').$type(), - hasProfile: boolean('hasProfile'), - hasCompanyLogo: boolean('hasCompanyLogo'), - licensedIn: jsonb('licensedIn').$type(), - gender: genderEnum('gender'), - customerType: customerTypeEnum('customerType'), - customerSubType: customerSubTypeEnum('customerSubType'), - created: timestamp('created'), - updated: timestamp('updated'), - latitude: doublePrecision('latitude'), - longitude: doublePrecision('longitude'), - subscriptionId: text('subscriptionId'), - subscriptionPlan: subscriptionTypeEnum('subscriptionPlan'), - // embedding: vector('embedding', { dimensions: 1536 }), -}); - -export const businesses = pgTable('businesses', { - id: uuid('id').primaryKey().defaultRandom().notNull(), - email: varchar('email', { length: 255 }).references(() => users.email), - type: varchar('type', { length: 255 }), - title: varchar('title', { length: 255 }), - description: text('description'), - city: varchar('city', { length: 255 }), - state: char('state', { length: 2 }), - // zipCode: integer('zipCode'), - // county: varchar('county', { length: 255 }), - price: doublePrecision('price'), - favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(), - draft: boolean('draft'), - listingsCategory: listingsCategoryEnum('listingsCategory'), //varchar('listingsCategory', { length: 255 }), - realEstateIncluded: boolean('realEstateIncluded'), - leasedLocation: boolean('leasedLocation'), - franchiseResale: boolean('franchiseResale'), - salesRevenue: doublePrecision('salesRevenue'), - cashFlow: doublePrecision('cashFlow'), - supportAndTraining: text('supportAndTraining'), - employees: integer('employees'), - established: integer('established'), - internalListingNumber: integer('internalListingNumber'), - reasonForSale: varchar('reasonForSale', { length: 255 }), - brokerLicencing: varchar('brokerLicencing', { length: 255 }), - internals: text('internals'), - imageName: varchar('imageName', { length: 200 }), - created: timestamp('created'), - updated: timestamp('updated'), - latitude: doublePrecision('latitude'), - longitude: doublePrecision('longitude'), - // embedding: vector('embedding', { dimensions: 1536 }), -}); - -export const commercials = pgTable('commercials', { - id: uuid('id').primaryKey().defaultRandom().notNull(), - serialId: serial('serialId'), - email: varchar('email', { length: 255 }).references(() => users.email), - type: varchar('type', { length: 255 }), - title: varchar('title', { length: 255 }), - description: text('description'), - city: varchar('city', { length: 255 }), - state: char('state', { length: 2 }), - price: doublePrecision('price'), - favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(), - listingsCategory: listingsCategoryEnum('listingsCategory'), //listingsCategory: varchar('listingsCategory', { length: 255 }), - draft: boolean('draft'), - // zipCode: integer('zipCode'), - // county: varchar('county', { length: 255 }), - imageOrder: varchar('imageOrder', { length: 200 }).array(), - imagePath: varchar('imagePath', { length: 200 }), - created: timestamp('created'), - updated: timestamp('updated'), - latitude: doublePrecision('latitude'), - longitude: doublePrecision('longitude'), - // embedding: vector('embedding', { dimensions: 1536 }), + listingId: uuid('listing_id').notNull(), // Assuming listings are referenced by UUID, adjust as necessary + userId: uuid('user_id'), // Nullable, if user is logged in, otherwise null + eventType: varchar('event_type', { length: 50 }).notNull(), // 'view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact' + eventTimestamp: timestamp('event_timestamp').defaultNow().notNull(), + userIp: varchar('user_ip', { length: 45 }), // Optional if you choose to track IP in frontend or backend + userAgent: varchar('user_agent', { length: 255 }), // Store User-Agent as string + locationCountry: varchar('location_country', { length: 100 }), // Country from IP + locationCity: varchar('location_city', { length: 100 }), // City from IP + locationLat: varchar('location_lat', { length: 20 }), // Latitude from IP, stored as varchar + locationLng: varchar('location_lng', { length: 20 }), // Longitude from IP, stored as varchar + referrer: varchar('referrer', { length: 255 }), // Referrer URL if applicable + additionalData: jsonb('additional_data'), // JSON for any other optional data (like email, social shares etc.) }); diff --git a/bizmatch-server/src/listings/business-listing.service.ts b/bizmatch-server/src/listings/business-listing.service.ts index 7acf9ae..050b9f9 100644 --- a/bizmatch-server/src/listings/business-listing.service.ts +++ b/bizmatch-server/src/listings/business-listing.service.ts @@ -10,7 +10,7 @@ import { FileService } from '../file/file.service'; import { GeoService } from '../geo/geo.service'; import { BusinessListing, BusinessListingSchema } from '../models/db.model'; import { BusinessListingCriteria, JwtUser } from '../models/main.model'; -import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery, splitName } from '../utils'; +import { getDistanceQuery, splitName } from '../utils'; @Injectable() export class BusinessListingService { @@ -25,7 +25,8 @@ export class BusinessListingService { const whereConditions: SQL[] = []; if (criteria.city && criteria.searchType === 'exact') { - whereConditions.push(ilike(businesses.city, `%${criteria.city.name}%`)); + whereConditions.push(sql`${businesses.location}->>'name' ilike ${criteria.city.name}`); + //whereConditions.push(ilike(businesses.location-->'city', `%${criteria.city.name}%`)); } if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name); @@ -36,7 +37,7 @@ export class BusinessListingService { } if (criteria.state) { - whereConditions.push(eq(businesses.state, criteria.state)); + whereConditions.push(sql`${businesses.location}->>'state' = ${criteria.city.state}`); } if (criteria.minPrice) { @@ -165,7 +166,7 @@ export class BusinessListingService { const data = await query; const totalCount = await this.getBusinessListingsCount(criteria, user); - const results = data.map(r => r.business).map(r => convertDrizzleBusinessToBusiness(r)); + const results = data.map(r => r.business); return { results, totalCount, @@ -197,7 +198,7 @@ export class BusinessListingService { .from(businesses) .where(and(...conditions)); if (result.length > 0) { - return convertDrizzleBusinessToBusiness(result[0]) as BusinessListing; + return result[0] as BusinessListing; } else { throw new BadRequestException(`No entry available for ${id}`); } @@ -214,7 +215,7 @@ export class BusinessListingService { .from(businesses) .where(and(...conditions))) as BusinessListing[]; - return listings.map(l => convertDrizzleBusinessToBusiness(l)); + return listings; } // #### Find Favorites ######################################## async findFavoriteListings(user: JwtUser): Promise { @@ -222,7 +223,7 @@ export class BusinessListingService { .select() .from(businesses) .where(arrayContains(businesses.favoritesForUser, [user.username])); - return userFavorites.map(l => convertDrizzleBusinessToBusiness(l)); + return userFavorites; } // #### CREATE ######################################## async createListing(data: BusinessListing): Promise { @@ -230,10 +231,10 @@ export class BusinessListingService { data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.updated = new Date(); BusinessListingSchema.parse(data); - const convertedBusinessListing = convertBusinessToDrizzleBusiness(data); + const convertedBusinessListing = data; delete convertedBusinessListing.id; const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning(); - return convertDrizzleBusinessToBusiness(createdListing); + return createdListing; } catch (error) { if (error instanceof ZodError) { const filteredErrors = error.errors @@ -253,9 +254,9 @@ export class BusinessListingService { data.updated = new Date(); data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); BusinessListingSchema.parse(data); - const convertedBusinessListing = convertBusinessToDrizzleBusiness(data); + const convertedBusinessListing = data; const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning(); - return convertDrizzleBusinessToBusiness(updateListing); + return updateListing; } catch (error) { if (error instanceof ZodError) { const filteredErrors = error.errors @@ -285,11 +286,11 @@ export class BusinessListingService { // ############################################################## // States // ############################################################## - async getStates(): Promise { - return await this.conn - .select({ state: businesses.state, count: sql`count(${businesses.id})`.mapWith(Number) }) - .from(businesses) - .groupBy(sql`${businesses.state}`) - .orderBy(sql`count desc`); - } + // async getStates(): Promise { + // return await this.conn + // .select({ state: businesses.state, count: sql`count(${businesses.id})`.mapWith(Number) }) + // .from(businesses) + // .groupBy(sql`${businesses.state}`) + // .orderBy(sql`count desc`); + // } } diff --git a/bizmatch-server/src/listings/business-listings.controller.ts b/bizmatch-server/src/listings/business-listings.controller.ts index e7e17e5..3150060 100644 --- a/bizmatch-server/src/listings/business-listings.controller.ts +++ b/bizmatch-server/src/listings/business-listings.controller.ts @@ -55,10 +55,10 @@ export class BusinessListingsController { deleteById(@Param('id') id: string) { this.listingsService.deleteListing(id); } - @Get('states/all') - getStates(): any { - return this.listingsService.getStates(); - } + // @Get('states/all') + // getStates(): any { + // return this.listingsService.getStates(); + // } @UseGuards(JwtAuthGuard) @Delete('favorites/:id') deleteFavorite(@Request() req, @Param('id') id: string) { diff --git a/bizmatch-server/src/listings/commercial-property-listings.controller.ts b/bizmatch-server/src/listings/commercial-property-listings.controller.ts index b80009c..49707d6 100644 --- a/bizmatch-server/src/listings/commercial-property-listings.controller.ts +++ b/bizmatch-server/src/listings/commercial-property-listings.controller.ts @@ -41,10 +41,10 @@ export class CommercialPropertyListingsController { findTotal(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise { return this.listingsService.getCommercialPropertiesCount(criteria, req.user as JwtUser); } - @Get('states/all') - getStates(): any { - return this.listingsService.getStates(); - } + // @Get('states/all') + // getStates(): any { + // return this.listingsService.getStates(); + // } @Post() async create(@Body() listing: any) { this.logger.info(`Save Listing`); diff --git a/bizmatch-server/src/listings/commercial-property.service.ts b/bizmatch-server/src/listings/commercial-property.service.ts index 99701f3..30a7154 100644 --- a/bizmatch-server/src/listings/commercial-property.service.ts +++ b/bizmatch-server/src/listings/commercial-property.service.ts @@ -10,7 +10,7 @@ import { FileService } from '../file/file.service'; import { GeoService } from '../geo/geo.service'; import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model'; import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model'; -import { convertCommercialToDrizzleCommercial, convertDrizzleBusinessToBusiness, convertDrizzleCommercialToCommercial, getDistanceQuery } from '../utils'; +import { getDistanceQuery } from '../utils'; @Injectable() export class CommercialPropertyService { @@ -24,7 +24,7 @@ export class CommercialPropertyService { const whereConditions: SQL[] = []; if (criteria.city && criteria.searchType === 'exact') { - whereConditions.push(ilike(schema.commercials.city, `%${criteria.city.name}%`)); + whereConditions.push(sql`${commercials.location}->>'name' ilike ${criteria.city.name}`); } if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name); @@ -35,7 +35,7 @@ export class CommercialPropertyService { } if (criteria.state) { - whereConditions.push(eq(schema.commercials.state, criteria.state)); + whereConditions.push(sql`${schema.commercials.location}->>'state' = ${criteria.city.state}`); } if (criteria.minPrice) { @@ -89,7 +89,7 @@ export class CommercialPropertyService { query.limit(length).offset(start); const data = await query; - const results = data.map(r => r.commercial).map(r => convertDrizzleCommercialToCommercial(r)); + const results = data.map(r => r.commercial); const totalCount = await this.getCommercialPropertiesCount(criteria, user); return { @@ -122,7 +122,7 @@ export class CommercialPropertyService { .from(commercials) .where(and(...conditions)); if (result.length > 0) { - return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing; + return result[0] as CommercialPropertyListing; } else { throw new BadRequestException(`No entry available for ${id}`); } @@ -139,7 +139,7 @@ export class CommercialPropertyService { .select() .from(commercials) .where(and(...conditions))) as CommercialPropertyListing[]; - return listings.map(l => convertDrizzleCommercialToCommercial(l)) as CommercialPropertyListing[]; + return listings as CommercialPropertyListing[]; } // #### Find Favorites ######################################## async findFavoriteListings(user: JwtUser): Promise { @@ -147,7 +147,7 @@ export class CommercialPropertyService { .select() .from(commercials) .where(arrayContains(commercials.favoritesForUser, [user.username])); - return userFavorites.map(l => convertDrizzleBusinessToBusiness(l)); + return userFavorites; } // #### Find by imagePath ######################################## async findByImagePath(imagePath: string, serial: string): Promise { @@ -155,7 +155,7 @@ export class CommercialPropertyService { .select() .from(commercials) .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`)); - return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing; + return result[0] as CommercialPropertyListing; } // #### CREATE ######################################## async createListing(data: CommercialPropertyListing): Promise { @@ -163,10 +163,10 @@ export class CommercialPropertyService { data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.updated = new Date(); CommercialPropertyListingSchema.parse(data); - const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data); + const convertedCommercialPropertyListing = data; delete convertedCommercialPropertyListing.id; const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning(); - return convertDrizzleCommercialToCommercial(createdListing); + return createdListing; } catch (error) { if (error instanceof ZodError) { const filteredErrors = error.errors @@ -192,9 +192,9 @@ export class CommercialPropertyService { this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`); data.imageOrder = imageOrder; } - const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data); + const convertedCommercialPropertyListing = data; const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning(); - return convertDrizzleCommercialToCommercial(updateListing); + return updateListing; } catch (error) { if (error instanceof ZodError) { const filteredErrors = error.errors @@ -240,11 +240,11 @@ export class CommercialPropertyService { // ############################################################## // States // ############################################################## - async getStates(): Promise { - return await this.conn - .select({ state: commercials.state, count: sql`count(${commercials.id})`.mapWith(Number) }) - .from(commercials) - .groupBy(sql`${commercials.state}`) - .orderBy(sql`count desc`); - } + // async getStates(): Promise { + // return await this.conn + // .select({ state: commercials.state, count: sql`count(${commercials.id})`.mapWith(Number) }) + // .from(commercials) + // .groupBy(sql`${commercials.state}`) + // .orderBy(sql`count desc`); + // } } diff --git a/bizmatch-server/src/models/db.model.ts b/bizmatch-server/src/models/db.model.ts index c0d857a..a25d513 100644 --- a/bizmatch-server/src/models/db.model.ts +++ b/bizmatch-server/src/models/db.model.ts @@ -144,6 +144,10 @@ export const GeoSchema = z.object({ message: 'Longitude muss zwischen -180 und 180 liegen', }, ), + county: z.string().optional().nullable(), + housenumber: z.string().optional().nullable(), + street: z.string().optional().nullable(), + zipCode: z.number().optional().nullable(), }); const phoneRegex = /^(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/; export const UserSchema = z @@ -157,7 +161,7 @@ export const UserSchema = z companyName: z.string().optional().nullable(), companyOverview: z.string().optional().nullable(), companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(), - companyLocation: GeoSchema.optional().nullable(), + location: GeoSchema.optional().nullable(), offeredServices: z.string().optional().nullable(), areasServed: z.array(AreasServedSchema).optional().nullable(), hasProfile: z.boolean().optional().nullable(), @@ -213,7 +217,7 @@ export const UserSchema = z }); } - if (!data.companyLocation) { + if (!data.location) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Company location is required for professional customers', diff --git a/bizmatch-server/src/models/main.model.ts b/bizmatch-server/src/models/main.model.ts index 0107376..602817e 100644 --- a/bizmatch-server/src/models/main.model.ts +++ b/bizmatch-server/src/models/main.model.ts @@ -233,6 +233,10 @@ export interface UploadParams { export interface GeoResult { id: number; name: string; + street?: string; + housenumber?: string; + county?: string; + zipCode?: number; state: string; latitude: number; longitude: number; @@ -318,7 +322,7 @@ export function createDefaultUser(email: string, firstname: string, lastname: st companyName: null, companyOverview: null, companyWebsite: null, - companyLocation: null, + location: null, offeredServices: null, areasServed: [], hasProfile: false, diff --git a/bizmatch-server/src/models/server.model.ts b/bizmatch-server/src/models/server.model.ts index 9fd90da..20e5b19 100644 --- a/bizmatch-server/src/models/server.model.ts +++ b/bizmatch-server/src/models/server.model.ts @@ -70,3 +70,34 @@ export interface CountyRequest { prefix: string; states: string[]; } +export interface Address { + house_number: string; + road: string; + quarter: string; + suburb: string; + city: string; + county: string; + state: string; + ISO3166_2_lvl4: string; + postcode: string; + country: string; + country_code: string; +} + +export interface Place { + place_id: number; + licence: string; + osm_type: string; + osm_id: number; + lat: string; + lon: string; + class: string; + type: string; + place_rank: number; + importance: number; + addresstype: string; + name: string; + display_name: string; + address: Address; + boundingbox: [string, string, string, string]; +} diff --git a/bizmatch-server/src/user/user.service.ts b/bizmatch-server/src/user/user.service.ts index 96aa607..dcb465f 100644 --- a/bizmatch-server/src/user/user.service.ts +++ b/bizmatch-server/src/user/user.service.ts @@ -9,7 +9,7 @@ import { FileService } from '../file/file.service'; import { GeoService } from '../geo/geo.service'; import { User, UserSchema } from '../models/db.model'; import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model'; -import { convertDrizzleUserToUser, convertUserToDrizzleUser, getDistanceQuery, splitName } from '../utils'; +import { DrizzleUser, getDistanceQuery, splitName } from '../utils'; type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number]; @Injectable() @@ -25,7 +25,7 @@ export class UserService { const whereConditions: SQL[] = []; whereConditions.push(eq(schema.users.customerType, 'professional')); if (criteria.city && criteria.searchType === 'exact') { - whereConditions.push(ilike(schema.users.city, `%${criteria.city.name}%`)); + whereConditions.push(sql`${schema.users.location}->>'name' ilike ${criteria.city.name}`); } if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name); @@ -80,7 +80,7 @@ export class UserService { query.limit(length).offset(start); const data = await query; - const results = data.map(r => convertDrizzleUserToUser(r)); + const results = data; const totalCount = await this.getUserListingsCount(criteria); return { @@ -108,12 +108,12 @@ export class UserService { if (users.length === 0) { const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname, null) }; const u = await this.saveUser(user, false); - return convertDrizzleUserToUser(u); + return u; } else { const user = users[0]; user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email)); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email)); - return convertDrizzleUserToUser(user); + return user; } } async getUserById(id: string) { @@ -125,7 +125,7 @@ export class UserService { const user = users[0]; user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email)); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email)); - return convertDrizzleUserToUser(user); + return user; } async saveUser(user: User, processValidation = true): Promise { try { @@ -139,13 +139,14 @@ export class UserService { if (processValidation) { validatedUser = UserSchema.parse(user); } - const drizzleUser = convertUserToDrizzleUser(validatedUser); + //const drizzleUser = convertUserToDrizzleUser(validatedUser); + const drizzleUser = validatedUser as DrizzleUser; if (user.id) { const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning(); - return convertDrizzleUserToUser(updateUser) as User; + return updateUser as User; } else { const [newUser] = await this.conn.insert(schema.users).values(drizzleUser).returning(); - return convertDrizzleUserToUser(newUser) as User; + return newUser as User; } } catch (error) { throw error; diff --git a/bizmatch-server/src/utils.ts b/bizmatch-server/src/utils.ts index 9b60efa..2b576dd 100644 --- a/bizmatch-server/src/utils.ts +++ b/bizmatch-server/src/utils.ts @@ -1,6 +1,5 @@ import { sql } from 'drizzle-orm'; import { businesses, commercials, users } from './drizzle/schema'; -import { BusinessListing, CommercialPropertyListing, User } from './models/db.model'; export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen export function convertStringToNullUndefined(value) { @@ -20,125 +19,140 @@ export function convertStringToNullUndefined(value) { export const getDistanceQuery = (schema: typeof businesses | typeof commercials | typeof users, lat: number, lon: number, unit: 'km' | 'miles' = 'miles') => { const radius = unit === 'km' ? EARTH_RADIUS_KM : EARTH_RADIUS_MILES; + // return sql` + // ${radius} * 2 * ASIN(SQRT( + // POWER(SIN((${lat} - ${schema.latitude}) * PI() / 180 / 2), 2) + + // COS(${lat} * PI() / 180) * COS(${schema.latitude} * PI() / 180) * + // POWER(SIN((${lon} - ${schema.longitude}) * PI() / 180 / 2), 2) + // )) + // `; return sql` ${radius} * 2 * ASIN(SQRT( - POWER(SIN((${lat} - ${schema.latitude}) * PI() / 180 / 2), 2) + - COS(${lat} * PI() / 180) * COS(${schema.latitude} * PI() / 180) * - POWER(SIN((${lon} - ${schema.longitude}) * PI() / 180 / 2), 2) + POWER(SIN((${lat} - (${schema.location}->>'latitude')::float) * PI() / 180 / 2), 2) + + COS(${lat} * PI() / 180) * COS((${schema.location}->>'latitude')::float * PI() / 180) * + POWER(SIN((${lon} - (${schema.location}->>'longitude')::float) * PI() / 180 / 2), 2) )) `; }; -type DrizzleUser = typeof users.$inferSelect; -type DrizzleBusinessListing = typeof businesses.$inferSelect; -type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect; -export function convertBusinessToDrizzleBusiness(businessListing: Partial): DrizzleBusinessListing { - const drizzleBusinessListing = flattenObject(businessListing); - drizzleBusinessListing.city = drizzleBusinessListing.name; - delete drizzleBusinessListing.name; - return drizzleBusinessListing; -} -export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial): BusinessListing { - const o = { - location: drizzleBusinessListing.city ? undefined : null, - location_name: drizzleBusinessListing.city ? drizzleBusinessListing.city : undefined, - location_state: drizzleBusinessListing.state ? drizzleBusinessListing.state : undefined, - location_latitude: drizzleBusinessListing.latitude ? drizzleBusinessListing.latitude : undefined, - location_longitude: drizzleBusinessListing.longitude ? drizzleBusinessListing.longitude : undefined, - ...drizzleBusinessListing, - }; - Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); - delete o.city; - delete o.state; - delete o.latitude; - delete o.longitude; - return unflattenObject(o); -} -export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial): DrizzleCommercialPropertyListing { - const drizzleCommercialPropertyListing = flattenObject(commercialPropertyListing); - drizzleCommercialPropertyListing.city = drizzleCommercialPropertyListing.name; - delete drizzleCommercialPropertyListing.name; - return drizzleCommercialPropertyListing; -} -export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial): CommercialPropertyListing { - const o = { - location: drizzleCommercialPropertyListing.city ? undefined : null, - location_name: drizzleCommercialPropertyListing.city ? drizzleCommercialPropertyListing.city : undefined, - location_state: drizzleCommercialPropertyListing.state ? drizzleCommercialPropertyListing.state : undefined, - location_latitude: drizzleCommercialPropertyListing.latitude ? drizzleCommercialPropertyListing.latitude : undefined, - location_longitude: drizzleCommercialPropertyListing.longitude ? drizzleCommercialPropertyListing.longitude : undefined, - ...drizzleCommercialPropertyListing, - }; - Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); - delete o.city; - delete o.state; - delete o.latitude; - delete o.longitude; - return unflattenObject(o); -} -export function convertUserToDrizzleUser(user: Partial): DrizzleUser { - const drizzleUser = flattenObject(user); - drizzleUser.city = drizzleUser.name; - delete drizzleUser.name; - return drizzleUser; -} +export type DrizzleUser = typeof users.$inferSelect; +export type DrizzleBusinessListing = typeof businesses.$inferSelect; +export type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect; +// export function convertBusinessToDrizzleBusiness(businessListing: Partial): DrizzleBusinessListing { +// const drizzleBusinessListing = flattenObject(businessListing); +// drizzleBusinessListing.city = drizzleBusinessListing.name; +// delete drizzleBusinessListing.name; +// return drizzleBusinessListing; +// } +// export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial): BusinessListing { +// const o = { +// location: drizzleBusinessListing.city ? undefined : null, +// location_name: drizzleBusinessListing.city ? drizzleBusinessListing.city : undefined, +// location_state: drizzleBusinessListing.state ? drizzleBusinessListing.state : undefined, +// location_latitude: drizzleBusinessListing.latitude ? drizzleBusinessListing.latitude : undefined, +// location_longitude: drizzleBusinessListing.longitude ? drizzleBusinessListing.longitude : undefined, +// ...drizzleBusinessListing, +// }; +// Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); +// delete o.city; +// delete o.state; +// delete o.latitude; +// delete o.longitude; +// return unflattenObject(o); +// } +// export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial): DrizzleCommercialPropertyListing { +// const drizzleCommercialPropertyListing = flattenObject(commercialPropertyListing); +// drizzleCommercialPropertyListing.city = drizzleCommercialPropertyListing.name; +// delete drizzleCommercialPropertyListing.name; +// return drizzleCommercialPropertyListing; +// } +// export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial): CommercialPropertyListing { +// const o = { +// location: drizzleCommercialPropertyListing.city ? undefined : null, +// location_name: drizzleCommercialPropertyListing.city ? drizzleCommercialPropertyListing.city : undefined, +// location_state: drizzleCommercialPropertyListing.state ? drizzleCommercialPropertyListing.state : undefined, +// location_street: drizzleCommercialPropertyListing.street ? drizzleCommercialPropertyListing.street : undefined, +// location_housenumber: drizzleCommercialPropertyListing.housenumber ? drizzleCommercialPropertyListing.housenumber : undefined, +// location_county: drizzleCommercialPropertyListing.county ? drizzleCommercialPropertyListing.county : undefined, +// location_zipCode: drizzleCommercialPropertyListing.zipCode ? drizzleCommercialPropertyListing.zipCode : undefined, +// location_latitude: drizzleCommercialPropertyListing.latitude ? drizzleCommercialPropertyListing.latitude : undefined, +// location_longitude: drizzleCommercialPropertyListing.longitude ? drizzleCommercialPropertyListing.longitude : undefined, +// ...drizzleCommercialPropertyListing, +// }; +// Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); +// delete o.city; +// delete o.state; +// delete o.street; +// delete o.housenumber; +// delete o.county; +// delete o.zipCode; +// delete o.latitude; +// delete o.longitude; +// return unflattenObject(o); +// } +// export function convertUserToDrizzleUser(user: Partial): DrizzleUser { +// const drizzleUser = flattenObject(user); +// drizzleUser.city = drizzleUser.name; +// delete drizzleUser.name; +// return drizzleUser; +// } -export function convertDrizzleUserToUser(drizzleUser: Partial): User { - const o: any = { - companyLocation: drizzleUser.city ? undefined : null, - companyLocation_name: drizzleUser.city ? drizzleUser.city : undefined, - companyLocation_state: drizzleUser.state ? drizzleUser.state : undefined, - companyLocation_latitude: drizzleUser.latitude ? drizzleUser.latitude : undefined, - companyLocation_longitude: drizzleUser.longitude ? drizzleUser.longitude : undefined, - ...drizzleUser, - }; - Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); - delete o.city; - delete o.state; - delete o.latitude; - delete o.longitude; +// export function convertDrizzleUserToUser(drizzleUser: Partial): User { +// const o: any = { +// companyLocation: drizzleUser.city ? undefined : null, +// companyLocation_name: drizzleUser.city ? drizzleUser.city : undefined, +// companyLocation_state: drizzleUser.state ? drizzleUser.state : undefined, +// companyLocation_latitude: drizzleUser.latitude ? drizzleUser.latitude : undefined, +// companyLocation_longitude: drizzleUser.longitude ? drizzleUser.longitude : undefined, +// ...drizzleUser, +// }; +// Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); +// 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]; +// 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 = {}; +// 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); - } - } +// 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; -} +// return result; +// } export function splitName(fullName: string): { firstname: string; lastname: string } { const parts = fullName.trim().split(/\s+/); // Teile den Namen am Leerzeichen auf diff --git a/bizmatch-server/tsconfig.json b/bizmatch-server/tsconfig.json index af69ced..f522e41 100644 --- a/bizmatch-server/tsconfig.json +++ b/bizmatch-server/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ES2021", + "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "declaration": true, diff --git a/bizmatch/src/app/components/validated-location/validated-location.component.html b/bizmatch/src/app/components/validated-location/validated-location.component.html new file mode 100644 index 0000000..4d9e542 --- /dev/null +++ b/bizmatch/src/app/components/validated-location/validated-location.component.html @@ -0,0 +1,31 @@ +
+ + + @for (place of places$ | async; track place.place_id) { + {{ formatPlaceAddress(place) }} + } + +
diff --git a/bizmatch/src/app/components/validated-location/validated-location.component.scss b/bizmatch/src/app/components/validated-location/validated-location.component.scss new file mode 100644 index 0000000..b27bb07 --- /dev/null +++ b/bizmatch/src/app/components/validated-location/validated-location.component.scss @@ -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; + } +} diff --git a/bizmatch/src/app/components/validated-location/validated-location.component.ts b/bizmatch/src/app/components/validated-location/validated-location.component.ts new file mode 100644 index 0000000..fdde13a --- /dev/null +++ b/bizmatch/src/app/components/validated-location/validated-location.component.ts @@ -0,0 +1,157 @@ +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, debounceTime, distinctUntilChanged, Observable, of, Subject, switchMap, tap } from 'rxjs'; +import { GeoResult } from '../../../../../bizmatch-server/src/models/main.model'; +import { Place } 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-location', + standalone: true, + imports: [CommonModule, FormsModule, NgSelectModule, TooltipComponent], + templateUrl: './validated-location.component.html', + styleUrl: './validated-location.component.scss', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => ValidatedLocationComponent), + multi: true, + }, + ], +}) +export class ValidatedLocationComponent extends BaseInputComponent { + @Input() items; + @Input() labelClasses: string; + places$: Observable; + placeInput$ = new Subject(); + placeLoading = false; + constructor(validationMessagesService: ValidationMessagesService, private geoService: GeoService, public selectOptions: SelectOptionsService) { + super(validationMessagesService); + } + + override ngOnInit() { + super.ngOnInit(); + this.loadCities(); + } + onInputChange(event: Place): void { + this.value = event; //{ ...event, longitude: parseFloat(event.longitude), latitude: parseFloat(event.latitude) }; + this.value = { + id: event?.place_id, + name: event?.address.city, + county: event?.address.county, + street: event?.address.road, + housenumber: event?.address.house_number, + state: event?.address['ISO3166-2-lvl4'].substr(3), + latitude: event ? parseFloat(event?.lat) : undefined, + longitude: event ? parseFloat(event?.lon) : undefined, + }; + this.onChange(this.value); + } + private loadCities() { + this.places$ = concat( + of([]), // default items + this.placeInput$.pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => (this.placeLoading = true)), + switchMap(term => + this.geoService.findLocationStartingWith(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.placeLoading = false)), + ), + ), + ), + ); + } + trackByFn(item: GeoResult) { + return item.id; + } + compareFn = (item, selected) => { + return item.id === selected.id; + }; + formatGeoAddress(geoResult: GeoResult | null | undefined): string { + // Überprüfen, ob geoResult null oder undefined ist + if (!geoResult) { + return ''; + } + + let addressParts: string[] = []; + + // Füge Hausnummer hinzu, wenn vorhanden + if (geoResult.housenumber) { + addressParts.push(geoResult.housenumber); + } + + // Füge Straße hinzu, wenn vorhanden + if (geoResult.street) { + addressParts.push(geoResult.street); + } + + // Kombiniere Hausnummer und Straße + let address = addressParts.join(' '); + + // Füge Namen hinzu, wenn vorhanden + if (geoResult.name) { + address = address ? `${address}, ${geoResult.name}` : geoResult.name; + } + + // Füge County hinzu, wenn vorhanden + if (geoResult.county) { + address = address ? `${address}, ${geoResult.county}` : geoResult.county; + } + + // Füge Bundesland hinzu, wenn vorhanden + if (geoResult.state) { + address = address ? `${address} - ${geoResult.state}` : geoResult.state; + } + + return address; + } + formatPlaceAddress(place: Place | null | undefined): string { + // Überprüfen, ob place null oder undefined ist + if (!place) { + return ''; + } + + const { house_number, road, city, county, state } = place.address; + + let addressParts: string[] = []; + + // Füge Hausnummer hinzu, wenn vorhanden + if (house_number) { + addressParts.push(house_number); + } + + // Füge Straße hinzu, wenn vorhanden + if (road) { + addressParts.push(road); + } + + // Kombiniere Hausnummer und Straße + let address = addressParts.join(' '); + + // Füge Stadt hinzu, wenn vorhanden + if (city) { + address = address ? `${address}, ${city}` : city; + } + + // Füge County hinzu, wenn vorhanden + if (county) { + address = address ? `${address}, ${county}` : county; + } + + // Füge Bundesland hinzu, wenn vorhanden + if (state) { + address = address ? `${address} - ${state}` : state; + } + + return address; + } +} diff --git a/bizmatch/src/app/pages/details/details-user/details-user.component.html b/bizmatch/src/app/pages/details/details-user/details-user.component.html index 0d32251..9ccfb61 100644 --- a/bizmatch/src/app/pages/details/details-user/details-user.component.html +++ b/bizmatch/src/app/pages/details/details-user/details-user.component.html @@ -63,7 +63,7 @@
Company Location - {{ user.companyLocation?.name }} - {{ user.companyLocation?.state }} + {{ user.location?.name }} - {{ user.location?.state }}
Professional Type diff --git a/bizmatch/src/app/pages/subscription/account/account.component.html b/bizmatch/src/app/pages/subscription/account/account.component.html index 2f05ab4..5abe3e1 100644 --- a/bizmatch/src/app/pages/subscription/account/account.component.html +++ b/bizmatch/src/app/pages/subscription/account/account.component.html @@ -119,7 +119,8 @@ - + +
- + + diff --git a/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts b/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts index 7c901ab..68cb1cd 100644 --- a/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts +++ b/bizmatch/src/app/pages/subscription/edit-business-listing/edit-business-listing.component.ts @@ -19,6 +19,7 @@ import { environment } from '../../../../environments/environment'; import { MessageService } from '../../../components/message/message.service'; import { ValidatedCityComponent } from '../../../components/validated-city/validated-city.component'; import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component'; +import { ValidatedLocationComponent } from '../../../components/validated-location/validated-location.component'; import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component'; import { ValidatedPriceComponent } from '../../../components/validated-price/validated-price.component'; import { ValidatedQuillComponent } from '../../../components/validated-quill/validated-quill.component'; @@ -47,6 +48,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults'; ValidatedPriceComponent, ValidatedTextareaComponent, ValidatedCityComponent, + ValidatedLocationComponent, ], providers: [], templateUrl: './edit-business-listing.component.html', diff --git a/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.html b/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.html index 11b8bd8..4a3ae3c 100644 --- a/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.html +++ b/bizmatch/src/app/pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component.html @@ -38,7 +38,8 @@ -->
- + +