Compare commits
58 Commits
tailwind
...
b9a9b983e9
| Author | SHA1 | Date | |
|---|---|---|---|
| b9a9b983e9 | |||
| 1282d30b49 | |||
| 974a6503ef | |||
| 860d30b16f | |||
| 178f2b4810 | |||
| bb26972377 | |||
| 16b880384b | |||
| 3e84b82c92 | |||
| 205793faab | |||
| eaa8a5064f | |||
| c00c2caccc | |||
| 8595e70ceb | |||
| 8dd13d5472 | |||
| f36d9fb4d7 | |||
| eb5a334868 | |||
| 446d568378 | |||
| d4ec9d067f | |||
| 7c9a47cf4e | |||
| 40ba402c70 | |||
| d2f6b3ec3f | |||
| 77c9973256 | |||
| 68d2615f0f | |||
| 60866473f7 | |||
| 8a7e26d2b6 | |||
| fe759f953f | |||
| 83307684ee | |||
| 17213ba4b0 | |||
| 24ed50a48f | |||
| 06d83a478d | |||
| 9ecc0c2429 | |||
| 7807afbad3 | |||
| 624fa74eb6 | |||
| 3b012a8113 | |||
| d8429f9b4a | |||
| c5577969c8 | |||
| f4f576d4a9 | |||
| 630c31cfc9 | |||
| ede8b66d83 | |||
| 8721be4a90 | |||
| c1b72bbc12 | |||
| 0f301fb534 | |||
| 8157dcc376 | |||
| f66badbfb1 | |||
| 74d5f92aba | |||
| 7a286e3519 | |||
| b4609d07ba | |||
| 48bff89526 | |||
| 056db7b199 | |||
| 8c6c6e3dbd | |||
| 7f756a71e8 | |||
| a8bb163acf | |||
| 1f8febc479 | |||
| ec0576e7b8 | |||
| 3a6a64cce9 | |||
| 245e76f697 | |||
| d71a5c25c3 | |||
| 1e1d5cea57 | |||
| 6d1c50d5df |
4
bizmatch-server/.env.development
Normal file
4
bizmatch-server/.env.development
Normal file
@@ -0,0 +1,4 @@
|
||||
REALM=bizmatch-dev
|
||||
usersURL=/admin/realms/bizmatch-dev/users
|
||||
WEB_HOST=https://dev.bizmatch.net
|
||||
STRIPE_WEBHOOK_SECRET=whsec_w2yvJY8qFMfO5wJgyNHCn6oYT7o2J5pS
|
||||
2
bizmatch-server/.env.production
Normal file
2
bizmatch-server/.env.production
Normal file
@@ -0,0 +1,2 @@
|
||||
REALM=bizmatch
|
||||
WEB_HOST=https://www.bizmatch.net
|
||||
6
bizmatch-server/.gitignore
vendored
6
bizmatch-server/.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
/dist
|
||||
/node_modules
|
||||
/build
|
||||
/data
|
||||
|
||||
# Logs
|
||||
logs
|
||||
@@ -60,3 +61,8 @@ pictures_base
|
||||
|
||||
src/*.js
|
||||
bun.lockb
|
||||
|
||||
#drizzle migrations
|
||||
src/drizzle/migrations
|
||||
|
||||
importlog.txt
|
||||
28
bizmatch-server/.vscode/launch.json
vendored
28
bizmatch-server/.vscode/launch.json
vendored
@@ -14,7 +14,8 @@
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"HOST_NAME": "localhost"
|
||||
}
|
||||
},
|
||||
"preLaunchTask": "Start Stripe Listener"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
@@ -60,5 +61,30 @@
|
||||
"sourceMaps": true,
|
||||
"smartStep": true
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Start Stripe Listener",
|
||||
"type": "shell",
|
||||
"command": "stripe listen -e checkout.session.completed --forward-to http://localhost:3000/bizmatch/payment/webhook",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [
|
||||
{
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": ".",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": ".",
|
||||
"endsPattern": "."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
31
bizmatch-server/.vscode/tasks.json
vendored
Normal file
31
bizmatch-server/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Start Stripe Listener",
|
||||
"type": "shell",
|
||||
"command": "stripe listen -e checkout.session.completed --forward-to http://localhost:3000/bizmatch/payment/webhook",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true, // Task läuft im Hintergrund
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Start Nest.js",
|
||||
"type": "npm",
|
||||
"script": "start:debug",
|
||||
"isBackground": false,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
|
||||
/**
|
||||
* AUTO-GENERATED FILE - DO NOT EDIT!
|
||||
*
|
||||
* This file was automatically generated by pg-to-ts v.4.1.1
|
||||
* $ pg-to-ts generate -c postgresql://username:password@localhost:5432/bizmatch -t businesses -t commercials -t users -s public
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export type Json = unknown;
|
||||
export type customerSubType = 'appraiser' | 'attorney' | 'broker' | 'cpa' | 'surveyor' | 'titleCompany';
|
||||
export type customerType = 'buyer' | 'professional';
|
||||
export type gender = 'female' | 'male';
|
||||
export type listingsCategory = 'business' | 'commercialProperty';
|
||||
|
||||
// Table businesses
|
||||
export interface Businesses {
|
||||
id: string;
|
||||
email: string | null;
|
||||
type: string | null;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
zipCode: number | null;
|
||||
county: string | null;
|
||||
price: number | null;
|
||||
favoritesForUser: string[] | null;
|
||||
draft: boolean | null;
|
||||
listingsCategory: listingsCategory | null;
|
||||
realEstateIncluded: boolean | null;
|
||||
leasedLocation: boolean | null;
|
||||
franchiseResale: boolean | null;
|
||||
salesRevenue: number | null;
|
||||
cashFlow: number | null;
|
||||
supportAndTraining: string | null;
|
||||
employees: number | null;
|
||||
established: number | null;
|
||||
internalListingNumber: number | null;
|
||||
reasonForSale: string | null;
|
||||
brokerLicencing: string | null;
|
||||
internals: string | null;
|
||||
imageName: string | null;
|
||||
created: Date | null;
|
||||
updated: Date | null;
|
||||
visits: number | null;
|
||||
lastVisit: Date | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
}
|
||||
export interface BusinessesInput {
|
||||
id?: string;
|
||||
email?: string | null;
|
||||
type?: string | null;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
zipCode?: number | null;
|
||||
county?: string | null;
|
||||
price?: number | null;
|
||||
favoritesForUser?: string[] | null;
|
||||
draft?: boolean | null;
|
||||
listingsCategory?: listingsCategory | null;
|
||||
realEstateIncluded?: boolean | null;
|
||||
leasedLocation?: boolean | null;
|
||||
franchiseResale?: boolean | null;
|
||||
salesRevenue?: number | null;
|
||||
cashFlow?: number | null;
|
||||
supportAndTraining?: string | null;
|
||||
employees?: number | null;
|
||||
established?: number | null;
|
||||
internalListingNumber?: number | null;
|
||||
reasonForSale?: string | null;
|
||||
brokerLicencing?: string | null;
|
||||
internals?: string | null;
|
||||
imageName?: string | null;
|
||||
created?: Date | null;
|
||||
updated?: Date | null;
|
||||
visits?: number | null;
|
||||
lastVisit?: Date | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
}
|
||||
const businesses = {
|
||||
tableName: 'businesses',
|
||||
columns: ['id', 'email', 'type', 'title', 'description', 'city', 'state', 'zipCode', 'county', 'price', 'favoritesForUser', 'draft', 'listingsCategory', 'realEstateIncluded', 'leasedLocation', 'franchiseResale', 'salesRevenue', 'cashFlow', 'supportAndTraining', 'employees', 'established', 'internalListingNumber', 'reasonForSale', 'brokerLicencing', 'internals', 'imageName', 'created', 'updated', 'visits', 'lastVisit', 'latitude', 'longitude'],
|
||||
requiredForInsert: [],
|
||||
primaryKey: 'id',
|
||||
foreignKeys: { email: { table: 'users', column: 'email', $type: null as unknown as Users }, },
|
||||
$type: null as unknown as Businesses,
|
||||
$input: null as unknown as BusinessesInput
|
||||
} as const;
|
||||
|
||||
// Table commercials
|
||||
export interface Commercials {
|
||||
id: string;
|
||||
serialId: number;
|
||||
email: string | null;
|
||||
type: string | null;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
price: number | null;
|
||||
favoritesForUser: string[] | null;
|
||||
listingsCategory: listingsCategory | null;
|
||||
hideImage: boolean | null;
|
||||
draft: boolean | null;
|
||||
zipCode: number | null;
|
||||
county: string | null;
|
||||
imageOrder: string[] | null;
|
||||
imagePath: string | null;
|
||||
created: Date | null;
|
||||
updated: Date | null;
|
||||
visits: number | null;
|
||||
lastVisit: Date | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
}
|
||||
export interface CommercialsInput {
|
||||
id?: string;
|
||||
serialId?: number;
|
||||
email?: string | null;
|
||||
type?: string | null;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
price?: number | null;
|
||||
favoritesForUser?: string[] | null;
|
||||
listingsCategory?: listingsCategory | null;
|
||||
hideImage?: boolean | null;
|
||||
draft?: boolean | null;
|
||||
zipCode?: number | null;
|
||||
county?: string | null;
|
||||
imageOrder?: string[] | null;
|
||||
imagePath?: string | null;
|
||||
created?: Date | null;
|
||||
updated?: Date | null;
|
||||
visits?: number | null;
|
||||
lastVisit?: Date | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
}
|
||||
const commercials = {
|
||||
tableName: 'commercials',
|
||||
columns: ['id', 'serialId', 'email', 'type', 'title', 'description', 'city', 'state', 'price', 'favoritesForUser', 'listingsCategory', 'hideImage', 'draft', 'zipCode', 'county', 'imageOrder', 'imagePath', 'created', 'updated', 'visits', 'lastVisit', 'latitude', 'longitude'],
|
||||
requiredForInsert: [],
|
||||
primaryKey: 'id',
|
||||
foreignKeys: { email: { table: 'users', column: 'email', $type: null as unknown as Users }, },
|
||||
$type: null as unknown as Commercials,
|
||||
$input: null as unknown as CommercialsInput
|
||||
} as const;
|
||||
|
||||
// Table users
|
||||
export interface Users {
|
||||
id: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
phoneNumber: string | null;
|
||||
description: string | null;
|
||||
companyName: string | null;
|
||||
companyOverview: string | null;
|
||||
companyWebsite: string | null;
|
||||
companyLocation: string | null;
|
||||
offeredServices: string | null;
|
||||
areasServed: Json | null;
|
||||
hasProfile: boolean | null;
|
||||
hasCompanyLogo: boolean | null;
|
||||
licensedIn: Json | null;
|
||||
gender: gender | null;
|
||||
customerType: customerType | null;
|
||||
customerSubType: customerSubType | null;
|
||||
created: Date | null;
|
||||
updated: Date | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
}
|
||||
export interface UsersInput {
|
||||
id?: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
phoneNumber?: string | null;
|
||||
description?: string | null;
|
||||
companyName?: string | null;
|
||||
companyOverview?: string | null;
|
||||
companyWebsite?: string | null;
|
||||
companyLocation?: string | null;
|
||||
offeredServices?: string | null;
|
||||
areasServed?: Json | null;
|
||||
hasProfile?: boolean | null;
|
||||
hasCompanyLogo?: boolean | null;
|
||||
licensedIn?: Json | null;
|
||||
gender?: gender | null;
|
||||
customerType?: customerType | null;
|
||||
customerSubType?: customerSubType | null;
|
||||
created?: Date | null;
|
||||
updated?: Date | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
}
|
||||
const users = {
|
||||
tableName: 'users',
|
||||
columns: ['id', 'firstname', 'lastname', 'email', 'phoneNumber', 'description', 'companyName', 'companyOverview', 'companyWebsite', 'companyLocation', 'offeredServices', 'areasServed', 'hasProfile', 'hasCompanyLogo', 'licensedIn', 'gender', 'customerType', 'customerSubType', 'created', 'updated', 'latitude', 'longitude'],
|
||||
requiredForInsert: ['firstname', 'lastname', 'email'],
|
||||
primaryKey: 'id',
|
||||
foreignKeys: {},
|
||||
$type: null as unknown as Users,
|
||||
$input: null as unknown as UsersInput
|
||||
} as const;
|
||||
|
||||
|
||||
export interface TableTypes {
|
||||
businesses: {
|
||||
select: Businesses;
|
||||
input: BusinessesInput;
|
||||
};
|
||||
commercials: {
|
||||
select: Commercials;
|
||||
input: CommercialsInput;
|
||||
};
|
||||
users: {
|
||||
select: Users;
|
||||
input: UsersInput;
|
||||
};
|
||||
}
|
||||
|
||||
export const tables = {
|
||||
businesses,
|
||||
commercials,
|
||||
users,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,14 +5,14 @@
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "HOST_NAME=localhost nest start",
|
||||
"start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
|
||||
"start": "nest start",
|
||||
"start:local": "HOST_NAME=localhost node dist/src/main",
|
||||
"start:dev": "NODE_ENV=development node dist/src/main",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "HOST_NAME=www.bizmatch.net node dist/main",
|
||||
"start:prod": "NODE_ENV=production node dist/src/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
@@ -34,15 +34,22 @@
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"body-parser": "^1.20.2",
|
||||
"cls-hooked": "^4.2.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-flow": "^4.1.0",
|
||||
"drizzle-orm": "^0.32.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"groq-sdk": "^0.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwk-to-pem": "^2.0.6",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"ky": "^1.4.0",
|
||||
"nest-winston": "^1.9.4",
|
||||
"nestjs-cls": "^4.4.1",
|
||||
"nodemailer": "^6.9.10",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"openai": "^4.52.6",
|
||||
@@ -55,6 +62,7 @@
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.2",
|
||||
"stripe": "^16.8.0",
|
||||
"tsx": "^4.16.2",
|
||||
"urlcat": "^3.1.0",
|
||||
"winston": "^3.11.0",
|
||||
@@ -68,6 +76,8 @@
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/jsonwebtoken": "^9.0.6",
|
||||
"@types/jwk-to-pem": "^2.0.3",
|
||||
"@types/multer": "^1.4.11",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
@@ -114,4 +124,4 @@
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AiService } from './ai.service.js';
|
||||
import { AiService } from './ai.service';
|
||||
|
||||
@Controller('ai')
|
||||
export class AiController {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiController } from './ai.controller.js';
|
||||
import { AiService } from './ai.service.js';
|
||||
import { AiController } from './ai.controller';
|
||||
import { AiService } from './ai.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AiController],
|
||||
|
||||
@@ -3,30 +3,85 @@ import Groq from 'groq-sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { BusinessListingCriteria } from '../models/main.model';
|
||||
|
||||
const businessListingCriteriaStructure = {
|
||||
criteriaType: 'business | commercialProperty | broker',
|
||||
types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
|
||||
city: 'string',
|
||||
state: 'string',
|
||||
county: 'string',
|
||||
minPrice: 'number',
|
||||
maxPrice: 'number',
|
||||
minRevenue: 'number',
|
||||
maxRevenue: 'number',
|
||||
minCashFlow: 'number',
|
||||
maxCashFlow: 'number',
|
||||
minNumberEmployees: 'number',
|
||||
maxNumberEmployees: 'number',
|
||||
establishedSince: 'number',
|
||||
establishedUntil: 'number',
|
||||
realEstateChecked: 'boolean',
|
||||
leasedLocation: 'boolean',
|
||||
franchiseResale: 'boolean',
|
||||
title: 'string',
|
||||
brokerName: 'string',
|
||||
searchType: "'exact' | 'radius'",
|
||||
radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'",
|
||||
};
|
||||
// const businessListingCriteriaStructure = {
|
||||
// criteriaType: 'business | commercialProperty | broker',
|
||||
// types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
|
||||
// city: 'string',
|
||||
// state: 'string',
|
||||
// county: 'string',
|
||||
// minPrice: 'number',
|
||||
// maxPrice: 'number',
|
||||
// minRevenue: 'number',
|
||||
// maxRevenue: 'number',
|
||||
// minCashFlow: 'number',
|
||||
// maxCashFlow: 'number',
|
||||
// minNumberEmployees: 'number',
|
||||
// maxNumberEmployees: 'number',
|
||||
// establishedSince: 'number',
|
||||
// establishedUntil: 'number',
|
||||
// realEstateChecked: 'boolean',
|
||||
// leasedLocation: 'boolean',
|
||||
// franchiseResale: 'boolean',
|
||||
// title: 'string',
|
||||
// brokerName: 'string',
|
||||
// searchType: "'exact' | 'radius'",
|
||||
// radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'",
|
||||
// };
|
||||
|
||||
const BusinessListingCriteriaStructure = `
|
||||
export interface BusinessListingCriteria {
|
||||
state: string;
|
||||
city: string;
|
||||
searchType: 'exact' | 'radius';
|
||||
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
|
||||
|
||||
minPrice: number;
|
||||
maxPrice: number;
|
||||
minRevenue: number;
|
||||
maxRevenue: number;
|
||||
minCashFlow: number;
|
||||
maxCashFlow: number;
|
||||
minNumberEmployees: number;
|
||||
maxNumberEmployees: number;
|
||||
establishedSince: number;
|
||||
establishedUntil: number;
|
||||
realEstateChecked: boolean;
|
||||
leasedLocation: boolean;
|
||||
franchiseResale: boolean;
|
||||
//title: string;
|
||||
brokerName: string;
|
||||
//types:"'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
|
||||
criteriaType: 'businessListings';
|
||||
}
|
||||
`;
|
||||
const CommercialPropertyListingCriteriaStructure = `
|
||||
export interface CommercialPropertyListingCriteria {
|
||||
state: string;
|
||||
city: string;
|
||||
searchType: 'exact' | 'radius';
|
||||
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
|
||||
|
||||
minPrice: number;
|
||||
maxPrice: number;
|
||||
//title: string;
|
||||
//types:"'Retail'|'Land'|'Industrial'|'Office'|'Mixed Use'|'Multifamily'|'Uncategorized'"
|
||||
criteriaType: 'commercialPropertyListings';
|
||||
}
|
||||
`;
|
||||
const UserListingCriteriaStructure = `
|
||||
export interface UserListingCriteria {
|
||||
state: string;
|
||||
city: string;
|
||||
searchType: 'exact' | 'radius';
|
||||
radius: '20' | '50' | '100' | '200' | '300' | '400' | '500';
|
||||
|
||||
brokerName: string;
|
||||
companyName: string;
|
||||
counties: string[];
|
||||
criteriaType: 'brokerListings';
|
||||
}
|
||||
|
||||
`;
|
||||
@Injectable()
|
||||
export class AiService {
|
||||
private readonly openai: OpenAI;
|
||||
@@ -67,8 +122,10 @@ export class AiService {
|
||||
{
|
||||
role: 'system',
|
||||
content: `Please create unformatted JSON Object from a user input.
|
||||
The type must be: ${JSON.stringify(businessListingCriteriaStructure)}.
|
||||
If location details available please fill city, county and state as State Code`,
|
||||
The criteriaType must be only either 'businessListings' or 'commercialPropertyListings' or 'brokerListings' !!!!
|
||||
The format of the object (depending on your choice of criteriaType) must be either ${BusinessListingCriteriaStructure}, ${CommercialPropertyListingCriteriaStructure} or ${UserListingCriteriaStructure} !!!!
|
||||
If location details available please fill city and state as State Code and only county if explicitly mentioned.
|
||||
If you decide for searchType==='exact', please do not set the attribute radius`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
@@ -77,6 +134,8 @@ export class AiService {
|
||||
],
|
||||
model: 'llama-3.1-70b-versatile',
|
||||
//model: 'llama-3.1-8b-instant',
|
||||
// model: 'mixtral-8x7b-32768',
|
||||
//model: 'gemma2-9b-it',
|
||||
temperature: 0.2,
|
||||
max_tokens: 300,
|
||||
response_format: { type: 'json_object' },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
|
||||
import { AppService } from './app.service.js';
|
||||
import { AuthService } from './auth/auth.service.js';
|
||||
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js';
|
||||
import { AppService } from './app.service';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@@ -14,6 +14,5 @@ export class AppController {
|
||||
@Get()
|
||||
getHello(@Request() req): string {
|
||||
return req.user;
|
||||
//return 'dfgdf';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,46 @@
|
||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import * as dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
import { AiModule } from './ai/ai.module.js';
|
||||
import { AppController } from './app.controller.js';
|
||||
import { AppService } from './app.service.js';
|
||||
import { AuthModule } from './auth/auth.module.js';
|
||||
import { FileService } from './file/file.service.js';
|
||||
import { GeoModule } from './geo/geo.module.js';
|
||||
import { ImageModule } from './image/image.module.js';
|
||||
import { ListingsModule } from './listings/listings.module.js';
|
||||
import { MailModule } from './mail/mail.module.js';
|
||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
||||
import { UserModule } from './user/user.module.js';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
import { AiModule } from './ai/ai.module';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { FileService } from './file/file.service';
|
||||
import { GeoModule } from './geo/geo.module';
|
||||
import { ImageModule } from './image/image.module';
|
||||
import { ListingsModule } from './listings/listings.module';
|
||||
import { LogController } from './log/log.controller';
|
||||
import { LogModule } from './log/log.module';
|
||||
|
||||
function loadEnvFiles() {
|
||||
// Load the .env file
|
||||
dotenv.config();
|
||||
console.log('Loaded .env file');
|
||||
import dotenvFlow from 'dotenv-flow';
|
||||
import { EventModule } from './event/event.module';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { MailModule } from './mail/mail.module';
|
||||
|
||||
// Determine which additional env file to load
|
||||
let envFilePath = '';
|
||||
const host = process.env.HOST_NAME || '';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ClsMiddleware, ClsModule } from 'nestjs-cls';
|
||||
import { LoggingInterceptor } from './interceptors/logging.interceptor';
|
||||
import { UserInterceptor } from './interceptors/user.interceptor';
|
||||
import { PaymentModule } from './payment/payment.module';
|
||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
|
||||
if (host.includes('localhost')) {
|
||||
envFilePath = '.env.local';
|
||||
} else if (host.includes('dev.bizmatch.net')) {
|
||||
envFilePath = '.env.dev';
|
||||
} else if (host.includes('www.bizmatch.net') || host.includes('bizmatch.net')) {
|
||||
envFilePath = '.env.prod';
|
||||
}
|
||||
|
||||
// Load the additional env file if it exists
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
dotenv.config({ path: envFilePath });
|
||||
console.log(`Loaded ${envFilePath} file`);
|
||||
} else {
|
||||
console.log(`No additional .env file found for HOST_NAME: ${host}`);
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFiles();
|
||||
//loadEnvFiles();
|
||||
dotenvFlow.config();
|
||||
console.log('Loaded environment variables:');
|
||||
console.log(JSON.stringify(process.env, null, 2));
|
||||
@Module({
|
||||
imports: [
|
||||
ClsModule.forRoot({
|
||||
global: true, // Macht den ClsService global verfügbar
|
||||
middleware: { mount: true }, // Registriert automatisch die ClsMiddleware
|
||||
// setup: clsService => {
|
||||
// // Optional: zusätzliche Setup-Logik
|
||||
// },
|
||||
}),
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
MailModule,
|
||||
AuthModule,
|
||||
@@ -56,7 +48,9 @@ loadEnvFiles();
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.timestamp({
|
||||
format: 'YYYY-MM-DD hh:mm:ss.SSS A',
|
||||
}),
|
||||
winston.format.ms(),
|
||||
nestWinstonModuleUtilities.format.nestLike('Bizmatch', {
|
||||
colors: true,
|
||||
@@ -75,12 +69,28 @@ loadEnvFiles();
|
||||
ImageModule,
|
||||
PassportModule,
|
||||
AiModule,
|
||||
LogModule,
|
||||
PaymentModule,
|
||||
EventModule,
|
||||
],
|
||||
controllers: [AppController, LogController],
|
||||
providers: [
|
||||
AppService,
|
||||
FileService,
|
||||
JwtStrategy,
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: UserInterceptor, // Registriere den Interceptor global
|
||||
},
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: LoggingInterceptor, // Registriere den LoggingInterceptor global
|
||||
},
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, FileService],
|
||||
})
|
||||
export class AppModule {
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(ClsMiddleware).forRoutes('*');
|
||||
consumer.apply(RequestDurationMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
||||
30
bizmatch-server/src/assets/keycloak-certs.json
Normal file
30
bizmatch-server/src/assets/keycloak-certs.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kid": "0NxHr10meEVrGYmGlWz_WHiTPxbuNaU6vmShQYWFBh8",
|
||||
"kty": "RSA",
|
||||
"alg": "RSA-OAEP",
|
||||
"use": "enc",
|
||||
"n": "7hzWTnW6WOrZQmeZ26fD5Fu0NvxiQP8pVfesK9MXO4R1gjGlPViGWCdUKrG9Ux6h9X6SXHOWPWZmbfmjNeK7kQOjYPS_06GQ3X19tFikdWoufZMTpAb6p9CENsIbpzX9c1JZRs1xSJ9B505NjLVp29WzhugQfQR2ctv4nLZYmo1ojGjUQMGPNO_4bMqzO_luBQGEAqnRojZzxHVp-ruNyR9DmQbPbUULrOOXfGjCeAYukZ-5UHl6pngk8b6NKdGq6E_qxNsZVStWxbeGAG5UhxSl6oaGL8R0fP9JiAtlWfubJsCtibk712MaMb59JEdr_f3R3pXN7He8brS3smPgcQ",
|
||||
"e": "AQAB",
|
||||
"x5c": [
|
||||
"MIIClTCCAX0CBgGN9oQZDTANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjQwMjI5MjAxNjA4WhcNMzQwMjI4MjAxNzQ4WjAOMQwwCgYDVQQDDANkZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDuHNZOdbpY6tlCZ5nbp8PkW7Q2/GJA/ylV96wr0xc7hHWCMaU9WIZYJ1Qqsb1THqH1fpJcc5Y9ZmZt+aM14ruRA6Ng9L/ToZDdfX20WKR1ai59kxOkBvqn0IQ2whunNf1zUllGzXFIn0HnTk2MtWnb1bOG6BB9BHZy2/ictliajWiMaNRAwY807/hsyrM7+W4FAYQCqdGiNnPEdWn6u43JH0OZBs9tRQus45d8aMJ4Bi6Rn7lQeXqmeCTxvo0p0aroT+rE2xlVK1bFt4YAblSHFKXqhoYvxHR8/0mIC2VZ+5smwK2JuTvXYxoxvn0kR2v9/dHelc3sd7xutLeyY+BxAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAL5CFk/T8Thoi6yNRg7CSiWuehCwwzC+FfsoVQNkcq2loZYgWjO34b9fXysT0xXLJOAnw0+xvwAhbVdBwTathQb2PJST5Ei6RGIOsT2gfE91Je3BGpXnsNNDja0be1bS/uN07aa3MshkgVBOYVDe2FoK7g4zSgViMXLEzGpRdio9mIrH3KADdEAFrhiNClu19gefONT86vUvIpSCS4XJ+nSUPbNkbhe9MlvZ8TRWFMoUzuZML6Xf+FbimAv1ZBk1NWobWPtyaDFF9Lgse7LHGiKPKvBHonVMbWYf7Lk8nGA7/90WVOX5Fd2LItH/13rPNlwbspAcz/nB2groa8/DrdE="
|
||||
],
|
||||
"x5t": "3ZyfzL7Gn0dcNq8H8X1L0uagQMI",
|
||||
"x5t#S256": "Wwu30X3ZnchcXsJHJmOHT8BLOFCH6y2TpO3hyzojhdk"
|
||||
},
|
||||
{
|
||||
"kid": "yAfIWlA3TFvR_h112X4sJHK0kog4_4xDLkRnJnzTv98",
|
||||
"kty": "RSA",
|
||||
"alg": "RS256",
|
||||
"use": "sig",
|
||||
"n": "xpYiq2XOtKV-xeLmFM-4sUWDpzw1UJlN9NXj833MZKsW_bwWixlsJTsB-2kfQ6mXUTbfxsuoZuWMZdQVpsWoKOPeK1Gsd8Gsoa0v2pv3uzPA8_SLqDrBNtIz9mDJc6jf-XkOdtAfPzW_aMf4TzThzIkEH5ptUde0gDKNd8je2lFo4loFJkLhOO2HZ7cLQcspXB_vNqpjAMED15GmGRizeTsA4IWC9WjGyziVvlbgQqC0MqCieT2r4dB0FZGWFwzlm-EhvyHu6G1Hw55jn5AcEHh5fke9XvTBzF6MmM_MQEDc9QWHj16ekVdQB7fxzBHbyLMr3ivQizcHAGYvemNhHw",
|
||||
"e": "AQAB",
|
||||
"x5c": [
|
||||
"MIIClTCCAX0CBgGN9oQYYzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjQwMjI5MjAxNjA4WhcNMzQwMjI4MjAxNzQ4WjAOMQwwCgYDVQQDDANkZXYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDGliKrZc60pX7F4uYUz7ixRYOnPDVQmU301ePzfcxkqxb9vBaLGWwlOwH7aR9DqZdRNt/Gy6hm5Yxl1BWmxago494rUax3wayhrS/am/e7M8Dz9IuoOsE20jP2YMlzqN/5eQ520B8/Nb9ox/hPNOHMiQQfmm1R17SAMo13yN7aUWjiWgUmQuE47YdntwtByylcH+82qmMAwQPXkaYZGLN5OwDghYL1aMbLOJW+VuBCoLQyoKJ5Pavh0HQVkZYXDOWb4SG/Ie7obUfDnmOfkBwQeHl+R71e9MHMXoyYz8xAQNz1BYePXp6RV1AHt/HMEdvIsyveK9CLNwcAZi96Y2EfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABQaqejZ5iWybWeiK0j9iKTn5DNr8LFXdJNRk+odI5TwCtaVDTCQRrF1KKT6F6RmzQyc6xyKojtnI1mKjs+Wo8vYE483pDgoGkv7UquKeQAWbXRajbkpGKasIux7m0MgDhPGKtxoha3kI2Yi2dOFYGdRuqv35/ZD+9nfHfk03fylrf5saroOYBGW6RRpdygB14zQ5ZbXin6gVJSBuJWMiWpxzAB05llZVaHOJ7kO+402YV2/l2TJm0bc883HZuIKxh11PI20lZop9ZwctVtmwf2iFfMfQgQ5wZpV/1gEMynVypxe6OY7biQyIERX6oEFWmZIOrnytSawLyy5gCFrStY="
|
||||
],
|
||||
"x5t": "L27m4VtyyHlrajDI_47_mmRSP08",
|
||||
"x5t#S256": "KOcIpGLNb4ZGg_G2jc6ieZC_86-QQjoaSsMDoV0RWZg"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,40 +1,38 @@
|
||||
import { Controller, Get, Param, Put } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { Body, Controller, Get, Param, Put, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from 'src/jwt-auth/jwt-auth.guard';
|
||||
import { KeycloakUser } from 'src/models/main.model';
|
||||
import { AdminAuthGuard } from '../jwt-auth/admin-auth.guard';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get()
|
||||
getAccessToken(): any {
|
||||
return this.authService.getAccessToken();
|
||||
async getAccessToken(): Promise<any> {
|
||||
return await this.authService.getAccessToken();
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
getUsers(): any {
|
||||
return this.authService.getUsers();
|
||||
}
|
||||
@Get('user/:userid')
|
||||
getUser(@Param('userid') userId: string): any {
|
||||
return this.authService.getUser(userId);
|
||||
}
|
||||
@Get('groups')
|
||||
getGroups(): any {
|
||||
return this.authService.getGroups();
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('user/all')
|
||||
async getUsers(): Promise<any> {
|
||||
return await this.authService.getUsers();
|
||||
}
|
||||
|
||||
@Get('user/:userid/groups') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
|
||||
getGroupsForUsers(@Param('userid') userId: string): any {
|
||||
return this.authService.getGroupsForUser(userId);
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('users/:userid')
|
||||
async getUser(@Param('userid') userId: string): Promise<any> {
|
||||
return await this.authService.getUser(userId);
|
||||
}
|
||||
|
||||
@Get('user/:userid/lastlogin') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
|
||||
getLastLogin(@Param('userid') userId: string): any {
|
||||
return this.authService.getLastLogin(userId);
|
||||
}
|
||||
|
||||
@Put('user/:userid/group/:groupid') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth //
|
||||
addUser2Group(@Param('userid') userId: string,@Param('groupid') groupId: string): any {
|
||||
return this.authService.addUser2Group(userId,groupId);
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Put('users/:userid')
|
||||
async updateKeycloakUser(@Body() keycloakUser: KeycloakUser): Promise<any> {
|
||||
return await this.authService.updateKeycloakUser(keycloakUser);
|
||||
}
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Get('user/:userid/lastlogin') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
|
||||
// getLastLogin(@Param('userid') userId: string): any {
|
||||
// return this.authService.getLastLogin(userId);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtStrategy } from '../jwt.strategy.js';
|
||||
import { AuthController } from './auth.controller.js';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Module({
|
||||
imports: [PassportModule],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
providers: [AuthService],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
})
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ky from 'ky';
|
||||
import { KeycloakUser } from 'src/models/main.model';
|
||||
import urlcat from 'urlcat';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
public async getAccessToken() {
|
||||
const form = new FormData();
|
||||
form.append('grant_type', 'password');
|
||||
form.append('username', process.env.user);
|
||||
form.append('password', process.env.password);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('grant_type', 'password');
|
||||
params.append('username', process.env.user);
|
||||
params.append('password', process.env.password);
|
||||
const URL = `${process.env.host}${process.env.tokenURL}`;
|
||||
params.append('username', process.env.KEYCLOAK_ADMIN_USER);
|
||||
params.append('password', process.env.KEYCLOAK_ADMIN_PASSWORD);
|
||||
const URL = `${process.env.KEYCLOAK_HOST}${process.env.KEYCLOAK_TOKEN_URL}`;
|
||||
|
||||
const response = await ky
|
||||
.post(URL, {
|
||||
body: params.toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: 'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return (<any>response).access_token;
|
||||
const response = await fetch(URL, {
|
||||
method: 'POST',
|
||||
body: params.toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: process.env.KEYCLOAK_ADMIN_TOKEN,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return (<any>data).access_token;
|
||||
} catch (error) {
|
||||
if (error.name === 'HTTPError') {
|
||||
const errorJson = await error.response.json();
|
||||
@@ -37,83 +34,68 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getUsers() {
|
||||
public async getUsers(): Promise<KeycloakUser[]> {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.usersURL}`;
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
const URL = `${process.env.KEYCLOAK_HOST}${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USERS_URL}`;
|
||||
const response = await fetch(URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data as KeycloakUser[];
|
||||
}
|
||||
public async getUser(userid: string) {
|
||||
public async getUser(userid: string): Promise<KeycloakUser> {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.userURL, { userid });
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USER_URL}`;
|
||||
const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid });
|
||||
const response = await fetch(URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data as KeycloakUser;
|
||||
}
|
||||
public async getGroups() {
|
||||
public async updateKeycloakUser(keycloakUser: KeycloakUser): Promise<void> {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.groupsURL}`;
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
|
||||
public async getGroupsForUser(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.userGroupsURL, { userid });
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async getLastLogin(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.lastLoginURL, { userid });
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async addUser2Group(userid: string, groupid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.addUser2GroupURL, { userid, groupid });
|
||||
const response = await ky
|
||||
.put(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
const userid = keycloakUser.id;
|
||||
const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_USER_URL}`;
|
||||
const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid });
|
||||
const response = await fetch(URL, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(keycloakUser),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
}
|
||||
// public async getLastLogin(userid: string) {
|
||||
// const token = await this.getAccessToken();
|
||||
// const URLPATH = `${process.env.KEYCLOAK_ADMIN_REALM}${process.env.REALM}${process.env.KEYCLOAK_LASTLOGIN_URL}`;
|
||||
// const URL = urlcat(process.env.KEYCLOAK_HOST, URLPATH, { userid });
|
||||
// const response = await ky
|
||||
// .get(URL, {
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/x-www-form-urlencoded',
|
||||
// Authorization: `Bearer ${token}`,
|
||||
// },
|
||||
// })
|
||||
// .json();
|
||||
// return response;
|
||||
// }
|
||||
}
|
||||
|
||||
8
bizmatch-server/src/decorators/real-ip.decorator.ts
Normal file
8
bizmatch-server/src/decorators/real-ip.decorator.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// src/decorators/real-ip.decorator.ts
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { getRealIpInfo, RealIpInfo } from '../utils/ip.util';
|
||||
|
||||
export const RealIp = createParamDecorator((data: unknown, ctx: ExecutionContext): RealIpInfo => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
return getRealIpInfo(request);
|
||||
});
|
||||
@@ -1,24 +1,36 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pkg from 'pg';
|
||||
const { Pool } = pkg;
|
||||
import * as schema from './schema.js';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { jsonb, varchar } from 'drizzle-orm/pg-core';
|
||||
import { PG_CONNECTION } from './schema.js';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import pkg from 'pg';
|
||||
import { Logger } from 'winston';
|
||||
import * as schema from './schema';
|
||||
import { PG_CONNECTION } from './schema';
|
||||
const { Pool } = pkg;
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: PG_CONNECTION,
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
inject: [ConfigService, WINSTON_MODULE_PROVIDER, ClsService],
|
||||
useFactory: async (configService: ConfigService, logger: Logger, cls: ClsService) => {
|
||||
const connectionString = configService.get<string>('DATABASE_URL');
|
||||
const pool = new Pool({
|
||||
connectionString,
|
||||
// ssl: true,
|
||||
// ssl: true, // Falls benötigt
|
||||
});
|
||||
|
||||
return drizzle(pool, { schema, logger:true });
|
||||
// Definiere einen benutzerdefinierten Logger für Drizzle
|
||||
const drizzleLogger = {
|
||||
logQuery(query: string, params: unknown[]): void {
|
||||
const ip = cls.get('ip') || 'unknown';
|
||||
const countryCode = cls.get('countryCode') || 'unknown';
|
||||
const username = cls.get('username') || 'unknown';
|
||||
logger.info(`IP: ${ip} (${countryCode}) (${username}) - Query: ${query} - Params: ${JSON.stringify(params)}`);
|
||||
},
|
||||
};
|
||||
|
||||
return drizzle(pool, { schema, logger: drizzleLogger });
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
35
bizmatch-server/src/drizzle/export.ts
Normal file
35
bizmatch-server/src/drizzle/export.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { promises as fs } from 'fs';
|
||||
import { Pool } from 'pg';
|
||||
import * as schema from './schema';
|
||||
|
||||
// Drizzle-Tabellen-Definitionen (hier hast du bereits die Tabellen definiert, wir nehmen an, sie werden hier importiert)
|
||||
import { businesses, commercials, users } from './schema'; // Anpassen je nach tatsächlicher Struktur
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
console.log(connectionString);
|
||||
const client = new Pool({ connectionString });
|
||||
const db = drizzle(client, { schema, logger: true });
|
||||
(async () => {
|
||||
try {
|
||||
// Abfrage der Daten für jede Tabelle
|
||||
const usersData = await db.select().from(users).execute();
|
||||
const businessesData = await db.select().from(businesses).execute();
|
||||
const commercialsData = await db.select().from(commercials).execute();
|
||||
|
||||
// Speichern der Daten in JSON-Dateien
|
||||
await fs.writeFile('./data/users_export.json', JSON.stringify(usersData, null, 2));
|
||||
console.log('Users exportiert in users.json');
|
||||
|
||||
await fs.writeFile('./data/businesses_export.json', JSON.stringify(businessesData, null, 2));
|
||||
console.log('Businesses exportiert in businesses.json');
|
||||
|
||||
await fs.writeFile('./data/commercials_export.json', JSON.stringify(commercialsData, null, 2));
|
||||
console.log('Commercials exportiert in commercials.json');
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Exportieren der Tabellen:', error);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
})();
|
||||
@@ -2,20 +2,19 @@ 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.js';
|
||||
import { CommercialPropertyService } from 'src/listings/commercial-property.service.js';
|
||||
import { Geo } from 'src/models/server.model.js';
|
||||
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.js';
|
||||
import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { convertUserToDrizzleUser } from '../utils.js';
|
||||
import * as schema from './schema.js';
|
||||
import { User, UserData } from '../models/db.model';
|
||||
import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName } from '../models/main.model';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service';
|
||||
import * as schema from './schema';
|
||||
interface PropertyImportListing {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -54,224 +53,226 @@ interface BusinessImportListing {
|
||||
internals: string;
|
||||
created: string;
|
||||
}
|
||||
const typesOfBusiness: Array<KeyValueStyle> = [
|
||||
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||
{ 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<KeyValueStyle> = [
|
||||
// { name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||
// { name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||
// { 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('', '', '');
|
||||
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.city = 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.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.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.city = 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<number[]> {
|
||||
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<number[]> {
|
||||
// const response = await openai.embeddings.create({
|
||||
// model: 'text-embedding-3-small',
|
||||
// input: text,
|
||||
// });
|
||||
// return response.data[0].embedding;
|
||||
// }
|
||||
|
||||
function getRandomItem<T>(arr: T[]): T {
|
||||
if (arr.length === 0) {
|
||||
@@ -283,7 +284,7 @@ function getRandomItem<T>(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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pkg from 'pg';
|
||||
import * as schema from './schema.js';
|
||||
import * as schema from './schema';
|
||||
const { Pool } = pkg;
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
const pool = new Pool({ connectionString });
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."customerSubType" AS ENUM('broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."customerType" AS ENUM('buyer', 'professional');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."gender" AS ENUM('male', 'female');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."listingsCategory" AS ENUM('commercialProperty', 'business');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "businesses" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"email" varchar(255),
|
||||
"type" varchar(255),
|
||||
"title" varchar(255),
|
||||
"description" text,
|
||||
"city" varchar(255),
|
||||
"state" char(2),
|
||||
"price" double precision,
|
||||
"favoritesForUser" varchar(30)[],
|
||||
"draft" boolean,
|
||||
"listingsCategory" "listingsCategory",
|
||||
"realEstateIncluded" boolean,
|
||||
"leasedLocation" boolean,
|
||||
"franchiseResale" boolean,
|
||||
"salesRevenue" double precision,
|
||||
"cashFlow" double precision,
|
||||
"supportAndTraining" text,
|
||||
"employees" integer,
|
||||
"established" integer,
|
||||
"internalListingNumber" integer,
|
||||
"reasonForSale" varchar(255),
|
||||
"brokerLicencing" varchar(255),
|
||||
"internals" text,
|
||||
"imageName" varchar(200),
|
||||
"created" timestamp,
|
||||
"updated" timestamp,
|
||||
"latitude" double precision,
|
||||
"longitude" double precision
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "commercials" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"serialId" serial NOT NULL,
|
||||
"email" varchar(255),
|
||||
"type" varchar(255),
|
||||
"title" varchar(255),
|
||||
"description" text,
|
||||
"city" varchar(255),
|
||||
"state" char(2),
|
||||
"price" double precision,
|
||||
"favoritesForUser" varchar(30)[],
|
||||
"listingsCategory" "listingsCategory",
|
||||
"draft" boolean,
|
||||
"imageOrder" varchar(200)[],
|
||||
"imagePath" varchar(200),
|
||||
"created" timestamp,
|
||||
"updated" timestamp,
|
||||
"latitude" double precision,
|
||||
"longitude" double precision
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"firstname" varchar(255) NOT NULL,
|
||||
"lastname" varchar(255) NOT NULL,
|
||||
"email" varchar(255) NOT NULL,
|
||||
"phoneNumber" varchar(255),
|
||||
"description" text,
|
||||
"companyName" varchar(255),
|
||||
"companyOverview" text,
|
||||
"companyWebsite" varchar(255),
|
||||
"city" varchar(255),
|
||||
"state" char(2),
|
||||
"offeredServices" text,
|
||||
"areasServed" jsonb,
|
||||
"hasProfile" boolean,
|
||||
"hasCompanyLogo" boolean,
|
||||
"licensedIn" jsonb,
|
||||
"gender" "gender",
|
||||
"customerType" "customerType",
|
||||
"customerSubType" "customerSubType",
|
||||
"created" timestamp,
|
||||
"updated" timestamp,
|
||||
"latitude" double precision,
|
||||
"longitude" double precision,
|
||||
CONSTRAINT "users_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "businesses" ADD CONSTRAINT "businesses_email_users_email_fk" FOREIGN KEY ("email") REFERENCES "public"."users"("email") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "commercials" ADD CONSTRAINT "commercials_email_users_email_fk" FOREIGN KEY ("email") REFERENCES "public"."users"("email") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
@@ -1,541 +0,0 @@
|
||||
{
|
||||
"id": "a8283ca6-2c10-42bb-a640-ca984544ba30",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
},
|
||||
"city": {
|
||||
"name": "city",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "char(2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"offeredServices": {
|
||||
"name": "offeredServices",
|
||||
"type": "text",
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1723045357281,
|
||||
"tag": "0000_lean_marvex",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,92 +1,149 @@
|
||||
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']);
|
||||
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'professional']);
|
||||
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'seller', 'professional']);
|
||||
export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
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<AreasServed[]>(),
|
||||
hasProfile: boolean('hasProfile'),
|
||||
hasCompanyLogo: boolean('hasCompanyLogo'),
|
||||
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
||||
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<AreasServed[]>(),
|
||||
hasProfile: boolean('hasProfile'),
|
||||
hasCompanyLogo: boolean('hasCompanyLogo'),
|
||||
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
||||
gender: genderEnum('gender'),
|
||||
customerType: customerTypeEnum('customerType'),
|
||||
customerSubType: customerSubTypeEnum('customerSubType'),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
latitude: doublePrecision('latitude'),
|
||||
longitude: doublePrecision('longitude'),
|
||||
// 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: varchar('listing_id', { length: 255 }), // Assuming listings are referenced by UUID, adjust as necessary
|
||||
email: varchar('email', { length: 255 }),
|
||||
eventType: varchar('event_type', { length: 50 }), // 'view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact'
|
||||
eventTimestamp: timestamp('event_timestamp').defaultNow(),
|
||||
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.)
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Angenommen, du hast eine Datei `databaseModels.js` mit deinen pgTable-Definitionen
|
||||
const { users } = require('./schema.js');
|
||||
const { users } = require('./schema');
|
||||
|
||||
function generateTypeScriptInterface(tableDefinition, tableName) {
|
||||
let interfaceString = `export interface ${tableName} {\n`;
|
||||
|
||||
24
bizmatch-server/src/event/event.controller.ts
Normal file
24
bizmatch-server/src/event/event.controller.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Body, Controller, Headers, Post, UseGuards } from '@nestjs/common';
|
||||
import { RealIp } from 'src/decorators/real-ip.decorator';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { ListingEvent } from 'src/models/db.model';
|
||||
import { RealIpInfo } from 'src/models/main.model';
|
||||
import { EventService } from './event.service';
|
||||
|
||||
@Controller('event')
|
||||
export class EventController {
|
||||
constructor(private eventService: EventService) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post()
|
||||
async createEvent(
|
||||
@Body() event: ListingEvent, // Struktur des Body-Objekts entsprechend anpassen
|
||||
@RealIp() ipInfo: RealIpInfo, // IP Adresse des Clients
|
||||
@Headers('user-agent') userAgent: string, // User-Agent des Clients
|
||||
) {
|
||||
event.userIp = ipInfo.ip;
|
||||
event.userAgent = userAgent;
|
||||
await this.eventService.createEvent(event);
|
||||
return { message: 'Event gespeichert' };
|
||||
}
|
||||
}
|
||||
11
bizmatch-server/src/event/event.module.ts
Normal file
11
bizmatch-server/src/event/event.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DrizzleModule } from 'src/drizzle/drizzle.module';
|
||||
import { EventController } from './event.controller';
|
||||
import { EventService } from './event.service';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule],
|
||||
controllers: [EventController],
|
||||
providers: [EventService],
|
||||
})
|
||||
export class EventModule {}
|
||||
18
bizmatch-server/src/event/event.service.ts
Normal file
18
bizmatch-server/src/event/event.service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { ListingEvent } from 'src/models/db.model';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { listingEvents, PG_CONNECTION } from '../drizzle/schema';
|
||||
@Injectable()
|
||||
export class EventService {
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
) {}
|
||||
async createEvent(event: ListingEvent) {
|
||||
// Speichere das Event in der Datenbank
|
||||
event.eventTimestamp = new Date();
|
||||
await this.conn.insert(listingEvents).values(event).execute();
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,22 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import path, { join } from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { Logger } from 'winston';
|
||||
import { ImageProperty, Subscription } from '../models/main.model.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class FileService {
|
||||
private subscriptions: any;
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
this.loadSubscriptions();
|
||||
fs.ensureDirSync(`./pictures`);
|
||||
fs.ensureDirSync(`./pictures/profile`);
|
||||
fs.ensureDirSync(`./pictures/logo`);
|
||||
fs.ensureDirSync(`./pictures/property`);
|
||||
}
|
||||
// ############
|
||||
// Subscriptions
|
||||
// ############
|
||||
private loadSubscriptions(): void {
|
||||
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
this.subscriptions = JSON.parse(rawData);
|
||||
}
|
||||
getSubscriptions(): Subscription[] {
|
||||
return this.subscriptions;
|
||||
}
|
||||
// ############
|
||||
// Profile
|
||||
// ############
|
||||
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const quality = 50;
|
||||
const output = await sharp(file.buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
@@ -50,7 +31,7 @@ export class FileService {
|
||||
// Logo
|
||||
// ############
|
||||
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const quality = 50;
|
||||
const output = await sharp(file.buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
@@ -79,7 +60,6 @@ export class FileService {
|
||||
}
|
||||
}
|
||||
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
|
||||
const result: ImageProperty[] = [];
|
||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||
if (fs.existsSync(directory)) {
|
||||
const files = await fs.readdir(directory);
|
||||
@@ -89,7 +69,6 @@ export class FileService {
|
||||
}
|
||||
}
|
||||
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
|
||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
|
||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||
fs.ensureDirSync(`${directory}`);
|
||||
const imageName = await this.getNextImageName(directory);
|
||||
@@ -116,16 +95,15 @@ export class FileService {
|
||||
}
|
||||
}
|
||||
async resizeImageToAVIF(buffer: Buffer, maxSize: number, imageName: string, directory: string) {
|
||||
let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
|
||||
let output;
|
||||
let start = Date.now();
|
||||
output = await sharp(buffer)
|
||||
const quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
|
||||
const start = Date.now();
|
||||
const output = await sharp(buffer)
|
||||
.resize({ width: 1500 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
|
||||
let timeTaken = Date.now() - start;
|
||||
const timeTaken = Date.now() - start;
|
||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
||||
}
|
||||
deleteImage(path: string) {
|
||||
|
||||
@@ -1,27 +1,41 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { CountyRequest } from 'src/models/server.model.js';
|
||||
import { GeoService } from './geo.service.js';
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { RealIp } from 'src/decorators/real-ip.decorator';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { RealIpInfo } from 'src/models/main.model';
|
||||
import { CountyRequest } from 'src/models/server.model';
|
||||
import { GeoService } from './geo.service';
|
||||
|
||||
@Controller('geo')
|
||||
export class GeoController {
|
||||
constructor(private geoService: GeoService) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':prefix')
|
||||
findByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('citiesandstates/:prefix')
|
||||
findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesAndStatesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':prefix/:state')
|
||||
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix, state);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('counties')
|
||||
findByPrefixAndStates(@Body() countyRequest: CountyRequest): any {
|
||||
return this.geoService.findCountiesStartingWith(countyRequest.prefix, countyRequest.states);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('ipinfo/georesult/wysiwyg')
|
||||
async fetchIpAndGeoLocation(@RealIp() ipInfo: RealIpInfo): Promise<any> {
|
||||
return await this.geoService.fetchIpAndGeoLocation(ipInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GeoController } from './geo.controller.js';
|
||||
import { GeoService } from './geo.service.js';
|
||||
import { GeoController } from './geo.controller';
|
||||
import { GeoService } from './geo.service';
|
||||
|
||||
@Module({
|
||||
controllers: [GeoController],
|
||||
providers: [GeoService]
|
||||
providers: [GeoService],
|
||||
})
|
||||
export class GeoModule {}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
import { CountyResult, GeoResult } from 'src/models/main.model.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { City, CountyData, Geo, State } from '../models/server.model.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { join } from 'path';
|
||||
import { CityAndStateResult, CountyResult, GeoResult, IpInfo, RealIpInfo } from 'src/models/main.model';
|
||||
import { Logger } from 'winston';
|
||||
import { City, CountyData, Geo, State } from '../models/server.model';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class GeoService {
|
||||
geo: Geo;
|
||||
counties: CountyData[];
|
||||
constructor() {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
this.loadGeo();
|
||||
}
|
||||
private loadGeo(): void {
|
||||
@@ -24,13 +24,13 @@ export class GeoService {
|
||||
this.counties = JSON.parse(rawCountiesData);
|
||||
}
|
||||
findCountiesStartingWith(prefix: string, states?: string[]) {
|
||||
let results: CountyResult[] = [];
|
||||
const results: CountyResult[] = [];
|
||||
let idCounter = 1;
|
||||
|
||||
this.counties.forEach(stateData => {
|
||||
if (!states || states.includes(stateData.state)) {
|
||||
stateData.counties.forEach(county => {
|
||||
if (county.startsWith(prefix.toUpperCase())) {
|
||||
if (county.startsWith(prefix?.toUpperCase())) {
|
||||
results.push({
|
||||
id: idCounter++,
|
||||
name: county,
|
||||
@@ -52,7 +52,7 @@ export class GeoService {
|
||||
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
result.push({
|
||||
id: city.id,
|
||||
city: city.name,
|
||||
name: city.name,
|
||||
state: state.state_code,
|
||||
//state_code: state.state_code,
|
||||
latitude: city.latitude,
|
||||
@@ -63,8 +63,8 @@ export class GeoService {
|
||||
});
|
||||
return state ? result.filter(e => e.state.toLowerCase() === state.toLowerCase()) : result;
|
||||
}
|
||||
findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> {
|
||||
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = [];
|
||||
findCitiesAndStatesStartingWith(prefix: string): Array<CityAndStateResult> {
|
||||
const results: Array<CityAndStateResult> = [];
|
||||
|
||||
const lowercasePrefix = prefix.toLowerCase();
|
||||
|
||||
@@ -73,10 +73,9 @@ export class GeoService {
|
||||
for (const state of this.geo.states) {
|
||||
if (state.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: state.id.toString(),
|
||||
name: state.name,
|
||||
id: state.id,
|
||||
type: 'state',
|
||||
state: state.state_code,
|
||||
content: state,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,10 +83,9 @@ export class GeoService {
|
||||
for (const city of state.cities) {
|
||||
if (city.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: city.id.toString(),
|
||||
name: city.name,
|
||||
id: city.id,
|
||||
type: 'city',
|
||||
state: state.state_code,
|
||||
content: { state: state.state_code, ...city },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -97,10 +95,27 @@ export class GeoService {
|
||||
return results.sort((a, b) => {
|
||||
if (a.type === 'state' && b.type === 'city') return -1;
|
||||
if (a.type === 'city' && b.type === 'state') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
return a.content.name.localeCompare(b.content.name);
|
||||
});
|
||||
}
|
||||
getCityWithCoords(state: string, city: string): City {
|
||||
return this.geo.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||
}
|
||||
async fetchIpAndGeoLocation(ipInfo: RealIpInfo): Promise<IpInfo> {
|
||||
this.logger.info(`IP:${ipInfo.ip} - CountryCode:${ipInfo.countryCode}`);
|
||||
const response = await fetch(`${process.env.IP_INFO_URL}/${ipInfo.ip}/geo?token=${process.env.IP_INFO_TOKEN}`, {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// Fügen Sie den Ländercode aus Cloudflare hinzu, falls verfügbar
|
||||
if (ipInfo.countryCode) {
|
||||
data.cloudflareCountry = ipInfo.countryCode;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { JwtAuthGuard } from 'src/jwt-auth/jwt-auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { CommercialPropertyService } from '../listings/commercial-property.service.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { CommercialPropertyService } from '../listings/commercial-property.service';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service';
|
||||
|
||||
@Controller('image')
|
||||
export class ImageController {
|
||||
@@ -17,12 +18,14 @@ export class ImageController {
|
||||
// ############
|
||||
// Property
|
||||
// ############
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('uploadPropertyPicture/:imagePath/:serial')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
|
||||
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
|
||||
await this.listingService.addImage(imagePath, serial, imagename);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
||||
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
|
||||
@@ -31,11 +34,13 @@ export class ImageController {
|
||||
// ############
|
||||
// Profile
|
||||
// ############
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('uploadProfile/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeProfilePicture(file, adjustedEmail);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('profile/:email/')
|
||||
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
||||
@@ -43,11 +48,13 @@ export class ImageController {
|
||||
// ############
|
||||
// Logo
|
||||
// ############
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('uploadCompanyLogo/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeCompanyLogo(file, adjustedEmail);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('logo/:email/')
|
||||
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { ListingsModule } from '../listings/listings.module.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { ImageController } from './image.controller.js';
|
||||
import { ImageService } from './image.service.js';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { ListingsModule } from '../listings/listings.module';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service';
|
||||
import { ImageController } from './image.controller';
|
||||
import { ImageService } from './image.service';
|
||||
|
||||
@Module({
|
||||
imports: [ListingsModule],
|
||||
|
||||
40
bizmatch-server/src/interceptors/logging.interceptor.ts
Normal file
40
bizmatch-server/src/interceptors/logging.interceptor.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// src/interceptors/logging.interceptor.ts
|
||||
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class LoggingInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(LoggingInterceptor.name);
|
||||
|
||||
constructor(private readonly cls: ClsService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
const ip = this.cls.get('ip') || 'unknown';
|
||||
const countryCode = this.cls.get('countryCode') || 'unknown';
|
||||
const username = this.cls.get('username') || 'unknown';
|
||||
|
||||
const method = request.method;
|
||||
const url = request.originalUrl;
|
||||
const start = Date.now();
|
||||
|
||||
this.logger.log(`Entering ${method} ${url} from ${ip} (${countryCode})- User: ${username}`);
|
||||
|
||||
return next.handle().pipe(
|
||||
tap(() => {
|
||||
const duration = Date.now() - start;
|
||||
let logMessage = `${method} ${url} - ${duration}ms - IP: ${ip} - User: ${username}`;
|
||||
|
||||
if (method === 'POST' || method === 'PUT') {
|
||||
const body = JSON.stringify(request.body);
|
||||
logMessage += ` - Incoming Body: ${body}`;
|
||||
}
|
||||
|
||||
this.logger.log(logMessage);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
29
bizmatch-server/src/interceptors/user.interceptor.ts
Normal file
29
bizmatch-server/src/interceptors/user.interceptor.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// src/interceptors/user.interceptor.ts
|
||||
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class UserInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(UserInterceptor.name);
|
||||
|
||||
constructor(private readonly cls: ClsService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
|
||||
// Überprüfe, ob der Benutzer authentifiziert ist
|
||||
if (request.user && request.user.username) {
|
||||
try {
|
||||
this.cls.set('username', request.user.username);
|
||||
this.logger.log(`CLS context gesetzt: Username=${request.user.username}`);
|
||||
} catch (error) {
|
||||
this.logger.error('Fehler beim Setzen der Username im CLS-Kontext', error);
|
||||
}
|
||||
} else {
|
||||
this.logger.log('Kein authentifizierter Benutzer gefunden');
|
||||
}
|
||||
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
18
bizmatch-server/src/jwt-auth/admin-auth.guard.ts
Normal file
18
bizmatch-server/src/jwt-auth/admin-auth.guard.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthGuard extends AuthGuard('jwt') implements CanActivate {
|
||||
canActivate(context: ExecutionContext) {
|
||||
// Add your custom authentication logic here
|
||||
// for example, call super.logIn(request) to establish a session.
|
||||
return super.canActivate(context);
|
||||
}
|
||||
handleRequest(err, user, info) {
|
||||
// You can throw an exception based on either "info" or "err" arguments
|
||||
if (err || !user || !user.roles.includes('ADMIN')) {
|
||||
throw err || new UnauthorizedException(info);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import fs from 'fs';
|
||||
import { passportJwtSecret } from 'jwks-rsa';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import path from 'path';
|
||||
import { Logger } from 'winston';
|
||||
import { JwtPayload, JwtUser } from './models/main.model';
|
||||
// const logger = winston.createLogger({
|
||||
// transports: [new winston.transports.Console()],
|
||||
// });
|
||||
// const pemCache = new Map();
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
@@ -13,13 +19,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {
|
||||
const realm = configService.get<string>('REALM');
|
||||
// const staticCerts = loadStaticCerts();
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKeyProvider: passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
rateLimit: false,
|
||||
// jwksRequestsPerMinute: 5,
|
||||
jwksUri: `https://auth.bizmatch.net/realms/${realm}/protocol/openid-connect/certs`,
|
||||
}),
|
||||
audience: 'account', // Keycloak Client ID
|
||||
@@ -28,7 +35,6 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
algorithms: ['RS256'],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload): Promise<JwtUser> {
|
||||
if (!payload) {
|
||||
this.logger.error('Invalid payload');
|
||||
@@ -39,7 +45,11 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
const result = { userId: payload.sub, firstname: payload.given_name, lastname: payload.family_name, username: payload.preferred_username, roles: payload.realm_access?.roles };
|
||||
this.logger.info(`JWT User: ${JSON.stringify(result)}`); // Debugging: JWT Payload anzeigen
|
||||
return result;
|
||||
}
|
||||
}
|
||||
export function loadStaticCerts() {
|
||||
const certsPath = path.join(__dirname, '../', 'assets', 'keycloak-certs.json');
|
||||
const certsData = fs.readFileSync(certsPath, 'utf8');
|
||||
return JSON.parse(certsData);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Body, Controller, Inject, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { UserListingCriteria } from 'src/models/main.model.js';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { UserListingCriteria } from 'src/models/main.model';
|
||||
import { Logger } from 'winston';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { UserService } from '../user/user.service';
|
||||
|
||||
@Controller('listings/professionals_brokers')
|
||||
export class BrokerListingsController {
|
||||
@@ -11,8 +12,9 @@ export class BrokerListingsController {
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('search')
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
return this.userService.searchUserListings(criteria);
|
||||
async find(@Body() criteria: UserListingCriteria): Promise<any> {
|
||||
return await this.userService.searchUserListings(criteria);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
||||
import { and, arrayContains, asc, count, desc, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { ZodError } from 'zod';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { BusinessListing, BusinessListingSchema } from '../models/db.model.js';
|
||||
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery } from '../utils.js';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { businesses, PG_CONNECTION } from '../drizzle/schema';
|
||||
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 { getDistanceQuery, splitName } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class BusinessListingService {
|
||||
@@ -21,14 +21,15 @@ export class BusinessListingService {
|
||||
private geoService?: GeoService,
|
||||
) {}
|
||||
|
||||
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
|
||||
private getWhereConditions(criteria: BusinessListingCriteria, user: JwtUser): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
|
||||
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);
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
|
||||
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
@@ -36,7 +37,7 @@ export class BusinessListingService {
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(eq(businesses.state, criteria.state));
|
||||
whereConditions.push(sql`${businesses.location}->>'state' = ${criteria.state}`);
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
@@ -94,9 +95,16 @@ export class BusinessListingService {
|
||||
if (criteria.title) {
|
||||
whereConditions.push(or(ilike(businesses.title, `%${criteria.title}%`), ilike(businesses.description, `%${criteria.title}%`)));
|
||||
}
|
||||
|
||||
if (criteria.brokerName) {
|
||||
whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${criteria.brokerName}%`)));
|
||||
const { firstname, lastname } = splitName(criteria.brokerName);
|
||||
if (firstname === lastname) {
|
||||
whereConditions.push(or(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
|
||||
} else {
|
||||
whereConditions.push(and(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
|
||||
}
|
||||
}
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
whereConditions.push(or(eq(businesses.email, user?.username), ne(businesses.draft, true)));
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
|
||||
return whereConditions;
|
||||
@@ -113,29 +121,59 @@ export class BusinessListingService {
|
||||
.from(businesses)
|
||||
.leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
||||
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
const whereConditions = this.getWhereConditions(criteria, user);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
|
||||
// Sortierung
|
||||
switch (criteria.sortBy) {
|
||||
case 'priceAsc':
|
||||
query.orderBy(asc(businesses.price));
|
||||
break;
|
||||
case 'priceDesc':
|
||||
query.orderBy(desc(businesses.price));
|
||||
break;
|
||||
case 'srAsc':
|
||||
query.orderBy(asc(businesses.salesRevenue));
|
||||
break;
|
||||
case 'srDesc':
|
||||
query.orderBy(desc(businesses.salesRevenue));
|
||||
break;
|
||||
case 'cfAsc':
|
||||
query.orderBy(asc(businesses.cashFlow));
|
||||
break;
|
||||
case 'cfDesc':
|
||||
query.orderBy(desc(businesses.cashFlow));
|
||||
break;
|
||||
case 'creationDateFirst':
|
||||
query.orderBy(asc(businesses.created));
|
||||
break;
|
||||
case 'creationDateLast':
|
||||
query.orderBy(desc(businesses.created));
|
||||
break;
|
||||
default:
|
||||
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
|
||||
break;
|
||||
}
|
||||
// Paginierung
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const data = await query;
|
||||
const totalCount = await this.getBusinessListingsCount(criteria);
|
||||
const results = data.map(r => r.business).map(r => convertDrizzleBusinessToBusiness(r));
|
||||
const totalCount = await this.getBusinessListingsCount(criteria, user);
|
||||
const results = data.map(r => r.business);
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
async getBusinessListingsCount(criteria: BusinessListingCriteria): Promise<number> {
|
||||
async getBusinessListingsCount(criteria: BusinessListingCriteria, user: JwtUser): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(businesses).leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
||||
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
const whereConditions = this.getWhereConditions(criteria, user);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
@@ -147,17 +185,25 @@ export class BusinessListingService {
|
||||
}
|
||||
|
||||
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
|
||||
let result = await this.conn
|
||||
const conditions = [];
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
conditions.push(or(eq(businesses.email, user?.username), ne(businesses.draft, true)));
|
||||
}
|
||||
conditions.push(sql`${businesses.id} = ${id}`);
|
||||
const result = await this.conn
|
||||
.select()
|
||||
.from(businesses)
|
||||
.where(and(sql`${businesses.id} = ${id}`));
|
||||
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
||||
return convertDrizzleBusinessToBusiness(result[0]) as BusinessListing;
|
||||
.where(and(...conditions));
|
||||
if (result.length > 0) {
|
||||
return result[0] as BusinessListing;
|
||||
} else {
|
||||
throw new BadRequestException(`No entry available for ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
|
||||
const conditions = [];
|
||||
conditions.push(eq(businesses.imageName, emailToDirName(email)));
|
||||
conditions.push(eq(businesses.email, email));
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||
conditions.push(ne(businesses.draft, true));
|
||||
}
|
||||
@@ -166,25 +212,35 @@ 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<BusinessListing[]> {
|
||||
const userFavorites = await this.conn
|
||||
.select()
|
||||
.from(businesses)
|
||||
.where(arrayContains(businesses.favoritesForUser, [user.username]));
|
||||
return userFavorites;
|
||||
}
|
||||
|
||||
// #### CREATE ########################################
|
||||
async createListing(data: BusinessListing): Promise<BusinessListing> {
|
||||
try {
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
data.updated = new Date();
|
||||
const validatedBusinessListing = BusinessListingSchema.parse(data);
|
||||
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
|
||||
BusinessListingSchema.parse(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 formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
const filteredErrors = error.errors
|
||||
.map(item => ({
|
||||
...item,
|
||||
field: item.path[0],
|
||||
}))
|
||||
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
|
||||
throw new BadRequestException(filteredErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -194,17 +250,19 @@ export class BusinessListingService {
|
||||
try {
|
||||
data.updated = new Date();
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
const validatedBusinessListing = BusinessListingSchema.parse(data);
|
||||
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
|
||||
BusinessListingSchema.parse(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 formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
const filteredErrors = error.errors
|
||||
.map(item => ({
|
||||
...item,
|
||||
field: item.path[0],
|
||||
}))
|
||||
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
|
||||
throw new BadRequestException(filteredErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -213,14 +271,13 @@ export class BusinessListingService {
|
||||
async deleteListing(id: string): Promise<void> {
|
||||
await this.conn.delete(businesses).where(eq(businesses.id, id));
|
||||
}
|
||||
// ##############################################################
|
||||
// States
|
||||
// ##############################################################
|
||||
async getStates(): Promise<any[]> {
|
||||
return await this.conn
|
||||
.select({ state: businesses.state, count: sql<number>`count(${businesses.id})`.mapWith(Number) })
|
||||
.from(businesses)
|
||||
.groupBy(sql`${businesses.state}`)
|
||||
.orderBy(sql`count desc`);
|
||||
// #### DELETE Favorite ###################################
|
||||
async deleteFavorite(id: string, user: JwtUser): Promise<void> {
|
||||
await this.conn
|
||||
.update(businesses)
|
||||
.set({
|
||||
favoritesForUser: sql`array_remove(${businesses.favoritesForUser}, ${user.username})`,
|
||||
})
|
||||
.where(sql`${businesses.id} = ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { BusinessListing } from 'src/models/db.model.js';
|
||||
import { Logger } from 'winston';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
|
||||
import { BusinessListingService } from './business-listing.service.js';
|
||||
import { JwtAuthGuard } from '../jwt-auth/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
import { BusinessListing } from '../models/db.model';
|
||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
|
||||
@Controller('listings/business')
|
||||
export class BusinessListingsController {
|
||||
@@ -15,47 +16,52 @@ export class BusinessListingsController {
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Request() req, @Param('id') id: string): any {
|
||||
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
return await this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('favorites/all')
|
||||
async findFavorites(@Request() req): Promise<any> {
|
||||
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('user/:userid')
|
||||
findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||
async findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||
return await this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('find')
|
||||
find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||
async find(@Request() req, @Body() criteria: BusinessListingCriteria): Promise<any> {
|
||||
return await this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: BusinessListingCriteria): Promise<number> {
|
||||
return this.listingsService.getBusinessListingsCount(criteria);
|
||||
async findTotal(@Request() req, @Body() criteria: BusinessListingCriteria): Promise<number> {
|
||||
return await this.listingsService.getBusinessListingsCount(criteria, req.user as JwtUser);
|
||||
}
|
||||
// @UseGuards(OptionalJwtAuthGuard)
|
||||
// @Post('search')
|
||||
// search(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
// return this.listingsService.searchBusinessListings(criteria.prompt);
|
||||
// }
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post()
|
||||
create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return this.listingsService.createListing(listing);
|
||||
async create(@Body() listing: any) {
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Put()
|
||||
update(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return this.listingsService.updateBusinessListing(listing.id, listing);
|
||||
async update(@Body() listing: any) {
|
||||
return await this.listingsService.updateBusinessListing(listing.id, listing);
|
||||
}
|
||||
@Delete(':id')
|
||||
deleteById(@Param('id') id: string) {
|
||||
this.listingsService.deleteListing(id);
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Delete('listing/:id')
|
||||
async deleteById(@Param('id') id: string) {
|
||||
await this.listingsService.deleteListing(id);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates();
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('favorite/:id')
|
||||
async deleteFavorite(@Request() req, @Param('id') id: string) {
|
||||
await this.listingsService.deleteFavorite(id, req.user as JwtUser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { JwtAuthGuard } from '../jwt-auth/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
import { CommercialPropertyListing } from '../models/db.model';
|
||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model.js';
|
||||
import { CommercialPropertyService } from './commercial-property.service.js';
|
||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model';
|
||||
import { CommercialPropertyService } from './commercial-property.service';
|
||||
|
||||
@Controller('listings/commercialProperty')
|
||||
export class CommercialPropertyListingsController {
|
||||
@@ -17,41 +18,54 @@ export class CommercialPropertyListingsController {
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Request() req, @Param('id') id: string): any {
|
||||
return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
return await this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('favorites/all')
|
||||
async findFavorites(@Request() req): Promise<any> {
|
||||
return await this.listingsService.findFavoriteListings(req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('user/:email')
|
||||
findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||
async findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||
return await this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('find')
|
||||
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
|
||||
return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
return this.listingsService.getCommercialPropertiesCount(criteria);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates();
|
||||
async findTotal(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
return await this.listingsService.getCommercialPropertiesCount(criteria, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post()
|
||||
async create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Put()
|
||||
async update(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
|
||||
}
|
||||
@Delete(':id/:imagePath')
|
||||
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||
this.listingsService.deleteListing(id);
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Delete('listing/:id/:imagePath')
|
||||
async deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||
await this.listingsService.deleteListing(id);
|
||||
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||
}
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Delete('favorite/:id')
|
||||
async deleteFavorite(@Request() req, @Param('id') id: string) {
|
||||
await this.listingsService.deleteFavorite(id, req.user as JwtUser);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
||||
import { and, arrayContains, asc, count, desc, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { ZodError } from 'zod';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js';
|
||||
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
import { convertCommercialToDrizzleCommercial, convertDrizzleCommercialToCommercial, getDistanceQuery } from '../utils.js';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { commercials, PG_CONNECTION } from '../drizzle/schema';
|
||||
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 { getDistanceQuery } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
export class CommercialPropertyService {
|
||||
@@ -20,14 +20,14 @@ export class CommercialPropertyService {
|
||||
private fileService?: FileService,
|
||||
private geoService?: GeoService,
|
||||
) {}
|
||||
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
|
||||
private getWhereConditions(criteria: CommercialPropertyListingCriteria, user: JwtUser): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
|
||||
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);
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
|
||||
whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
@@ -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.state}`);
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
@@ -49,7 +49,10 @@ export class CommercialPropertyService {
|
||||
if (criteria.title) {
|
||||
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional')));
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
whereConditions.push(or(eq(commercials.email, user?.username), ne(commercials.draft, true)));
|
||||
}
|
||||
// whereConditions.push(and(eq(schema.users.customerType, 'professional')));
|
||||
return whereConditions;
|
||||
}
|
||||
// #### Find by criteria ########################################
|
||||
@@ -57,28 +60,46 @@ export class CommercialPropertyService {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
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, user);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
// Sortierung
|
||||
switch (criteria.sortBy) {
|
||||
case 'priceAsc':
|
||||
query.orderBy(asc(commercials.price));
|
||||
break;
|
||||
case 'priceDesc':
|
||||
query.orderBy(desc(commercials.price));
|
||||
break;
|
||||
case 'creationDateFirst':
|
||||
query.orderBy(asc(commercials.created));
|
||||
break;
|
||||
case 'creationDateLast':
|
||||
query.orderBy(desc(commercials.created));
|
||||
break;
|
||||
default:
|
||||
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
|
||||
break;
|
||||
}
|
||||
|
||||
// Paginierung
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const data = await query;
|
||||
const results = data.map(r => r.commercial).map(r => convertDrizzleCommercialToCommercial(r));
|
||||
const totalCount = await this.getCommercialPropertiesCount(criteria);
|
||||
const results = data.map(r => r.commercial);
|
||||
const totalCount = await this.getCommercialPropertiesCount(criteria, user);
|
||||
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<number> {
|
||||
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, user);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
@@ -91,18 +112,26 @@ export class CommercialPropertyService {
|
||||
|
||||
// #### Find by ID ########################################
|
||||
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
let result = await this.conn
|
||||
const conditions = [];
|
||||
if (!user?.roles?.includes('ADMIN') ?? false) {
|
||||
conditions.push(or(eq(commercials.email, user?.username), ne(commercials.draft, true)));
|
||||
}
|
||||
conditions.push(sql`${commercials.id} = ${id}`);
|
||||
const result = await this.conn
|
||||
.select()
|
||||
.from(commercials)
|
||||
.where(and(sql`${commercials.id} = ${id}`));
|
||||
result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
||||
return convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
|
||||
.where(and(...conditions));
|
||||
if (result.length > 0) {
|
||||
return result[0] as CommercialPropertyListing;
|
||||
} else {
|
||||
throw new BadRequestException(`No entry available for ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
// #### Find by User EMail ########################################
|
||||
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
||||
const conditions = [];
|
||||
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
|
||||
conditions.push(eq(commercials.email, email));
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||
conditions.push(ne(commercials.draft, true));
|
||||
}
|
||||
@@ -110,7 +139,15 @@ 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<CommercialPropertyListing[]> {
|
||||
const userFavorites = await this.conn
|
||||
.select()
|
||||
.from(commercials)
|
||||
.where(arrayContains(commercials.favoritesForUser, [user.username]));
|
||||
return userFavorites;
|
||||
}
|
||||
// #### Find by imagePath ########################################
|
||||
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
|
||||
@@ -118,24 +155,27 @@ 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<CommercialPropertyListing> {
|
||||
try {
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
data.updated = new Date();
|
||||
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
|
||||
const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
|
||||
CommercialPropertyListingSchema.parse(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 formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
const filteredErrors = error.errors
|
||||
.map(item => ({
|
||||
...item,
|
||||
field: item.path[0],
|
||||
}))
|
||||
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
|
||||
throw new BadRequestException(filteredErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -145,23 +185,25 @@ export class CommercialPropertyService {
|
||||
try {
|
||||
data.updated = new Date();
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
|
||||
CommercialPropertyListingSchema.parse(data);
|
||||
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
|
||||
let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
|
||||
const difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
|
||||
if (difference.length > 0) {
|
||||
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 formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
const filteredErrors = error.errors
|
||||
.map(item => ({
|
||||
...item,
|
||||
field: item.path[0],
|
||||
}))
|
||||
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
|
||||
throw new BadRequestException(filteredErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -186,14 +228,23 @@ export class CommercialPropertyService {
|
||||
async deleteListing(id: string): Promise<void> {
|
||||
await this.conn.delete(commercials).where(eq(commercials.id, id));
|
||||
}
|
||||
// #### DELETE Favorite ###################################
|
||||
async deleteFavorite(id: string, user: JwtUser): Promise<void> {
|
||||
await this.conn
|
||||
.update(commercials)
|
||||
.set({
|
||||
favoritesForUser: sql`array_remove(${commercials.favoritesForUser}, ${user.username})`,
|
||||
})
|
||||
.where(sql`${commercials.id} = ${id}`);
|
||||
}
|
||||
// ##############################################################
|
||||
// States
|
||||
// ##############################################################
|
||||
async getStates(): Promise<any[]> {
|
||||
return await this.conn
|
||||
.select({ state: commercials.state, count: sql<number>`count(${commercials.id})`.mapWith(Number) })
|
||||
.from(commercials)
|
||||
.groupBy(sql`${commercials.state}`)
|
||||
.orderBy(sql`count desc`);
|
||||
}
|
||||
// async getStates(): Promise<any[]> {
|
||||
// return await this.conn
|
||||
// .select({ state: commercials.state, count: sql<number>`count(${commercials.id})`.mapWith(Number) })
|
||||
// .from(commercials)
|
||||
// .groupBy(sql`${commercials.state}`)
|
||||
// .orderBy(sql`count desc`);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { BrokerListingsController } from './broker-listings.controller.js';
|
||||
import { BusinessListingsController } from './business-listings.controller.js';
|
||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { BrokerListingsController } from './broker-listings.controller';
|
||||
import { BusinessListingsController } from './business-listings.controller';
|
||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller';
|
||||
|
||||
import { GeoModule } from '../geo/geo.module.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { BusinessListingService } from './business-listing.service.js';
|
||||
import { CommercialPropertyService } from './commercial-property.service.js';
|
||||
import { UnknownListingsController } from './unknown-listings.controller.js';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
import { CommercialPropertyService } from './commercial-property.service';
|
||||
import { UnknownListingsController } from './unknown-listings.controller';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, AuthModule, GeoModule],
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { Controller, Inject } from '@nestjs/common';
|
||||
import { Controller, Get, Inject, Param, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
import { BusinessListingService } from './business-listing.service';
|
||||
import { CommercialPropertyService } from './commercial-property.service';
|
||||
|
||||
@Controller('listings/undefined')
|
||||
export class UnknownListingsController {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
private readonly businessListingsService: BusinessListingService,
|
||||
private readonly propertyListingsService: CommercialPropertyService,
|
||||
) {}
|
||||
|
||||
// @Get(':id')
|
||||
// async findById(@Param('id') id: string): Promise<any> {
|
||||
// const result = await this.listingsService.findById(id, businesses);
|
||||
// if (result) {
|
||||
// return result;
|
||||
// } else {
|
||||
// return await this.listingsService.findById(id, commercials);
|
||||
// }
|
||||
// }
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':id')
|
||||
async findById(@Request() req, @Param('id') id: string): Promise<any> {
|
||||
try {
|
||||
return await this.businessListingsService.findBusinessesById(id, req.user);
|
||||
} catch (error) {
|
||||
return await this.propertyListingsService.findCommercialPropertiesById(id, req.user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
bizmatch-server/src/log/log.controller.ts
Normal file
19
bizmatch-server/src/log/log.controller.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Body, Controller, Inject, Post, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
import { LogMessage } from '../models/main.model';
|
||||
@Controller('log')
|
||||
export class LogController {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post()
|
||||
log(@Request() req, @Body() message: LogMessage) {
|
||||
if (message.severity === 'info') {
|
||||
this.logger.info(message.text);
|
||||
} else {
|
||||
this.logger.error(message.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
bizmatch-server/src/log/log.module.ts
Normal file
7
bizmatch-server/src/log/log.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LogController } from './log.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [LogController],
|
||||
})
|
||||
export class LogModule {}
|
||||
14
bizmatch-server/src/mail/mail.config.ts
Normal file
14
bizmatch-server/src/mail/mail.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('mail', () => ({
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.AMAZON_USER,
|
||||
pass: process.env.AMAZON_PASSWORD,
|
||||
},
|
||||
defaults: {
|
||||
from: '"No Reply" <noreply@example.com>',
|
||||
},
|
||||
}));
|
||||
@@ -1,16 +1,32 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { ShareByEMail, User } from 'src/models/db.model';
|
||||
import { ErrorResponse, MailInfo } from '../models/main.model';
|
||||
import { MailService } from './mail.service.js';
|
||||
import { MailService } from './mail.service';
|
||||
|
||||
@Controller('mail')
|
||||
export class MailController {
|
||||
constructor(private mailService: MailService) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post()
|
||||
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
async sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
if (mailInfo.listing) {
|
||||
return this.mailService.sendInquiry(mailInfo);
|
||||
return await this.mailService.sendInquiry(mailInfo);
|
||||
} else {
|
||||
return this.mailService.sendRequest(mailInfo);
|
||||
return await this.mailService.sendRequest(mailInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('subscriptionConfirmation')
|
||||
async sendSubscriptionConfirmation(@Body() user: User): Promise<void | ErrorResponse> {
|
||||
return await this.mailService.sendSubscriptionConfirmation(user);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('send2Friend')
|
||||
async send2Friend(@Body() shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
|
||||
return await this.mailService.send2Friend(shareByEMail);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,75 @@
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||
import { Module } from '@nestjs/common';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoModule } from '../geo/geo.module.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { MailController } from './mail.controller.js';
|
||||
import { MailService } from './mail.service.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const user = process.env.amazon_user;
|
||||
const password = process.env.amazon_password;
|
||||
import { join } from 'path';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { MailController } from './mail.controller';
|
||||
import { MailService } from './mail.service';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DrizzleModule,
|
||||
UserModule,
|
||||
GeoModule,
|
||||
MailerModule.forRoot({
|
||||
transport: {
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
secure: false,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: 'AKIAU6GDWVAQ2QNFLNWN',
|
||||
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
// ConfigModule.forFeature(mailConfig),
|
||||
// MailerModule.forRoot({
|
||||
// transport: {
|
||||
// host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
// secure: false,
|
||||
// port: 587,
|
||||
// auth: {
|
||||
// user: user, //'AKIAU6GDWVAQ2QNFLNWN',
|
||||
// pass: password, //'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
// },
|
||||
// },
|
||||
// defaults: {
|
||||
// from: '"No Reply" <noreply@example.com>',
|
||||
// },
|
||||
// template: {
|
||||
// dir: join(__dirname, 'templates'),
|
||||
// adapter: new HandlebarsAdapter({
|
||||
// eq: function (a, b) {
|
||||
// return a === b;
|
||||
// },
|
||||
// }),
|
||||
// options: {
|
||||
// strict: true,
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
MailerModule.forRootAsync({
|
||||
useFactory: () => ({
|
||||
transport: {
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
secure: false,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.AMAZON_USER, //'AKIAU6GDWVAQ2QNFLNWN',
|
||||
pass: process.env.AMAZON_PASSWORD, //'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
from: '"No Reply" <noreply@example.com>',
|
||||
},
|
||||
template: {
|
||||
dir: join(__dirname, 'templates'),
|
||||
adapter: new HandlebarsAdapter(), // or new PugAdapter() or new EjsAdapter()
|
||||
options: {
|
||||
strict: true,
|
||||
defaults: {
|
||||
from: '"No Reply" <noreply@example.com>',
|
||||
},
|
||||
},
|
||||
template: {
|
||||
dir: join(__dirname, 'templates'),
|
||||
adapter: new HandlebarsAdapter({
|
||||
eq: function (a, b) {
|
||||
return a === b;
|
||||
},
|
||||
}),
|
||||
options: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
providers: [MailService, UserService, FileService, GeoService],
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { join } from 'path';
|
||||
import { ZodError } from 'zod';
|
||||
import { SenderSchema } from '../models/db.model.js';
|
||||
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
import { SenderSchema, ShareByEMail, ShareByEMailSchema, User } from '../models/db.model';
|
||||
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model';
|
||||
import { UserService } from '../user/user.service';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
@@ -18,7 +17,7 @@ export class MailService {
|
||||
|
||||
async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
const validatedSender = SenderSchema.parse(mailInfo.sender);
|
||||
SenderSchema.parse(mailInfo.sender);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
@@ -55,7 +54,7 @@ export class MailService {
|
||||
}
|
||||
async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
const validatedSender = SenderSchema.parse(mailInfo.sender);
|
||||
SenderSchema.parse(mailInfo.sender);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
@@ -81,4 +80,48 @@ export class MailService {
|
||||
},
|
||||
});
|
||||
}
|
||||
async sendSubscriptionConfirmation(user: User): Promise<void> {
|
||||
await this.mailerService.sendMail({
|
||||
to: user.email,
|
||||
from: `"Bizmatch Support Team" <info@bizmatch.net>`,
|
||||
subject: `Subscription Confirmation`,
|
||||
//template: './inquiry', // `.hbs` extension is appended automatically
|
||||
template: join(__dirname, '../..', 'mail/templates/subscriptionConfirmation.hbs'),
|
||||
context: {
|
||||
// ✏️ filling curly brackets with content
|
||||
firstname: user.firstname,
|
||||
lastname: user.lastname,
|
||||
subscriptionPlan: user.subscriptionPlan,
|
||||
},
|
||||
});
|
||||
}
|
||||
async send2Friend(shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
ShareByEMailSchema.parse(shareByEMail);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await this.mailerService.sendMail({
|
||||
to: shareByEMail.recipientEmail,
|
||||
from: `"Bizmatch.net" <info@bizmatch.net>`,
|
||||
subject: `${shareByEMail.type === 'business' ? 'Business' : 'Commercial Property'} For Sale: ${shareByEMail.listingTitle}`,
|
||||
//template: './inquiry', // `.hbs` extension is appended automatically
|
||||
template: join(__dirname, '../..', 'mail/templates/send2Friend.hbs'),
|
||||
context: {
|
||||
name: shareByEMail.yourName,
|
||||
email: shareByEMail.yourEmail,
|
||||
listingTitle: shareByEMail.listingTitle,
|
||||
url: shareByEMail.url,
|
||||
id: shareByEMail.id,
|
||||
type: shareByEMail.type,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
73
bizmatch-server/src/mail/templates/send2Friend.hbs
Normal file
73
bizmatch-server/src/mail/templates/send2Friend.hbs
Normal file
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notification</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
color: #333333;
|
||||
}
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.content p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.content .plan-info {
|
||||
font-weight: bold;
|
||||
color: #0056b3;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #888888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<h1>Notification</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>Your friend {{name}} ({{email}}) believed you might find this <b>{{#if (eq type "commercialProperty")}}Commercial Property{{else if (eq type "business")}}Business{{/if}} for sale listing </b> on <a href="{{url}}">bizmatch.net</a> interesting.</p>
|
||||
|
||||
<span class="info-value"><a href="{{url}}/listing/{{id}}">{{listingTitle}}</a></span>
|
||||
|
||||
<p>Bizmatch is one of the most reliable platforms for buying and selling businesses.</p>
|
||||
|
||||
<p>Best regards,</p>
|
||||
<p>The Bizmatch Support Team</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2024 Bizmatch. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,77 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Subscription Confirmation</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f4;
|
||||
color: #333333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
background-color: #ffffff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
text-align: center;
|
||||
border-bottom: 1px solid #dddddd;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
color: #333333;
|
||||
}
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.content p {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.content .plan-info {
|
||||
font-weight: bold;
|
||||
color: #0056b3;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #888888;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<h1>Subscription Confirmation</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Dear {{firstname}} {{lastname}},</p>
|
||||
|
||||
<p>Thank you for subscribing to our service! We are thrilled to have you on board.</p>
|
||||
|
||||
<p>Your subscription details are as follows:</p>
|
||||
|
||||
<p><span class="plan-info">{{#if (eq subscriptionPlan "professional")}}Professional Plan (CPA, Attorney, Title Company, Surveyor, Appraiser){{else if (eq subscriptionPlan "broker")}}Business Broker Plan{{/if}}</span></p>
|
||||
|
||||
<p>If you have any questions or need further assistance, please feel free to contact our support team at any time.</p>
|
||||
|
||||
<p>Thank you for choosing Bizmatch!</p>
|
||||
|
||||
<p>Best regards,</p>
|
||||
<p>The Bizmatch Support Team</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© 2024 Bizmatch. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +1,25 @@
|
||||
import { LoggerService } from '@nestjs/common';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import bodyParser from 'body-parser';
|
||||
import express from 'express';
|
||||
import { AppModule } from './app.module.js';
|
||||
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const server = express();
|
||||
server.set('trust proxy', true);
|
||||
const app = await NestFactory.create(AppModule);
|
||||
// const logger = app.get<Logger>(WINSTON_MODULE_NEST_PROVIDER);
|
||||
const logger = app.get<LoggerService>(WINSTON_MODULE_NEST_PROVIDER);
|
||||
app.useLogger(logger);
|
||||
app.use('/bizmatch/payment/webhook', bodyParser.raw({ type: 'application/json' }));
|
||||
app.setGlobalPrefix('bizmatch');
|
||||
|
||||
app.enableCors({
|
||||
origin: '*',
|
||||
//origin: 'http://localhost:4200', // Die URL Ihrer Angular-App
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization, x-hide-loading',
|
||||
});
|
||||
//origin: 'http://localhost:4200',
|
||||
await app.listen(3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -17,20 +17,25 @@ export interface UserData {
|
||||
hasCompanyLogo?: boolean;
|
||||
licensedIn?: string[];
|
||||
gender?: 'male' | 'female';
|
||||
customerType?: 'buyer' | 'broker' | 'professional';
|
||||
customerType?: 'buyer' | 'seller' | 'professional';
|
||||
customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
created?: Date;
|
||||
updated?: Date;
|
||||
}
|
||||
export type SortByOptions = 'priceAsc' | 'priceDesc' | 'creationDateFirst' | 'creationDateLast' | 'nameAsc' | 'nameDesc' | 'srAsc' | 'srDesc' | 'cfAsc' | 'cfDesc';
|
||||
export type SortByTypes = 'professional' | 'listing' | 'business' | 'commercial';
|
||||
export type Gender = 'male' | 'female';
|
||||
export type CustomerType = 'buyer' | 'professional';
|
||||
export type CustomerType = 'buyer' | 'seller' | 'professional';
|
||||
export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
export type ListingsCategory = 'commercialProperty' | 'business';
|
||||
|
||||
export const GenderEnum = z.enum(['male', 'female']);
|
||||
export const CustomerTypeEnum = z.enum(['buyer', 'professional']);
|
||||
export const CustomerTypeEnum = z.enum(['buyer', 'seller', 'professional']);
|
||||
export const SubscriptionTypeEnum = z.enum(['free', 'professional', 'broker']);
|
||||
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);
|
||||
export const ZodEventTypeEnum = z.enum(['view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact', 'favorite', 'emailus', 'pricing']);
|
||||
export type EventTypeEnum = z.infer<typeof ZodEventTypeEnum>;
|
||||
const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']);
|
||||
const TypeEnum = z.enum([
|
||||
'automotive',
|
||||
@@ -102,38 +107,61 @@ const USStates = z.enum([
|
||||
'WY',
|
||||
]);
|
||||
export const AreasServedSchema = z.object({
|
||||
county: z.string().nonempty('County is required'),
|
||||
state: z.string().nonempty('State is required'),
|
||||
county: z.string().optional().nullable(),
|
||||
state: z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine(val => val !== null && val !== '', {
|
||||
message: 'State is required',
|
||||
}),
|
||||
});
|
||||
|
||||
export const LicensedInSchema = z.object({
|
||||
registerNo: z.string().nonempty('Registration number is required'),
|
||||
state: z.string().nonempty('State is required'),
|
||||
state: z
|
||||
.string()
|
||||
.nullable()
|
||||
.refine(val => val !== null && val !== '', {
|
||||
message: 'State is required',
|
||||
}),
|
||||
registerNo: z.string().nonempty('License number is required'),
|
||||
});
|
||||
export const GeoSchema = z.object({
|
||||
city: z.string(),
|
||||
state: z.string().refine(val => USStates.safeParse(val).success, {
|
||||
message: 'Invalid state. Must be a valid 2-letter US state code.',
|
||||
}),
|
||||
latitude: z.number().refine(
|
||||
value => {
|
||||
return value >= -90 && value <= 90;
|
||||
},
|
||||
{
|
||||
message: 'Latitude muss zwischen -90 und 90 liegen',
|
||||
},
|
||||
),
|
||||
longitude: z.number().refine(
|
||||
value => {
|
||||
return value >= -180 && value <= 180;
|
||||
},
|
||||
{
|
||||
message: 'Longitude muss zwischen -180 und 180 liegen',
|
||||
},
|
||||
),
|
||||
});
|
||||
const phoneRegex = /^\(\d{3}\)\s\d{3}-\d{4}$/;
|
||||
|
||||
export const GeoSchema = z
|
||||
.object({
|
||||
name: z.string().optional().nullable(),
|
||||
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',
|
||||
},
|
||||
),
|
||||
county: z.string().optional().nullable(),
|
||||
housenumber: z.string().optional().nullable(),
|
||||
street: z.string().optional().nullable(),
|
||||
zipCode: z.number().optional().nullable(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (!data.name && !data.county) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'You need to select either a city or a county',
|
||||
path: ['name'],
|
||||
});
|
||||
}
|
||||
});
|
||||
const phoneRegex = /^(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;
|
||||
export const UserSchema = z
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
@@ -145,7 +173,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(),
|
||||
@@ -156,6 +184,8 @@ export const UserSchema = z
|
||||
customerSubType: CustomerSubTypeEnum.optional().nullable(),
|
||||
created: z.date().optional().nullable(),
|
||||
updated: z.date().optional().nullable(),
|
||||
subscriptionId: z.string().optional().nullable(),
|
||||
subscriptionPlan: SubscriptionTypeEnum.optional().nullable(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.customerType === 'professional') {
|
||||
@@ -199,7 +229,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',
|
||||
@@ -244,7 +274,7 @@ export const BusinessListingSchema = z.object({
|
||||
established: z.number().int().min(1800).max(2030).optional().nullable(),
|
||||
internalListingNumber: z.number().int().positive().optional().nullable(),
|
||||
reasonForSale: z.string().min(5).optional().nullable(),
|
||||
brokerLicencing: z.string().min(5).optional().nullable(),
|
||||
brokerLicencing: z.string().optional().nullable(),
|
||||
internals: z.string().min(5).optional().nullable(),
|
||||
imageName: z.string().optional().nullable(),
|
||||
created: z.date(),
|
||||
@@ -288,3 +318,30 @@ export const SenderSchema = z.object({
|
||||
comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }),
|
||||
});
|
||||
export type Sender = z.infer<typeof SenderSchema>;
|
||||
export const ShareByEMailSchema = z.object({
|
||||
yourName: z.string().min(6, { message: 'Name must be at least 6 characters long' }),
|
||||
recipientEmail: z.string().email({ message: 'Invalid email address' }),
|
||||
yourEmail: z.string().email({ message: 'Invalid email address' }),
|
||||
listingTitle: z.string().optional().nullable(),
|
||||
url: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
|
||||
id: z.string().optional().nullable(),
|
||||
type: ListingsCategoryEnum,
|
||||
});
|
||||
export type ShareByEMail = z.infer<typeof ShareByEMailSchema>;
|
||||
|
||||
export const ListingEventSchema = z.object({
|
||||
id: z.string().uuid(), // UUID für das Event
|
||||
listingId: z.string().uuid().optional().nullable(), // UUID für das Listing
|
||||
email: z.string().email().optional().nullable(), // EMail des den Benutzer, optional, wenn kein Benutzer eingeloggt ist
|
||||
eventType: ZodEventTypeEnum, // Die Event-Typen
|
||||
eventTimestamp: z.string().datetime().or(z.date()), // Der Zeitstempel des Events, kann ein String im ISO-Format oder ein Date-Objekt sein
|
||||
userIp: z.string().max(45).optional().nullable(), // IP-Adresse des Benutzers, optional
|
||||
userAgent: z.string().max(255).optional().nullable(), // User-Agent des Benutzers, optional
|
||||
locationCountry: z.string().max(100).optional().nullable(), // Land, optional
|
||||
locationCity: z.string().max(100).optional().nullable(), // Stadt, optional
|
||||
locationLat: z.string().max(20).optional().nullable(), // Latitude, als String
|
||||
locationLng: z.string().max(20).optional().nullable(), // Longitude, als String
|
||||
referrer: z.string().max(255).optional().nullable(), // Referrer URL, optional
|
||||
additionalData: z.record(z.any()).optional().nullable(), // JSON für zusätzliche Daten, z.B. soziale Medien, optional
|
||||
});
|
||||
export type ListingEvent = z.infer<typeof ListingEventSchema>;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
|
||||
import Stripe from 'stripe';
|
||||
import { BusinessListing, CommercialPropertyListing, Sender, SortByOptions, SortByTypes, User } from './db.model';
|
||||
import { State } from './server.model';
|
||||
|
||||
export interface StatesResult {
|
||||
state: string;
|
||||
@@ -9,6 +11,12 @@ export interface KeyValue {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
export interface KeyValueAsSortBy {
|
||||
name: string;
|
||||
value: SortByOptions;
|
||||
type?: SortByTypes;
|
||||
selectName?: string;
|
||||
}
|
||||
export interface KeyValueRatio {
|
||||
label: string;
|
||||
value: number;
|
||||
@@ -59,8 +67,9 @@ export interface ListCriteria {
|
||||
page: number;
|
||||
types: string[];
|
||||
state: string;
|
||||
city: string;
|
||||
city: GeoResult;
|
||||
prompt: string;
|
||||
sortBy: SortByOptions;
|
||||
searchType: 'exact' | 'radius';
|
||||
// radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500';
|
||||
radius: number;
|
||||
@@ -91,8 +100,7 @@ export interface CommercialPropertyListingCriteria extends ListCriteria {
|
||||
criteriaType: 'commercialPropertyListings';
|
||||
}
|
||||
export interface UserListingCriteria extends ListCriteria {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
brokerName: string;
|
||||
companyName: string;
|
||||
counties: string[];
|
||||
criteriaType: 'brokerListings';
|
||||
@@ -112,6 +120,7 @@ export interface KeycloakUser {
|
||||
requiredActions?: any[];
|
||||
notBefore?: number;
|
||||
access?: Access;
|
||||
attributes?: Attributes;
|
||||
}
|
||||
export interface JwtUser {
|
||||
userId: string;
|
||||
@@ -120,6 +129,10 @@ export interface JwtUser {
|
||||
lastname: string;
|
||||
roles: string[];
|
||||
}
|
||||
interface Attributes {
|
||||
[key: string]: any;
|
||||
priceID: any;
|
||||
}
|
||||
export interface Access {
|
||||
manageGroupMembership: boolean;
|
||||
view: boolean;
|
||||
@@ -166,6 +179,7 @@ export interface JwtToken {
|
||||
family_name: string;
|
||||
email: string;
|
||||
user_id: string;
|
||||
price_id: string;
|
||||
}
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
@@ -224,24 +238,46 @@ export interface UploadParams {
|
||||
}
|
||||
export interface GeoResult {
|
||||
id: number;
|
||||
city: string;
|
||||
name: string;
|
||||
street?: string;
|
||||
housenumber?: string;
|
||||
county?: string;
|
||||
zipCode?: number;
|
||||
state: string;
|
||||
// state_code: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
export interface CityAndStateResult {
|
||||
interface CityResult {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
state: string;
|
||||
type: 'city';
|
||||
content: GeoResult;
|
||||
}
|
||||
|
||||
interface StateResult {
|
||||
id: number;
|
||||
type: 'state';
|
||||
content: State;
|
||||
}
|
||||
export type CityAndStateResult = CityResult | StateResult;
|
||||
export interface CountyResult {
|
||||
id: number;
|
||||
name: string;
|
||||
state: string;
|
||||
state_code: string;
|
||||
}
|
||||
export interface LogMessage {
|
||||
severity: 'error' | 'info';
|
||||
text: string;
|
||||
}
|
||||
export interface ModalResult {
|
||||
accepted: boolean;
|
||||
criteria?: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
|
||||
}
|
||||
export interface Checkout {
|
||||
priceId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
}
|
||||
export function isEmpty(value: any): boolean {
|
||||
// Check for undefined or null
|
||||
if (value === undefined || value === null) {
|
||||
@@ -281,7 +317,7 @@ export interface ValidationMessage {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
export function createDefaultUser(email: string, firstname: string, lastname: string): User {
|
||||
export function createDefaultUser(email: string, firstname: string, lastname: string, subscriptionPlan: 'professional' | 'broker'): User {
|
||||
return {
|
||||
id: undefined,
|
||||
email,
|
||||
@@ -292,7 +328,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,
|
||||
@@ -303,6 +339,8 @@ export function createDefaultUser(email: string, firstname: string, lastname: st
|
||||
customerSubType: null,
|
||||
created: new Date(),
|
||||
updated: new Date(),
|
||||
subscriptionId: null,
|
||||
subscriptionPlan: subscriptionPlan,
|
||||
};
|
||||
}
|
||||
export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
|
||||
@@ -352,3 +390,25 @@ export function createDefaultBusinessListing(): BusinessListing {
|
||||
listingsCategory: 'business',
|
||||
};
|
||||
}
|
||||
export type StripeSubscription = Stripe.Subscription;
|
||||
export type StripeUser = Stripe.Customer;
|
||||
export type IpInfo = {
|
||||
ip: string;
|
||||
city: string;
|
||||
region: string;
|
||||
country: string;
|
||||
loc: string; // Coordinates in "latitude,longitude" format
|
||||
org: string;
|
||||
postal: string;
|
||||
timezone: string;
|
||||
};
|
||||
export interface CombinedUser {
|
||||
keycloakUser?: KeycloakUser;
|
||||
appUser?: User;
|
||||
stripeUser?: StripeUser;
|
||||
stripeSubscription?: StripeSubscription;
|
||||
}
|
||||
export interface RealIpInfo {
|
||||
ip: string;
|
||||
countryCode?: string;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
77
bizmatch-server/src/payment/payment.controller.ts
Normal file
77
bizmatch-server/src/payment/payment.controller.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Param, Post, Req, Res, UseGuards } from '@nestjs/common';
|
||||
import { Request, Response } from 'express';
|
||||
import { AdminAuthGuard } from 'src/jwt-auth/admin-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { Checkout } from 'src/models/main.model';
|
||||
import Stripe from 'stripe';
|
||||
import { PaymentService } from './payment.service';
|
||||
|
||||
@Controller('payment')
|
||||
export class PaymentController {
|
||||
constructor(private readonly paymentService: PaymentService) {}
|
||||
|
||||
// @Post()
|
||||
// async createSubscription(@Body() subscriptionData: any) {
|
||||
// return this.paymentService.createSubscription(subscriptionData);
|
||||
// }
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('user/all')
|
||||
async getAllStripeCustomer(): Promise<Stripe.Customer[]> {
|
||||
return await this.paymentService.getAllStripeCustomer();
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('subscription/all')
|
||||
async getAllStripeSubscriptions(): Promise<Stripe.Subscription[]> {
|
||||
return await this.paymentService.getAllStripeSubscriptions();
|
||||
}
|
||||
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('paymentmethod/:email')
|
||||
async getStripePaymentMethods(@Param('email') email: string): Promise<Stripe.PaymentMethod[]> {
|
||||
return await this.paymentService.getStripePaymentMethod(email);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('create-checkout-session')
|
||||
async createCheckoutSession(@Body() checkout: Checkout) {
|
||||
return await this.paymentService.createCheckoutSession(checkout);
|
||||
}
|
||||
@Post('webhook')
|
||||
async handleWebhook(@Req() req: Request, @Res() res: Response): Promise<void> {
|
||||
const signature = req.headers['stripe-signature'] as string;
|
||||
|
||||
try {
|
||||
// Konvertieren Sie den req.body Buffer in einen lesbaren String
|
||||
const payload = req.body instanceof Buffer ? req.body.toString('utf8') : req.body;
|
||||
const event = await this.paymentService.constructEvent(payload, signature);
|
||||
// const event = await this.paymentService.constructEvent(req.body, signature);
|
||||
|
||||
if (event.type === 'checkout.session.completed') {
|
||||
await this.paymentService.handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session);
|
||||
}
|
||||
|
||||
res.status(200).send('Webhook received');
|
||||
} catch (error) {
|
||||
console.error(`Webhook Error: ${error.message}`);
|
||||
throw new HttpException('Webhook Error', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('subscriptions/:email')
|
||||
async findSubscriptionsById(@Param('email') email: string): Promise<any> {
|
||||
return await this.paymentService.getSubscription(email);
|
||||
}
|
||||
/**
|
||||
* Endpoint zum Löschen eines Stripe-Kunden.
|
||||
* Beispiel: DELETE /stripe/customer/cus_12345
|
||||
*/
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Delete('customer/:id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async deleteCustomer(@Param('id') customerId: string): Promise<void> {
|
||||
await this.paymentService.deleteCustomerCompletely(customerId);
|
||||
}
|
||||
}
|
||||
19
bizmatch-server/src/payment/payment.module.ts
Normal file
19
bizmatch-server/src/payment/payment.module.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { MailModule } from '../mail/mail.module';
|
||||
import { MailService } from '../mail/mail.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { PaymentController } from './payment.controller';
|
||||
import { PaymentService } from './payment.service';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, UserModule, MailModule, AuthModule],
|
||||
providers: [PaymentService, UserService, MailService, FileService, GeoService, AuthService],
|
||||
controllers: [PaymentController],
|
||||
})
|
||||
export class PaymentModule {}
|
||||
218
bizmatch-server/src/payment/payment.service.ts
Normal file
218
bizmatch-server/src/payment/payment.service.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import Stripe from 'stripe';
|
||||
import { Logger } from 'winston';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { PG_CONNECTION } from '../drizzle/schema';
|
||||
import { MailService } from '../mail/mail.service';
|
||||
import { Checkout } from '../models/main.model';
|
||||
import { UserService } from '../user/user.service';
|
||||
export interface BillingAddress {
|
||||
country: string;
|
||||
state: string;
|
||||
}
|
||||
@Injectable()
|
||||
export class PaymentService {
|
||||
private stripe: Stripe;
|
||||
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private readonly userService: UserService,
|
||||
private readonly mailService: MailService,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: '2024-06-20',
|
||||
});
|
||||
}
|
||||
async createCheckoutSession(checkout: Checkout) {
|
||||
try {
|
||||
let customerId;
|
||||
const existingCustomers = await this.stripe.customers.list({
|
||||
email: checkout.email,
|
||||
limit: 1,
|
||||
});
|
||||
if (existingCustomers.data.length > 0) {
|
||||
// Kunde existiert
|
||||
customerId = existingCustomers.data[0].id;
|
||||
} else {
|
||||
// Kunde existiert nicht, neuen Kunden erstellen
|
||||
const newCustomer = await this.stripe.customers.create({
|
||||
email: checkout.email,
|
||||
name: checkout.name,
|
||||
shipping: {
|
||||
name: checkout.name,
|
||||
address: {
|
||||
city: '',
|
||||
state: '',
|
||||
country: 'US',
|
||||
},
|
||||
},
|
||||
});
|
||||
customerId = newCustomer.id;
|
||||
}
|
||||
const price = await this.stripe.prices.retrieve(checkout.priceId);
|
||||
if (price.product) {
|
||||
const product = await this.stripe.products.retrieve(price.product as string);
|
||||
const session = await this.stripe.checkout.sessions.create({
|
||||
mode: 'subscription',
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{
|
||||
price: checkout.priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${process.env.WEB_HOST}/success`,
|
||||
cancel_url: `${process.env.WEB_HOST}/pricing`,
|
||||
customer: customerId,
|
||||
shipping_address_collection: {
|
||||
allowed_countries: ['US'],
|
||||
},
|
||||
client_reference_id: btoa(checkout.name),
|
||||
locale: 'en',
|
||||
subscription_data: {
|
||||
trial_end: Math.floor(new Date().setMonth(new Date().getMonth() + 3) / 1000),
|
||||
metadata: { plan: product.name },
|
||||
},
|
||||
});
|
||||
return session;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
throw new BadRequestException(`error during checkout: ${e}`);
|
||||
}
|
||||
}
|
||||
async constructEvent(body: string | Buffer, signature: string) {
|
||||
return this.stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
|
||||
}
|
||||
async handleCheckoutSessionCompleted(session: Stripe.Checkout.Session): Promise<void> {
|
||||
try {
|
||||
const keycloakUsers = await this.authService.getUsers();
|
||||
const keycloakUser = keycloakUsers.find(u => u.email === session.customer_details.email);
|
||||
const user = await this.userService.getUserByMail(session.customer_details.email, {
|
||||
userId: keycloakUser.id,
|
||||
firstname: keycloakUser.firstName,
|
||||
lastname: keycloakUser.lastName,
|
||||
username: keycloakUser.email,
|
||||
roles: [],
|
||||
});
|
||||
user.subscriptionId = session.subscription as string;
|
||||
const subscription = await this.stripe.subscriptions.retrieve(user.subscriptionId);
|
||||
user.customerType = 'professional';
|
||||
if (subscription.metadata['plan'] === 'Broker Plan') {
|
||||
user.customerSubType = 'broker';
|
||||
}
|
||||
user.subscriptionPlan = subscription.metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker';
|
||||
await this.userService.saveUser(user, false);
|
||||
await this.mailService.sendSubscriptionConfirmation(user);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
async getSubscription(email: string): Promise<Stripe.Subscription[]> {
|
||||
const existingCustomers = await this.stripe.customers.list({
|
||||
email: email,
|
||||
limit: 1,
|
||||
});
|
||||
if (existingCustomers.data.length > 0) {
|
||||
const subscriptions = await this.stripe.subscriptions.list({
|
||||
customer: existingCustomers.data[0].id,
|
||||
status: 'all', // Optional: Gibt Abos in allen Status zurück, wie 'active', 'canceled', etc.
|
||||
limit: 20, // Optional: Begrenze die Anzahl der zurückgegebenen Abonnements
|
||||
});
|
||||
return subscriptions.data.filter(s => s.status === 'active' || s.status === 'trialing');
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Ruft alle Stripe-Kunden ab, indem die Paginierung gehandhabt wird.
|
||||
* @returns Ein Array von Stripe.Customer Objekten.
|
||||
*/
|
||||
async getAllStripeCustomer(): Promise<Stripe.Customer[]> {
|
||||
const allCustomers: Stripe.Customer[] = [];
|
||||
let hasMore = true;
|
||||
let startingAfter: string | undefined = undefined;
|
||||
|
||||
try {
|
||||
while (hasMore) {
|
||||
const response = await this.stripe.customers.list({
|
||||
limit: 100, // Maximale Anzahl pro Anfrage
|
||||
starting_after: startingAfter,
|
||||
});
|
||||
|
||||
allCustomers.push(...response.data);
|
||||
hasMore = response.has_more;
|
||||
|
||||
if (hasMore && response.data.length > 0) {
|
||||
startingAfter = response.data[response.data.length - 1].id;
|
||||
}
|
||||
}
|
||||
|
||||
return allCustomers;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Stripe-Kunden:', error);
|
||||
throw new Error('Kunden konnten nicht abgerufen werden.');
|
||||
}
|
||||
}
|
||||
async getAllStripeSubscriptions(): Promise<Stripe.Subscription[]> {
|
||||
const allSubscriptions: Stripe.Subscription[] = [];
|
||||
const response = await this.stripe.subscriptions.list({
|
||||
limit: 100,
|
||||
});
|
||||
allSubscriptions.push(...response.data);
|
||||
return allSubscriptions;
|
||||
}
|
||||
async getStripePaymentMethod(email: string): Promise<Stripe.PaymentMethod[]> {
|
||||
const existingCustomers = await this.stripe.customers.list({
|
||||
email: email,
|
||||
limit: 1,
|
||||
});
|
||||
const allPayments: Stripe.PaymentMethod[] = [];
|
||||
if (existingCustomers.data.length > 0) {
|
||||
const response = await this.stripe.paymentMethods.list({
|
||||
customer: existingCustomers.data[0].id,
|
||||
limit: 10,
|
||||
});
|
||||
allPayments.push(...response.data);
|
||||
}
|
||||
return allPayments;
|
||||
}
|
||||
async deleteCustomerCompletely(customerId: string): Promise<void> {
|
||||
try {
|
||||
// 1. Abonnements kündigen und löschen
|
||||
const subscriptions = await this.stripe.subscriptions.list({
|
||||
customer: customerId,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
for (const subscription of subscriptions.data) {
|
||||
await this.stripe.subscriptions.cancel(subscription.id);
|
||||
this.logger.info(`Abonnement ${subscription.id} gelöscht.`);
|
||||
}
|
||||
|
||||
// 2. Zahlungsmethoden entfernen
|
||||
const paymentMethods = await this.stripe.paymentMethods.list({
|
||||
customer: customerId,
|
||||
type: 'card',
|
||||
});
|
||||
|
||||
for (const paymentMethod of paymentMethods.data) {
|
||||
await this.stripe.paymentMethods.detach(paymentMethod.id);
|
||||
this.logger.info(`Zahlungsmethode ${paymentMethod.id} entfernt.`);
|
||||
}
|
||||
|
||||
// 4. Kunden löschen
|
||||
await this.stripe.customers.del(customerId);
|
||||
this.logger.info(`Kunde ${customerId} erfolgreich gelöscht.`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Fehler beim Löschen des Kunden ${customerId}:`, error);
|
||||
throw new InternalServerErrorException('Fehler beim Löschen des Stripe-Kunden.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,42 @@
|
||||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { getRealIpInfo } from 'src/utils/ip.util';
|
||||
|
||||
@Injectable()
|
||||
export class RequestDurationMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger(RequestDurationMiddleware.name);
|
||||
|
||||
constructor(private readonly cls: ClsService) {}
|
||||
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const start = Date.now();
|
||||
res.on('finish', () => {
|
||||
// const duration = Date.now() - start;
|
||||
// this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
|
||||
const duration = Date.now() - start;
|
||||
let logMessage = `${req.method} ${req.url} - ${duration}ms`;
|
||||
const { ip, countryCode } = getRealIpInfo(req);
|
||||
|
||||
if (req.method === 'POST' || req.method === 'PUT') {
|
||||
const body = JSON.stringify(req.body);
|
||||
logMessage += ` - Body: ${body}`;
|
||||
}
|
||||
// Setze die IP-Adresse und den Ländercode im CLS-Kontext
|
||||
try {
|
||||
this.cls.set('ip', ip);
|
||||
this.cls.set('countryCode', countryCode);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to set CLS context', error);
|
||||
}
|
||||
|
||||
// const start = Date.now();
|
||||
|
||||
// this.logger.log(`Entering ${req.method} ${req.originalUrl} from ${ip}`);
|
||||
|
||||
// res.on('finish', () => {
|
||||
// const duration = Date.now() - start;
|
||||
// const userEmail = this.cls.get('userEmail') || 'unknown';
|
||||
// let logMessage = `${req.method} ${req.originalUrl} - ${duration}ms - IP: ${ip} - User: ${userEmail}`;
|
||||
|
||||
// if (req.method === 'POST' || req.method === 'PUT') {
|
||||
// const body = JSON.stringify(req.body);
|
||||
// logMessage += ` - Incoming Body: ${body}`;
|
||||
// }
|
||||
|
||||
// this.logger.log(logMessage);
|
||||
// });
|
||||
|
||||
this.logger.log(logMessage);
|
||||
});
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { SelectOptionsService } from './select-options.service.js';
|
||||
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||
import { OptionalJwtAuthGuard } from 'src/jwt-auth/optional-jwt-auth.guard';
|
||||
import { SelectOptionsService } from './select-options.service';
|
||||
|
||||
@Controller('select-options')
|
||||
export class SelectOptionsController {
|
||||
constructor(private selectOptionsService: SelectOptionsService) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get()
|
||||
getSelectOption(): any {
|
||||
return {
|
||||
@@ -15,6 +18,7 @@ export class SelectOptionsController {
|
||||
typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty,
|
||||
customerSubTypes: this.selectOptionsService.customerSubTypes,
|
||||
distances: this.selectOptionsService.distances,
|
||||
sortByOptions: this.selectOptionsService.sortByOptions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SelectOptionsController } from './select-options.controller.js';
|
||||
import { SelectOptionsService } from './select-options.service.js';
|
||||
import { SelectOptionsController } from './select-options.controller';
|
||||
import { SelectOptionsService } from './select-options.service';
|
||||
|
||||
@Module({
|
||||
controllers: [SelectOptionsController],
|
||||
providers: [SelectOptionsService]
|
||||
})
|
||||
controllers: [SelectOptionsController],
|
||||
providers: [SelectOptionsService],
|
||||
})
|
||||
export class SelectOptionsModule {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ImageType, KeyValue, KeyValueStyle } from '../models/main.model.js';
|
||||
import { ImageType, KeyValue, KeyValueAsSortBy, KeyValueStyle } from '../models/main.model';
|
||||
|
||||
@Injectable()
|
||||
export class SelectOptionsService {
|
||||
@@ -35,7 +35,19 @@ export class SelectOptionsService {
|
||||
{ name: '$1M', value: '1000000' },
|
||||
{ name: '$5M', value: '5000000' },
|
||||
];
|
||||
|
||||
public sortByOptions: Array<KeyValueAsSortBy> = [
|
||||
{ name: 'Price Asc', value: 'priceAsc', type: 'listing' },
|
||||
{ name: 'Price Desc', value: 'priceDesc', type: 'listing' },
|
||||
{ name: 'Sales Revenue Asc', value: 'srAsc', type: 'business' },
|
||||
{ name: 'Sales Revenue Desc', value: 'srDesc', type: 'business' },
|
||||
{ name: 'Cash Flow Asc', value: 'cfAsc', type: 'business' },
|
||||
{ name: 'Cash Flow Desc', value: 'cfDesc', type: 'business' },
|
||||
{ name: 'Creation Date First', value: 'creationDateFirst', type: 'listing' },
|
||||
{ name: 'Creation Date Last', value: 'creationDateLast', type: 'listing' },
|
||||
{ name: 'Name Asc', value: 'nameAsc', type: 'professional' },
|
||||
{ name: 'Name Desc', value: 'nameDesc', type: 'professional' },
|
||||
{ name: 'Sort', value: null, selectName: 'Default Sorting' },
|
||||
];
|
||||
public distances: Array<KeyValue> = [
|
||||
{ name: '5 miles', value: '5' },
|
||||
{ name: '20 miles', value: '20' },
|
||||
@@ -52,6 +64,7 @@ export class SelectOptionsService {
|
||||
];
|
||||
public customerTypes: Array<KeyValue> = [
|
||||
{ name: 'Buyer', value: 'buyer' },
|
||||
{ name: 'Commercial Property Seller', value: 'seller' },
|
||||
{ name: 'Professional', value: 'professional' },
|
||||
];
|
||||
public customerSubTypes: Array<KeyValue> = [
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { BadRequestException, Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { AdminAuthGuard } from 'src/jwt-auth/admin-auth.guard';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { ZodError } from 'zod';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { JwtAuthGuard } from '../jwt-auth/jwt-auth.guard';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard';
|
||||
import { User } from '../models/db.model';
|
||||
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model.js';
|
||||
import { UserService } from './user.service.js';
|
||||
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
@@ -14,52 +17,70 @@ export class UserController {
|
||||
private fileService: FileService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get()
|
||||
findByMail(@Request() req, @Query('mail') mail: string): any {
|
||||
this.logger.info(`Searching for user with EMail: ${mail}`);
|
||||
const user = this.userService.getUserByMail(mail, req.user as JwtUser);
|
||||
this.logger.info(`Found user: ${JSON.stringify(user)}`);
|
||||
async findByMail(@Request() req, @Query('mail') mail: string): Promise<User> {
|
||||
const user = await this.userService.getUserByMail(mail, req.user as JwtUser);
|
||||
return user;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Param('id') id: string): any {
|
||||
this.logger.info(`Searching for user with ID: ${id}`);
|
||||
const user = this.userService.getUserById(id);
|
||||
this.logger.info(`Found user: ${JSON.stringify(user)}`);
|
||||
async findById(@Param('id') id: string): Promise<User> {
|
||||
const user = await this.userService.getUserById(id);
|
||||
return user;
|
||||
}
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Get('user/all')
|
||||
async getAllUser(): Promise<User[]> {
|
||||
return await this.userService.getAllUser();
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post()
|
||||
save(@Body() user: any): Promise<User> {
|
||||
this.logger.info(`Saving user: ${JSON.stringify(user)}`);
|
||||
const savedUser = this.userService.saveUser(user);
|
||||
this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`);
|
||||
async save(@Body() user: any): Promise<User> {
|
||||
try {
|
||||
const savedUser = await this.userService.saveUser(user);
|
||||
return savedUser;
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const filteredErrors = error.errors
|
||||
.map(item => ({
|
||||
...item,
|
||||
field: item.path[0],
|
||||
}))
|
||||
.filter((item, index, self) => index === self.findIndex(t => t.path[0] === item.path[0]));
|
||||
throw new BadRequestException(filteredErrors);
|
||||
}
|
||||
throw error; // Andere Fehler einfach durchreichen
|
||||
}
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('guaranteed')
|
||||
async saveGuaranteed(@Body() user: any): Promise<User> {
|
||||
const savedUser = await this.userService.saveUser(user, false);
|
||||
return savedUser;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('search')
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
|
||||
const foundUsers = this.userService.searchUserListings(criteria);
|
||||
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
|
||||
async find(@Body() criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> {
|
||||
const foundUsers = await this.userService.searchUserListings(criteria);
|
||||
return foundUsers;
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: UserListingCriteria): Promise<number> {
|
||||
return this.userService.getUserListingsCount(criteria);
|
||||
}
|
||||
@Get('states/all')
|
||||
async getStates(): Promise<any[]> {
|
||||
this.logger.info(`Getting all states for users`);
|
||||
const result = await this.userService.getStates();
|
||||
this.logger.info(`Found ${result.length} entries`);
|
||||
return result;
|
||||
async findTotal(@Body() criteria: UserListingCriteria): Promise<number> {
|
||||
return await this.userService.getUserListingsCount(criteria);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('subscriptions/:id')
|
||||
async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> {
|
||||
const subscriptions = this.fileService.getSubscriptions();
|
||||
const subscriptions = [];
|
||||
const user = await this.userService.getUserById(id);
|
||||
subscriptions.forEach(s => {
|
||||
s.userId = user.id;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoModule } from '../geo/geo.module.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { UserController } from './user.controller.js';
|
||||
import { UserService } from './user.service.js';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module';
|
||||
import { FileService } from '../file/file.service';
|
||||
import { GeoModule } from '../geo/geo.module';
|
||||
import { GeoService } from '../geo/geo.service';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, GeoModule],
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, asc, count, desc, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { ZodError } from 'zod';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { User, UserSchema } from '../models/db.model.js';
|
||||
import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
|
||||
import { convertDrizzleUserToUser, convertUserToDrizzleUser, getDistanceQuery } from '../utils.js';
|
||||
import * as schema from '../drizzle/schema';
|
||||
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema';
|
||||
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 { DrizzleUser, getDistanceQuery, splitName } from '../utils';
|
||||
|
||||
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
|
||||
@Injectable()
|
||||
@@ -26,10 +25,10 @@ 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}%`));
|
||||
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);
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
|
||||
whereConditions.push(sql`${getDistanceQuery(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
@@ -37,12 +36,9 @@ export class UserService {
|
||||
whereConditions.push(inArray(schema.users.customerSubType, criteria.types as CustomerSubType[]));
|
||||
}
|
||||
|
||||
if (criteria.firstname) {
|
||||
whereConditions.push(ilike(schema.users.firstname, `%${criteria.firstname}%`));
|
||||
}
|
||||
|
||||
if (criteria.lastname) {
|
||||
whereConditions.push(ilike(schema.users.lastname, `%${criteria.lastname}%`));
|
||||
if (criteria.brokerName) {
|
||||
const { firstname, lastname } = splitName(criteria.brokerName);
|
||||
whereConditions.push(or(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
|
||||
}
|
||||
|
||||
if (criteria.companyName) {
|
||||
@@ -58,7 +54,7 @@ export class UserService {
|
||||
}
|
||||
return whereConditions;
|
||||
}
|
||||
async searchUserListings(criteria: UserListingCriteria) {
|
||||
async searchUserListings(criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn.select().from(schema.users);
|
||||
@@ -68,12 +64,23 @@ export class UserService {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
|
||||
// Sortierung
|
||||
switch (criteria.sortBy) {
|
||||
case 'nameAsc':
|
||||
query.orderBy(asc(schema.users.lastname));
|
||||
break;
|
||||
case 'nameDesc':
|
||||
query.orderBy(desc(schema.users.lastname));
|
||||
break;
|
||||
default:
|
||||
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
|
||||
break;
|
||||
}
|
||||
// Paginierung
|
||||
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 {
|
||||
@@ -99,14 +106,14 @@ export class UserService {
|
||||
.from(schema.users)
|
||||
.where(sql`email = ${email}`)) as User[];
|
||||
if (users.length === 0) {
|
||||
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname) };
|
||||
const u = await this.saveUser(user);
|
||||
return convertDrizzleUserToUser(u);
|
||||
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname, null) };
|
||||
const u = await this.saveUser(user, false);
|
||||
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) {
|
||||
@@ -118,9 +125,13 @@ 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): Promise<User> {
|
||||
async getAllUser() {
|
||||
const users = await this.conn.select().from(schema.users);
|
||||
return users;
|
||||
}
|
||||
async saveUser(user: User, processValidation = true): Promise<User> {
|
||||
try {
|
||||
user.updated = new Date();
|
||||
if (user.id) {
|
||||
@@ -128,29 +139,21 @@ export class UserService {
|
||||
} else {
|
||||
user.created = new Date();
|
||||
}
|
||||
const validatedUser = UserSchema.parse(user);
|
||||
const drizzleUser = convertUserToDrizzleUser(validatedUser);
|
||||
let validatedUser = user;
|
||||
if (processValidation) {
|
||||
validatedUser = UserSchema.parse(user);
|
||||
}
|
||||
//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) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async getStates(): Promise<any[]> {
|
||||
const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`;
|
||||
const result = await this.conn.execute(query);
|
||||
return result.rows;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { businesses, commercials, users } from './drizzle/schema.js';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from './models/db.model.js';
|
||||
import { businesses, commercials, users } from './drizzle/schema';
|
||||
export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
|
||||
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
|
||||
export function convertStringToNullUndefined(value) {
|
||||
@@ -20,106 +19,150 @@ 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<BusinessListing>): DrizzleBusinessListing {
|
||||
return flattenObject(businessListing);
|
||||
}
|
||||
export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): BusinessListing {
|
||||
const o = {
|
||||
location_city: drizzleBusinessListing.city,
|
||||
location_state: drizzleBusinessListing.state,
|
||||
location_latitude: drizzleBusinessListing.latitude,
|
||||
location_longitude: drizzleBusinessListing.longitude,
|
||||
...drizzleBusinessListing,
|
||||
};
|
||||
delete o.city;
|
||||
delete o.state;
|
||||
delete o.latitude;
|
||||
delete o.longitude;
|
||||
return unflattenObject(o);
|
||||
}
|
||||
export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial<CommercialPropertyListing>): DrizzleCommercialPropertyListing {
|
||||
return flattenObject(commercialPropertyListing);
|
||||
}
|
||||
export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): CommercialPropertyListing {
|
||||
const o = {
|
||||
location_city: drizzleCommercialPropertyListing.city,
|
||||
location_state: drizzleCommercialPropertyListing.state,
|
||||
location_latitude: drizzleCommercialPropertyListing.latitude,
|
||||
location_longitude: drizzleCommercialPropertyListing.longitude,
|
||||
...drizzleCommercialPropertyListing,
|
||||
};
|
||||
delete o.city;
|
||||
delete o.state;
|
||||
delete o.latitude;
|
||||
delete o.longitude;
|
||||
return unflattenObject(o);
|
||||
}
|
||||
export function convertUserToDrizzleUser(user: Partial<User>): DrizzleUser {
|
||||
return flattenObject(user);
|
||||
}
|
||||
export type DrizzleUser = typeof users.$inferSelect;
|
||||
export type DrizzleBusinessListing = typeof businesses.$inferSelect;
|
||||
export type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect;
|
||||
// export function convertBusinessToDrizzleBusiness(businessListing: Partial<BusinessListing>): DrizzleBusinessListing {
|
||||
// const drizzleBusinessListing = flattenObject(businessListing);
|
||||
// drizzleBusinessListing.city = drizzleBusinessListing.name;
|
||||
// delete drizzleBusinessListing.name;
|
||||
// return drizzleBusinessListing;
|
||||
// }
|
||||
// export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): 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<CommercialPropertyListing>): DrizzleCommercialPropertyListing {
|
||||
// const drizzleCommercialPropertyListing = flattenObject(commercialPropertyListing);
|
||||
// drizzleCommercialPropertyListing.city = drizzleCommercialPropertyListing.name;
|
||||
// delete drizzleCommercialPropertyListing.name;
|
||||
// return drizzleCommercialPropertyListing;
|
||||
// }
|
||||
// export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): 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<User>): DrizzleUser {
|
||||
// const drizzleUser = flattenObject(user);
|
||||
// drizzleUser.city = drizzleUser.name;
|
||||
// delete drizzleUser.name;
|
||||
// return drizzleUser;
|
||||
// }
|
||||
|
||||
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];
|
||||
// export function convertDrizzleUserToUser(drizzleUser: Partial<DrizzleUser>): 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;
|
||||
|
||||
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 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;
|
||||
// }
|
||||
export function splitName(fullName: string): { firstname: string; lastname: string } {
|
||||
const parts = fullName.trim().split(/\s+/); // Teile den Namen am Leerzeichen auf
|
||||
|
||||
if (parts.length === 1) {
|
||||
// Falls es nur ein Teil gibt, ist firstname und lastname gleich
|
||||
return { firstname: parts[0], lastname: parts[0] };
|
||||
} else {
|
||||
// Ansonsten ist der letzte Teil der lastname, der Rest der firstname
|
||||
const lastname = parts.pop()!;
|
||||
const firstname = parts.join(' ');
|
||||
return { firstname, lastname };
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
16
bizmatch-server/src/utils/ip.util.ts
Normal file
16
bizmatch-server/src/utils/ip.util.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
export interface RealIpInfo {
|
||||
ip: string | undefined;
|
||||
countryCode?: string;
|
||||
}
|
||||
|
||||
export function getRealIpInfo(req: Request): RealIpInfo {
|
||||
const ip =
|
||||
(req.headers['cf-connecting-ip'] as string) ||
|
||||
(req.headers['x-real-ip'] as string) ||
|
||||
(typeof req.headers['x-forwarded-for'] === 'string' ? req.headers['x-forwarded-for'].split(',')[0] : req.connection.remoteAddress);
|
||||
const countryCode = req.headers['cf-ipcountry'] as string;
|
||||
|
||||
return { ip, countryCode };
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"],
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "src/drizzle/import.ts"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
@@ -18,6 +18,6 @@
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"esModuleInterop":true
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"node_modules/quill/dist/quill.snow.css"
|
||||
"node_modules/quill/dist/quill.snow.css",
|
||||
"node_modules/leaflet/dist/leaflet.css"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
@@ -81,7 +82,9 @@
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {"proxyConfig": "proxy.conf.json"}
|
||||
"options": {
|
||||
"proxyConfig": "proxy.conf.json"
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
@@ -102,7 +105,12 @@
|
||||
"src/assets",
|
||||
"cropped-Favicon-32x32.png",
|
||||
"cropped-Favicon-180x180.png",
|
||||
"cropped-Favicon-191x192.png"
|
||||
"cropped-Favicon-191x192.png",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "./node_modules/leaflet/dist/images",
|
||||
"output": "assets/"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
@@ -116,4 +124,4 @@
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
"@angular/platform-browser-dynamic": "^18.1.3",
|
||||
"@angular/platform-server": "^18.1.3",
|
||||
"@angular/router": "^18.1.3",
|
||||
"@bluehalo/ngx-leaflet": "^18.0.2",
|
||||
"@fortawesome/angular-fontawesome": "^0.15.0",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
@@ -31,7 +32,9 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@ng-select/ng-select": "^13.4.1",
|
||||
"@ngneat/until-destroy": "^10.0.0",
|
||||
"@stripe/stripe-js": "^4.3.0",
|
||||
"@types/cropperjs": "^1.3.0",
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"browser-bunyan": "^1.8.0",
|
||||
"dayjs": "^1.11.11",
|
||||
@@ -40,11 +43,15 @@
|
||||
"jwt-decode": "^4.0.0",
|
||||
"keycloak-angular": "^16.0.1",
|
||||
"keycloak-js": "^25.0.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"memoize-one": "^6.0.0",
|
||||
"ng-gallery": "^11.0.0",
|
||||
"ngx-currency": "^18.0.0",
|
||||
"ngx-image-cropper": "^8.0.0",
|
||||
"ngx-mask": "^18.0.0",
|
||||
"ngx-quill": "^26.0.5",
|
||||
"ngx-sharebuttons": "^15.0.3",
|
||||
"ngx-stripe": "^18.1.0",
|
||||
"on-change": "^5.0.1",
|
||||
"rxjs": "~7.8.1",
|
||||
"tslib": "^2.6.3",
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:3000",
|
||||
"secure": false
|
||||
"secure": false,
|
||||
"changeOrigin": true,
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"/pictures": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false
|
||||
},
|
||||
"/ipify": {
|
||||
"target": "https://api.ipify.org",
|
||||
"secure": true,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/ipify": ""
|
||||
}
|
||||
},
|
||||
"/ipinfo": {
|
||||
"target": "https://ipinfo.io",
|
||||
"secure": true,
|
||||
"changeOrigin": true,
|
||||
"pathRewrite": {
|
||||
"^/ipinfo": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
<!-- <div class="container"> -->
|
||||
<div [ngClass]="{ 'bg-slate-100': actualRoute !== 'home' }">
|
||||
@if (actualRoute !=='home' && actualRoute !=='pricing'){
|
||||
<div class="flex flex-col" [ngClass]="{ 'bg-slate-100 print:bg-white': actualRoute !== 'home' }">
|
||||
@if (actualRoute !=='home'){
|
||||
<header></header>
|
||||
}
|
||||
<router-outlet></router-outlet>
|
||||
<main class="flex-grow">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
@@ -30,26 +32,8 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- <div *ngIf="loadingService.isLoading$ | async" class="spinner-overlay">
|
||||
<div class="spinner-container">
|
||||
<ng-container *ngIf="loadingService.loadingText$ | async as loadingText">
|
||||
<div *ngIf="loadingText" class="spinner-text">{{ loadingText }}</div>
|
||||
</ng-container>
|
||||
<div role="status">
|
||||
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<app-message-container></app-message-container>
|
||||
<app-search-modal></app-search-modal>
|
||||
<app-confirmation></app-confirmation>
|
||||
<app-email></app-email>
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { KeycloakEventType, KeycloakService } from 'keycloak-angular';
|
||||
|
||||
import { filter } from 'rxjs/operators';
|
||||
import build from '../build';
|
||||
import { ConfirmationComponent } from './components/confirmation/confirmation.component';
|
||||
import { ConfirmationService } from './components/confirmation/confirmation.service';
|
||||
import { EMailComponent } from './components/email/email.component';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
import { MessageContainerComponent } from './components/message/message-container.component';
|
||||
import { SearchModalComponent } from './components/search-modal/search-modal.component';
|
||||
import { AuditService } from './services/audit.service';
|
||||
import { GeoService } from './services/geo.service';
|
||||
import { LoadingService } from './services/loading.service';
|
||||
import { UserService } from './services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
|
||||
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent, EMailComponent],
|
||||
providers: [],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
@@ -34,6 +37,8 @@ export class AppComponent {
|
||||
private keycloakService: KeycloakService,
|
||||
private userService: UserService,
|
||||
private confirmationService: ConfirmationService,
|
||||
private auditService: AuditService,
|
||||
private geoService: GeoService,
|
||||
) {
|
||||
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
|
||||
let currentRoute = this.activatedRoute.root;
|
||||
@@ -44,14 +49,38 @@ export class AppComponent {
|
||||
this.actualRoute = currentRoute.snapshot.url[0].path;
|
||||
});
|
||||
}
|
||||
ngOnInit() {}
|
||||
ngOnInit() {
|
||||
// Überwache Keycloak-Events, um den Token-Refresh zu kontrollieren
|
||||
this.keycloakService.keycloakEvents$.subscribe({
|
||||
next: event => {
|
||||
if (event.type === KeycloakEventType.OnTokenExpired) {
|
||||
// Wenn der Token abgelaufen ist, versuchen wir einen Refresh
|
||||
this.handleTokenExpiration();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
private async handleTokenExpiration(): Promise<void> {
|
||||
try {
|
||||
// Versuche, den Token zu erneuern
|
||||
const refreshed = await this.keycloakService.updateToken();
|
||||
if (!refreshed) {
|
||||
// Wenn der Token nicht erneuert werden kann, leite zur Login-Seite weiter
|
||||
this.keycloakService.login({
|
||||
redirectUri: window.location.href, // oder eine andere Seite
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.error === 'invalid_grant' && error.error_description === 'Token is not active') {
|
||||
// Hier wird der Fehler "invalid_grant" abgefangen
|
||||
this.keycloakService.login({
|
||||
redirectUri: window.location.href,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
handleKeyboardEvent(event: KeyboardEvent) {
|
||||
// this.router.events.subscribe(event => {
|
||||
// if (event instanceof NavigationEnd) {
|
||||
// initFlowbite();
|
||||
// }
|
||||
// });
|
||||
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
|
||||
this.showVersionDialog();
|
||||
}
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
|
||||
import { APP_INITIALIZER, ApplicationConfig, ErrorHandler } from '@angular/core';
|
||||
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
|
||||
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
|
||||
import { GALLERY_CONFIG, GalleryConfig } from 'ng-gallery';
|
||||
import { provideQuillConfig } from 'ngx-quill';
|
||||
import { environment } from '../environments/environment';
|
||||
import { customKeycloakAdapter } from '../keycloak';
|
||||
import { provideShareButtonsOptions, SharerMethods, withConfig } from 'ngx-sharebuttons';
|
||||
import { shareIcons } from 'ngx-sharebuttons/icons';
|
||||
import { provideNgxStripe } from 'ngx-stripe';
|
||||
import { routes } from './app.routes';
|
||||
import { LoadingInterceptor } from './interceptors/loading.interceptor';
|
||||
import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
|
||||
import { GlobalErrorHandler } from './services/globalErrorHandler';
|
||||
import { KeycloakInitializerService } from './services/keycloak-initializer.service';
|
||||
import { SelectOptionsService } from './services/select-options.service';
|
||||
import { createLogger } from './utils/utils';
|
||||
@@ -20,9 +24,9 @@ export const appConfig: ApplicationConfig = {
|
||||
{ provide: KeycloakService },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
// useFactory: initializeKeycloak,
|
||||
//useFactory: initializeKeycloak,
|
||||
useFactory: initializeKeycloak3,
|
||||
// useFactory: initializeKeycloak1,
|
||||
//useFactory: initializeKeycloak2,
|
||||
useFactory: initializeKeycloak,
|
||||
multi: true,
|
||||
//deps: [KeycloakService],
|
||||
deps: [KeycloakInitializerService],
|
||||
@@ -43,6 +47,30 @@ export const appConfig: ApplicationConfig = {
|
||||
useClass: KeycloakBearerInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: TimeoutInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: 'TIMEOUT_DURATION',
|
||||
useValue: 5000, // Standard-Timeout von 5 Sekunden
|
||||
},
|
||||
{
|
||||
provide: GALLERY_CONFIG,
|
||||
useValue: {
|
||||
autoHeight: true,
|
||||
imageSize: 'cover',
|
||||
} as GalleryConfig,
|
||||
},
|
||||
{ provide: ErrorHandler, useClass: GlobalErrorHandler }, // Registriere den globalen ErrorHandler
|
||||
provideShareButtonsOptions(
|
||||
shareIcons(),
|
||||
withConfig({
|
||||
debug: true,
|
||||
sharerMethod: SharerMethods.Anchor,
|
||||
}),
|
||||
),
|
||||
provideRouter(
|
||||
routes,
|
||||
withEnabledBlockingInitialNavigation(),
|
||||
@@ -52,6 +80,7 @@ export const appConfig: ApplicationConfig = {
|
||||
}),
|
||||
),
|
||||
provideAnimations(),
|
||||
provideNgxStripe('pk_test_IlpbVQhxAXZypLgnCHOCqlj8'),
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
@@ -71,47 +100,47 @@ function initServices(selectOptions: SelectOptionsService) {
|
||||
await selectOptions.init();
|
||||
};
|
||||
}
|
||||
export function initializeKeycloak3(keycloak: KeycloakInitializerService) {
|
||||
export function initializeKeycloak(keycloak: KeycloakInitializerService) {
|
||||
return () => keycloak.initialize();
|
||||
}
|
||||
|
||||
export function initializeKeycloak2(keycloak: KeycloakService): () => Promise<void> {
|
||||
return async () => {
|
||||
const { url, realm, clientId } = environment.keycloak;
|
||||
const adapter = customKeycloakAdapter(() => keycloak.getKeycloakInstance(), {});
|
||||
if (window.location.search.length > 0) {
|
||||
sessionStorage.setItem('SEARCH', window.location.search);
|
||||
}
|
||||
const { host, hostname, href, origin, pathname, port, protocol, search } = window.location;
|
||||
await keycloak.init({
|
||||
config: { url, realm, clientId },
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: window.location.hostname === 'localhost' ? `${window.location.origin}/assets/silent-check-sso.html` : `${window.location.origin}/dealerweb/assets/silent-check-sso.html`,
|
||||
adapter,
|
||||
redirectUri: `${origin}${pathname}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
function initializeKeycloak(keycloak: KeycloakService) {
|
||||
return async () => {
|
||||
logger.info(`###>calling keycloakService init ...`);
|
||||
const authenticated = await keycloak.init({
|
||||
config: {
|
||||
url: environment.keycloak.url,
|
||||
realm: environment.keycloak.realm,
|
||||
clientId: environment.keycloak.clientId,
|
||||
},
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||
},
|
||||
bearerExcludedUrls: ['/assets'],
|
||||
shouldUpdateToken(request) {
|
||||
return !request.headers.get('token-update') === false;
|
||||
},
|
||||
});
|
||||
logger.info(`+++>${authenticated}`);
|
||||
};
|
||||
}
|
||||
// export function initializeKeycloak1(keycloak: KeycloakService): () => Promise<void> {
|
||||
// return async () => {
|
||||
// const { url, realm, clientId } = environment.keycloak;
|
||||
// const adapter = customKeycloakAdapter(() => keycloak.getKeycloakInstance(), {});
|
||||
// if (window.location.search.length > 0) {
|
||||
// sessionStorage.setItem('SEARCH', window.location.search);
|
||||
// }
|
||||
// const { host, hostname, href, origin, pathname, port, protocol, search } = window.location;
|
||||
// await keycloak.init({
|
||||
// config: { url, realm, clientId },
|
||||
// initOptions: {
|
||||
// onLoad: 'check-sso',
|
||||
// silentCheckSsoRedirectUri: window.location.hostname === 'localhost' ? `${window.location.origin}/assets/silent-check-sso.html` : `${window.location.origin}/dealerweb/assets/silent-check-sso.html`,
|
||||
// adapter,
|
||||
// redirectUri: `${origin}${pathname}`,
|
||||
// },
|
||||
// });
|
||||
// };
|
||||
// }
|
||||
// function initializeKeycloak2(keycloak: KeycloakService) {
|
||||
// return async () => {
|
||||
// logger.info(`###>calling keycloakService init ...`);
|
||||
// const authenticated = await keycloak.init({
|
||||
// config: {
|
||||
// url: environment.keycloak.url,
|
||||
// realm: environment.keycloak.realm,
|
||||
// clientId: environment.keycloak.clientId,
|
||||
// },
|
||||
// initOptions: {
|
||||
// onLoad: 'check-sso',
|
||||
// silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||
// },
|
||||
// bearerExcludedUrls: ['/assets'],
|
||||
// shouldUpdateToken(request) {
|
||||
// return !request.headers.get('token-update') === false;
|
||||
// },
|
||||
// });
|
||||
// logger.info(`+++>${authenticated}`);
|
||||
// };
|
||||
// }
|
||||
|
||||
@@ -4,6 +4,7 @@ import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { ListingCategoryGuard } from './guards/listing-category.guard';
|
||||
import { UserListComponent } from './pages/admin/user-list/user-list.component';
|
||||
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
|
||||
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
|
||||
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
|
||||
@@ -11,6 +12,7 @@ import { HomeComponent } from './pages/home/home.component';
|
||||
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
|
||||
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
|
||||
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
|
||||
import { LoginComponent } from './pages/login/login.component';
|
||||
import { PricingComponent } from './pages/pricing/pricing.component';
|
||||
import { AccountComponent } from './pages/subscription/account/account.component';
|
||||
import { EditBusinessListingComponent } from './pages/subscription/edit-business-listing/edit-business-listing.component';
|
||||
@@ -18,6 +20,7 @@ import { EditCommercialPropertyListingComponent } from './pages/subscription/edi
|
||||
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
|
||||
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
|
||||
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
|
||||
import { SuccessComponent } from './pages/success/success.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
@@ -54,6 +57,14 @@ export const routes: Routes = [
|
||||
canActivate: [ListingCategoryGuard],
|
||||
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
},
|
||||
{
|
||||
path: 'login/:page',
|
||||
component: LoginComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
},
|
||||
{
|
||||
path: 'notfound',
|
||||
component: NotFoundComponent,
|
||||
},
|
||||
// #########
|
||||
// User Details
|
||||
{
|
||||
@@ -113,7 +124,7 @@ export const routes: Routes = [
|
||||
{
|
||||
path: 'emailUs',
|
||||
component: EmailUsComponent,
|
||||
canActivate: [AuthGuard],
|
||||
// canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// Logout
|
||||
@@ -128,5 +139,25 @@ export const routes: Routes = [
|
||||
path: 'pricing',
|
||||
component: PricingComponent,
|
||||
},
|
||||
{
|
||||
path: 'pricingOverview',
|
||||
component: PricingComponent,
|
||||
data: {
|
||||
pricingOverview: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'pricing/:id',
|
||||
component: PricingComponent,
|
||||
},
|
||||
{
|
||||
path: 'success',
|
||||
component: SuccessComponent,
|
||||
},
|
||||
{
|
||||
path: 'admin/users',
|
||||
component: UserListComponent,
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{ path: '**', redirectTo: 'home' },
|
||||
];
|
||||
|
||||
@@ -19,6 +19,7 @@ export abstract class BaseInputComponent implements ControlValueAccessor {
|
||||
@Input() label: string = '';
|
||||
// @Input() id: string = '';
|
||||
@Input() name: string = '';
|
||||
isTooltipVisible = false;
|
||||
constructor(protected validationMessagesService: ValidationMessagesService) {}
|
||||
ngOnInit() {
|
||||
this.subscription = this.validationMessagesService.messages$.subscribe(() => {
|
||||
@@ -51,4 +52,9 @@ export abstract class BaseInputComponent implements ControlValueAccessor {
|
||||
this.validationMessage = this.validationMessagesService.getMessage(this.name);
|
||||
}
|
||||
setDisabledState?(isDisabled: boolean): void {}
|
||||
toggleTooltip(event: Event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.isTooltipVisible = !this.isTooltipVisible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import { ConfirmationService } from './confirmation.service';
|
||||
<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>
|
||||
@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'){
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation?.message }}</h3>
|
||||
@if(confirmation?.buttons==='both'){
|
||||
<button
|
||||
(click)="confirmationService.accept()"
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
interface KeyValue {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-sub-type',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
template: `
|
||||
<ng-container [ngSwitch]="customerSubType">
|
||||
<span *ngSwitchCase="'broker'" class="bg-blue-100 text-blue-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-blue-400 border border-blue-400">Broker</span>
|
||||
<span *ngSwitchCase="'cpa'" class="bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-gray-400 border border-gray-500">CPA</span>
|
||||
<span *ngSwitchCase="'attorney'" class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400">Attorney</span>
|
||||
<span *ngSwitchCase="'titleCompany'" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Title Company</span>
|
||||
<span *ngSwitchCase="'surveyor'" class="bg-yellow-100 text-yellow-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-yellow-300 border border-yellow-300">Surveyor</span>
|
||||
<span *ngSwitchCase="'appraiser'" class="bg-pink-100 text-pink-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-pink-400 border border-pink-400">Appraiser</span>
|
||||
<span *ngSwitchDefault class="text-gray-500">Unknown</span>
|
||||
</ng-container>
|
||||
`,
|
||||
styles: [],
|
||||
})
|
||||
export class CustomerSubTypeComponent {
|
||||
@Input() customerSubType: 'broker' | 'cpa' | 'attorney' | 'surveyor' | 'appraiser' | 'titleCompany';
|
||||
}
|
||||
43
bizmatch/src/app/components/email/email.component.html
Normal file
43
bizmatch/src/app/components/email/email.component.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!-- Main modal -->
|
||||
<div *ngIf="eMailService.modalVisible$ | async" id="authentication-modal" tabindex="-1" class="z-40 fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
|
||||
<div class="relative p-4 w-full max-w-md max-h-full">
|
||||
<!-- Modal content -->
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<!-- Modal header -->
|
||||
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Email listing to a friend</h3>
|
||||
<button
|
||||
(click)="eMailService.reject()"
|
||||
type="button"
|
||||
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Modal body -->
|
||||
<div class="p-4 md:p-5">
|
||||
<form class="space-y-4" action="#">
|
||||
<div>
|
||||
<app-validated-input label="Your Email" name="yourEmail" [(ngModel)]="shareByEMail.yourEmail"></app-validated-input>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-input label="Your Name" name="yourName" [(ngModel)]="shareByEMail.yourName"></app-validated-input>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-input label="Your Friend's EMail" name="recipientEmail" [(ngModel)]="shareByEMail.recipientEmail"></app-validated-input>
|
||||
</div>
|
||||
|
||||
<button
|
||||
(click)="sendMail()"
|
||||
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
>
|
||||
Send EMail
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
40
bizmatch/src/app/components/email/email.component.ts
Normal file
40
bizmatch/src/app/components/email/email.component.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { ShareByEMail } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { MailService } from '../../services/mail.service';
|
||||
import { ValidatedInputComponent } from '../validated-input/validated-input.component';
|
||||
import { ValidationMessagesService } from '../validation-messages.service';
|
||||
import { EMailService } from './email.service';
|
||||
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-email',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, ValidatedInputComponent],
|
||||
templateUrl: './email.component.html',
|
||||
template: ``,
|
||||
})
|
||||
export class EMailComponent {
|
||||
shareByEMail: ShareByEMail = {};
|
||||
constructor(public eMailService: EMailService, private mailService: MailService, private validationMessagesService: ValidationMessagesService) {}
|
||||
ngOnInit() {
|
||||
this.eMailService.shareByEMail$.pipe(untilDestroyed(this)).subscribe(val => {
|
||||
this.shareByEMail = val;
|
||||
});
|
||||
}
|
||||
async sendMail() {
|
||||
try {
|
||||
const result = await this.mailService.mailToFriend(this.shareByEMail);
|
||||
this.eMailService.accept(this.shareByEMail);
|
||||
} catch (error) {
|
||||
if (error.error && Array.isArray(error.error?.message)) {
|
||||
this.validationMessagesService.updateMessages(error.error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
|
||||
}
|
||||
}
|
||||
33
bizmatch/src/app/components/email/email.service.ts
Normal file
33
bizmatch/src/app/components/email/email.service.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { ShareByEMail } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EMailService {
|
||||
private modalVisibleSubject = new Subject<boolean>();
|
||||
private shareByEMailSubject = new Subject<ShareByEMail>();
|
||||
private resolvePromise!: (value: boolean | ShareByEMail) => void;
|
||||
|
||||
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
|
||||
shareByEMail$: Observable<ShareByEMail> = this.shareByEMailSubject.asObservable();
|
||||
|
||||
showShareByEMail(shareByEMail: ShareByEMail): Promise<boolean | ShareByEMail> {
|
||||
this.shareByEMailSubject.next(shareByEMail);
|
||||
this.modalVisibleSubject.next(true);
|
||||
return new Promise<boolean | ShareByEMail>(resolve => {
|
||||
this.resolvePromise = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
accept(value: ShareByEMail): void {
|
||||
this.modalVisibleSubject.next(false);
|
||||
this.resolvePromise(value);
|
||||
}
|
||||
|
||||
reject(): void {
|
||||
this.modalVisibleSubject.next(false);
|
||||
this.resolvePromise(false);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,17 @@
|
||||
<!-- <div class="surface-0 px-4 py-4 md:px-6 lg:px-8">
|
||||
<div class="surface-0">
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-3 md:mb-0 mb-3 cursor-pointer" routerLink="/home">
|
||||
<img src="assets/images/header-logo.png" alt="footer sections" height="30" class="mr-3" />
|
||||
<div class="text-500">© 2024 Bizmatch All rights reserved.</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-3">
|
||||
<div class="text-black mb-4 flex flex-wrap" style="max-width: 290px">BizMatch, Inc., 1001 Blucher Street, Corpus Christi, Texas 78401</div>
|
||||
<div class="text-black mb-3"><i class="text-white pi pi-phone surface-800 border-round p-1 mr-2"></i>1-800-840-6025</div>
|
||||
<div class="text-black mb-3"><i class="text-white pi pi-inbox surface-800 border-round p-1 mr-2"></i>info@bizmatch.net</div>
|
||||
</div>
|
||||
<div class="col-12 md:col-3 text-500">
|
||||
<div class="text-black font-bold line-height-3 mb-3">Legal</div>
|
||||
<a class="line-height-3 block cursor-pointer mb-2" (click)="termsVisible = true">Terms of use</a>
|
||||
<a class="line-height-3 block cursor-pointer mb-2" (click)="privacyVisible = true">Privacy statement</a>
|
||||
</div>
|
||||
<div class="col-12 md:col-3 text-500">
|
||||
<div class="text-black font-bold line-height-3 mb-3">Actions</div>
|
||||
<a *ngIf="!keycloakService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
|
||||
<a *ngIf="!keycloakService.isLoggedIn()" (click)="register()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Register</a>
|
||||
<a *ngIf="keycloakService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
|
||||
<a *ngIf="keycloakService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="keycloakService.logout()">Log Out</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<footer class="bg-white px-4 py-2 md:px-6 mt-auto w-full">
|
||||
<footer class="bg-white px-4 py-2 md:px-6 mt-auto w-full print:hidden">
|
||||
<div class="container mx-auto flex flex-col lg:flex-row justify-between items-center">
|
||||
<div class="flex flex-col lg:flex-row items-center mb-4 lg:mb-0">
|
||||
<img src="assets/images/header-logo.png" alt="BizMatch Logo" class="h-8 mb-2 lg:mb-0 lg:mr-4" />
|
||||
<!-- <img src="assets/images/header-logo.png" alt="BizMatch Logo" class="h-8 mb-2 lg:mb-0 lg:mr-4" /> -->
|
||||
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="assets/images/header-logo.png" class="h-8" class="h-8 mb-2 lg:mb-0 lg:mr-4" />
|
||||
</a>
|
||||
<p class="text-sm text-gray-600 text-center lg:text-left">© {{ currentYear }} Bizmatch All rights reserved.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row items-center order-3 lg:order-2">
|
||||
<a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" data-drawer-target="terms-of-use" data-drawer-show="terms-of-use" aria-controls="terms-of-use">Terms of use</a>
|
||||
<a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" data-drawer-target="privacy" data-drawer-show="privacy" aria-controls="privacy">Privacy statement</a>
|
||||
<a class="text-sm text-blue-600 hover:underline hover:cursor-pointer mx-2" routerLink="/pricingOverview">Pricing</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row items-center order-2 lg:order-3">
|
||||
@@ -58,6 +34,17 @@
|
||||
</svg>
|
||||
Privacy Statement
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
data-drawer-hide="privacy"
|
||||
aria-controls="privacy"
|
||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close menu</span>
|
||||
</button>
|
||||
<section id="content" role="main">
|
||||
<article id="post-2" class="post-2 page type-page status-publish hentry pmpro-has-access">
|
||||
<section class="entry-content">
|
||||
@@ -262,6 +249,17 @@
|
||||
</svg>
|
||||
Terms of use
|
||||
</h5>
|
||||
<button
|
||||
type="button"
|
||||
data-drawer-hide="terms-of-use"
|
||||
aria-controls="terms-of-use"
|
||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 absolute top-2.5 end-2.5 flex items-center justify-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
<span class="sr-only">Close menu</span>
|
||||
</button>
|
||||
<section id="content" role="main">
|
||||
<article id="post-1" class="post-1 page type-page status-publish hentry pmpro-has-access">
|
||||
<section class="entry-content">
|
||||
|
||||
@@ -1,143 +1,57 @@
|
||||
<!-- <nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||
<nav class="bg-white border-gray-200 dark:bg-gray-900 print:hidden">
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
|
||||
</a>
|
||||
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
|
||||
<button
|
||||
type="button"
|
||||
class="flex text-sm bg-gray-200 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||
id="user-menu-button"
|
||||
aria-expanded="false"
|
||||
[attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'"
|
||||
data-dropdown-placement="bottom"
|
||||
>
|
||||
<span class="sr-only">Open user menu</span>
|
||||
@if(user){
|
||||
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" alt="user photo" />
|
||||
} @else {
|
||||
<i class="flex justify-center items-center text-stone-50 w-8 h-8 rounded-full fa-solid fa-bars"></i>
|
||||
}
|
||||
</button>
|
||||
@if(user){
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-login">
|
||||
<div class="px-4 py-3">
|
||||
<span class="block text-sm text-gray-900 dark:text-white">Welcome, {{ user.firstname }} </span>
|
||||
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">{{ user.email }}</span>
|
||||
</div>
|
||||
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||
<li>
|
||||
<a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
>Create Listing</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/myListings" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Listings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/emailUs" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">EMail Us</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-unknown">
|
||||
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||
<li>
|
||||
<a (click)="login()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Log In</a>
|
||||
</li>
|
||||
<li>
|
||||
<a (click)="register()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Register</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
data-collapse-toggle="navbar-user"
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||
aria-controls="navbar-user"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||
<ul
|
||||
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700"
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
routerLink="/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"
|
||||
aria-current="page"
|
||||
(click)="closeMenus()"
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
routerLink="/commercialPropertyListings"
|
||||
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/commercialPropertyListings') }"
|
||||
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
(click)="closeMenus()"
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
routerLinkActive="active-link"
|
||||
routerLink="/brokerListings"
|
||||
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/brokerListings') }"
|
||||
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||
(click)="closeMenus()"
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav> -->
|
||||
|
||||
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
|
||||
</a>
|
||||
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
|
||||
<div class="flex items-center md:order-2 space-x-3 rtl:space-x-reverse">
|
||||
<!-- Filter button -->
|
||||
@if(isListingUrl()){
|
||||
@if(isFilterUrl()){
|
||||
<button
|
||||
type="button"
|
||||
#triggerButton
|
||||
(click)="openModal()"
|
||||
id="filterDropdownButton"
|
||||
class="max-sm:hidden px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 md:me-2"
|
||||
class="max-sm:hidden px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
|
||||
</button>
|
||||
<!-- Sort button -->
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
id="sortDropdownButton"
|
||||
class="max-sm:hidden px-4 py-2 text-sm font-medium bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
(click)="toggleSortDropdown()"
|
||||
[ngClass]="{ 'text-blue-500': selectOptions.getSortByOption(criteria?.sortBy) !== 'Sort', 'text-gray-900': selectOptions.getSortByOption(criteria?.sortBy) === 'Sort' }"
|
||||
>
|
||||
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(criteria?.sortBy) }}
|
||||
</button>
|
||||
|
||||
<!-- Sort options dropdown -->
|
||||
<div *ngIf="sortDropdownVisible" class="absolute right-0 z-50 w-48 md:mt-2 max-md:mt-20 max-md:mr-[-2.5rem] bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-600">
|
||||
<ul class="py-1 text-sm text-gray-700 dark:text-gray-200">
|
||||
@for(item of sortByOptions; track item){
|
||||
<li (click)="sortBy(item.value)" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">{{ item.selectName ? item.selectName : item.name }}</li>
|
||||
}
|
||||
<!-- <li (click)="sortBy('priceAsc')" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Price Ascending</li>
|
||||
<li (click)="sortBy('priceDesc')" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Price Descending</li>
|
||||
<li (click)="sortBy('creationDateFirst')" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Creation Date First</li>
|
||||
<li (click)="sortBy('creationDateLast')" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Creation Date Last</li>
|
||||
<li (click)="sortBy(null)" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer">Default Sorting</li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="flex text-sm bg-gray-200 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||
class="flex text-sm bg-gray-400 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||
id="user-menu-button"
|
||||
aria-expanded="false"
|
||||
[attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'"
|
||||
data-dropdown-placement="bottom"
|
||||
>
|
||||
<span class="sr-only">Open user menu</span>
|
||||
@if(user){
|
||||
@if(user?.hasProfile){
|
||||
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" alt="user photo" />
|
||||
} @else {
|
||||
<i class="flex justify-center items-center text-stone-50 w-8 h-8 rounded-full fa-solid fa-bars"></i>
|
||||
@@ -154,14 +68,25 @@
|
||||
<li>
|
||||
<a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
|
||||
</li>
|
||||
@if(user.customerType==='professional' || user.customerType==='seller' || isAdmin()){
|
||||
<li>
|
||||
@if(user.customerSubType==='broker'){
|
||||
<a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
>Create Listing</a
|
||||
>
|
||||
}@else {
|
||||
<a routerLink="/createCommercialPropertyListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||
>Create Listing</a
|
||||
>
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/myListings" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Listings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="/myFavorites" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Favorites</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a routerLink="/emailUs" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">EMail Us</a>
|
||||
</li>
|
||||
@@ -169,6 +94,42 @@
|
||||
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
|
||||
</li>
|
||||
</ul>
|
||||
@if(isAdmin()){
|
||||
<ul class="py-2">
|
||||
<li>
|
||||
<a routerLink="admin/users" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Users (Admin)</a>
|
||||
</li>
|
||||
</ul>
|
||||
}
|
||||
<ul class="py-2 md:hidden">
|
||||
<li>
|
||||
<a
|
||||
routerLink="/businessListings"
|
||||
[ngClass]="{ 'text-blue-700': isActive('/businessListings'), 'text-gray-700': !isActive('/businessListings') }"
|
||||
class="block px-4 py-2 text-sm font-semibold"
|
||||
(click)="closeMenusAndSetCriteria('businessListings')"
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
routerLink="/commercialPropertyListings"
|
||||
[ngClass]="{ 'text-blue-700': isActive('/commercialPropertyListings'), 'text-gray-700': !isActive('/commercialPropertyListings') }"
|
||||
class="block px-4 py-2 text-sm font-semibold"
|
||||
(click)="closeMenusAndSetCriteria('commercialPropertyListings')"
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
[ngClass]="{ 'text-blue-700': isActive('/brokerListings'), 'text-gray-700': !isActive('/brokerListings') }"
|
||||
class="block px-4 py-2 text-sm font-semibold"
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')"
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-unknown">
|
||||
@@ -177,12 +138,41 @@
|
||||
<a (click)="login()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Log In</a>
|
||||
</li>
|
||||
<li>
|
||||
<a (click)="register()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Register</a>
|
||||
<a routerLink="/pricing" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Register</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-2 md:hidden">
|
||||
<li>
|
||||
<a
|
||||
routerLink="/businessListings"
|
||||
[ngClass]="{ 'text-blue-700': isActive('/businessListings'), 'text-gray-700': !isActive('/businessListings') }"
|
||||
class="block px-4 py-2 text-sm font-bold"
|
||||
(click)="closeMenusAndSetCriteria('businessListings')"
|
||||
>Businesses</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
routerLink="/commercialPropertyListings"
|
||||
[ngClass]="{ 'text-blue-700': isActive('/commercialPropertyListings'), 'text-gray-700': !isActive('/commercialPropertyListings') }"
|
||||
class="block px-4 py-2 text-sm font-bold"
|
||||
(click)="closeMenusAndSetCriteria('commercialPropertyListings')"
|
||||
>Properties</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
routerLink="/brokerListings"
|
||||
[ngClass]="{ 'text-blue-700': isActive('/brokerListings'), 'text-gray-700': !isActive('/brokerListings') }"
|
||||
class="block px-4 py-2 text-sm font-bold"
|
||||
(click)="closeMenusAndSetCriteria('brokerListings')"
|
||||
>Professionals</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
<!-- <button
|
||||
data-collapse-toggle="navbar-user"
|
||||
type="button"
|
||||
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||
@@ -193,7 +183,7 @@
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
|
||||
</svg>
|
||||
</button>
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||
<ul
|
||||
@@ -234,7 +224,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile filter button -->
|
||||
@if(isListingUrl()){
|
||||
@if(isFilterUrl()){
|
||||
<div class="md:hidden flex justify-center pb-4">
|
||||
<button
|
||||
(click)="openModal()"
|
||||
@@ -244,6 +234,16 @@
|
||||
>
|
||||
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
|
||||
</button>
|
||||
<!-- Sorting -->
|
||||
<button
|
||||
(click)="toggleSortDropdown()"
|
||||
type="button"
|
||||
id="sortDropdownMobileButton"
|
||||
class="mx-4 w-1/2 px-4 py-2 text-sm font-medium bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
[ngClass]="{ 'text-blue-500': selectOptions.getSortByOption(criteria?.sortBy) !== 'Sort', 'text-gray-900': selectOptions.getSortByOption(criteria?.sortBy) === 'Sort' }"
|
||||
>
|
||||
<i class="fas fa-sort mr-2"></i>{{ selectOptions.getSortByOption(criteria?.sortBy) }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
||||
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
|
||||
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
import { filter, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { SortByOptions, User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, KeyValueAsSortBy, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { CriteriaChangeService } from '../../services/criteria-change.service';
|
||||
import { SearchService } from '../../services/search.service';
|
||||
import { SelectOptionsService } from '../../services/select-options.service';
|
||||
import { SharedService } from '../../services/shared.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils';
|
||||
import { assignProperties, compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils';
|
||||
import { DropdownComponent } from '../dropdown/dropdown.component';
|
||||
import { ModalService } from '../search-modal/modal.service';
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'header',
|
||||
standalone: true,
|
||||
@@ -41,6 +44,8 @@ export class HeaderComponent {
|
||||
criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
|
||||
private routerSubscription: Subscription | undefined;
|
||||
baseRoute: string;
|
||||
sortDropdownVisible: boolean;
|
||||
sortByOptions: KeyValueAsSortBy[] = [];
|
||||
constructor(
|
||||
public keycloakService: KeycloakService,
|
||||
private router: Router,
|
||||
@@ -50,8 +55,15 @@ export class HeaderComponent {
|
||||
private modalService: ModalService,
|
||||
private searchService: SearchService,
|
||||
private criteriaChangeService: CriteriaChangeService,
|
||||
public selectOptions: SelectOptionsService,
|
||||
) {}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
handleGlobalClick(event: Event) {
|
||||
const target = event.target as HTMLElement;
|
||||
if (target.id !== 'sortDropdownButton' && target.id !== 'sortDropdownMobileButton') {
|
||||
this.sortDropdownVisible = false;
|
||||
}
|
||||
}
|
||||
async ngOnInit() {
|
||||
const token = await this.keycloakService.getToken();
|
||||
this.keycloakUser = map2User(token);
|
||||
@@ -65,64 +77,48 @@ export class HeaderComponent {
|
||||
}, 10);
|
||||
|
||||
this.sharedService.currentProfilePhoto.subscribe(photoUrl => {
|
||||
if (photoUrl) {
|
||||
this.profileUrl = photoUrl;
|
||||
}
|
||||
this.profileUrl = photoUrl;
|
||||
});
|
||||
|
||||
this.checkCurrentRoute(this.router.url);
|
||||
this.setupSortByOptions();
|
||||
|
||||
this.routerSubscription = this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: any) => {
|
||||
this.checkCurrentRoute(event.urlAfterRedirects);
|
||||
this.setupSortByOptions();
|
||||
});
|
||||
|
||||
this.userService.currentUser.pipe(untilDestroyed(this)).subscribe(u => {
|
||||
this.user = u;
|
||||
});
|
||||
}
|
||||
private checkCurrentRoute(url: string): void {
|
||||
this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/'
|
||||
const specialRoutes = [, '', ''];
|
||||
this.criteria = getCriteriaProxy(this.baseRoute, this);
|
||||
this.searchService.search(this.criteria);
|
||||
// this.searchService.search(this.criteria);
|
||||
}
|
||||
setupSortByOptions() {
|
||||
this.sortByOptions = [];
|
||||
if (this.isProfessionalListing()) {
|
||||
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'professional')];
|
||||
}
|
||||
if (this.isBusinessListing()) {
|
||||
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'business' || s.type === 'listing')];
|
||||
}
|
||||
if (this.isCommercialPropertyListing()) {
|
||||
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => s.type === 'commercial' || s.type === 'listing')];
|
||||
}
|
||||
this.sortByOptions = [...this.sortByOptions, ...this.selectOptions.sortByOptions.filter(s => !s.type)];
|
||||
}
|
||||
// getCriteriaProxy(path:string):BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria{
|
||||
// if ('businessListings' === path) {
|
||||
// return this.createEnhancedProxy(getCriteriaStateObject('business'));
|
||||
// } else if ('commercialPropertyListings' === path) {
|
||||
// return this.createEnhancedProxy(getCriteriaStateObject('commercialProperty'));
|
||||
// } else if ('brokerListings' === path) {
|
||||
// return this.createEnhancedProxy(getCriteriaStateObject('broker'));
|
||||
// } else {
|
||||
// return undefined;
|
||||
// }
|
||||
// }
|
||||
// private createEnhancedProxy(obj: any) {
|
||||
// const component = this;
|
||||
|
||||
// const sessionStorageHandler = function (path, value, previous, applyData) {
|
||||
// let criteriaType = '';
|
||||
// if ('/businessListings' === window.location.pathname) {
|
||||
// criteriaType = 'business';
|
||||
// } else if ('/commercialPropertyListings' === window.location.pathname) {
|
||||
// criteriaType = 'commercialProperty';
|
||||
// } else if ('/brokerListings' === window.location.pathname) {
|
||||
// criteriaType = 'broker';
|
||||
// }
|
||||
// sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this));
|
||||
// };
|
||||
|
||||
// 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();
|
||||
// });
|
||||
// }
|
||||
|
||||
ngAfterViewInit() {}
|
||||
|
||||
async openModal() {
|
||||
const accepted = await this.modalService.showModal(this.criteria);
|
||||
if (accepted) {
|
||||
const modalResult = await this.modalService.showModal(this.criteria);
|
||||
if (modalResult.accepted) {
|
||||
this.searchService.search(this.criteria);
|
||||
} else {
|
||||
this.criteria = assignProperties(this.criteria, modalResult.criteria);
|
||||
}
|
||||
}
|
||||
navigateWithState(dest: string, state: any) {
|
||||
@@ -130,18 +126,28 @@ export class HeaderComponent {
|
||||
}
|
||||
login() {
|
||||
this.keycloakService.login({
|
||||
redirectUri: window.location.href,
|
||||
redirectUri: `${window.location.origin}/login${this.router.routerState.snapshot.url}`,
|
||||
});
|
||||
}
|
||||
register() {
|
||||
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||
}
|
||||
|
||||
isActive(route: string): boolean {
|
||||
return this.router.url === route;
|
||||
}
|
||||
isListingUrl(): boolean {
|
||||
isFilterUrl(): boolean {
|
||||
return ['/businessListings', '/commercialPropertyListings', '/brokerListings'].includes(this.router.url);
|
||||
}
|
||||
isBusinessListing(): boolean {
|
||||
return ['/businessListings'].includes(this.router.url);
|
||||
}
|
||||
isCommercialPropertyListing(): boolean {
|
||||
return ['/commercialPropertyListings'].includes(this.router.url);
|
||||
}
|
||||
isProfessionalListing(): boolean {
|
||||
return ['/brokerListings'].includes(this.router.url);
|
||||
}
|
||||
// isSortingUrl(): boolean {
|
||||
// return ['/businessListings', '/commercialPropertyListings'].includes(this.router.url);
|
||||
// }
|
||||
closeDropdown() {
|
||||
const dropdownButton = document.getElementById('user-menu-button');
|
||||
const dropdownMenu = this.user ? document.getElementById('user-login') : document.getElementById('user-unknown');
|
||||
@@ -174,13 +180,24 @@ export class HeaderComponent {
|
||||
}
|
||||
getNumberOfFiltersSet() {
|
||||
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', 'sortBy']);
|
||||
} 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', 'sortBy']);
|
||||
} 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', 'sortBy']);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
isAdmin() {
|
||||
return this.keycloakService.getUserRoles(true).includes('ADMIN');
|
||||
}
|
||||
sortBy(sortBy: SortByOptions) {
|
||||
this.criteria.sortBy = sortBy;
|
||||
this.sortDropdownVisible = false;
|
||||
this.searchService.search(this.criteria);
|
||||
}
|
||||
toggleSortDropdown() {
|
||||
this.sortDropdownVisible = !this.sortDropdownVisible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,35 @@
|
||||
<p>not-found works!</p>
|
||||
<!-- <section class="bg-white dark:bg-gray-900">
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
||||
<div class="mx-auto max-w-screen-sm text-center">
|
||||
<h1 class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-primary-600 dark:text-primary-500">404</h1>
|
||||
<p class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white">Something's missing.</p>
|
||||
<p class="mb-4 text-lg font-light text-gray-500 dark:text-gray-400">Sorry, we can't find that page.</p>
|
||||
<a
|
||||
routerLink="/home"
|
||||
class="inline-flex text-white bg-primary-600 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900 my-4"
|
||||
>Back to Homepage</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section> -->
|
||||
<section class="bg-white dark:bg-gray-900">
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
||||
<div class="mx-auto max-w-screen-sm text-center">
|
||||
<h1 class="mb-4 text-7xl tracking-tight font-extrabold lg:text-9xl text-blue-700 dark:text-blue-500">404</h1>
|
||||
<p class="mb-4 text-3xl tracking-tight font-bold text-gray-900 md:text-4xl dark:text-white">Something's missing.</p>
|
||||
<p class="mb-4 text-lg font-light text-gray-500 dark:text-gray-400">Sorry, we can't find that page</p>
|
||||
<!-- <a
|
||||
href="#"
|
||||
class="inline-flex text-white bg-primary-600 hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:focus:ring-primary-900 my-4"
|
||||
>Back to Homepage</a
|
||||
> -->
|
||||
<button
|
||||
type="button"
|
||||
[routerLink]="['/home']"
|
||||
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 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
|
||||
>
|
||||
Back to Homepage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-not-found',
|
||||
standalone: true,
|
||||
template: '<h2>Page not found</h2>',
|
||||
imports: [CommonModule, RouterModule],
|
||||
templateUrl: './not-found.component.html',
|
||||
})
|
||||
export class NotFoundComponent {}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// 1. Shared Service (modal.service.ts)
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, ModalResult, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -9,26 +9,26 @@ import { BusinessListingCriteria, CommercialPropertyListingCriteria, UserListing
|
||||
export class ModalService {
|
||||
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
|
||||
private messageSubject = new BehaviorSubject<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria>(null);
|
||||
private resolvePromise!: (value: boolean) => void;
|
||||
private resolvePromise!: (value: ModalResult) => void;
|
||||
|
||||
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
|
||||
message$: Observable<BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria> = this.messageSubject.asObservable();
|
||||
|
||||
showModal(message: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): Promise<boolean> {
|
||||
showModal(message: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): Promise<ModalResult> {
|
||||
this.messageSubject.next(message);
|
||||
this.modalVisibleSubject.next(true);
|
||||
return new Promise<boolean>(resolve => {
|
||||
return new Promise<ModalResult>(resolve => {
|
||||
this.resolvePromise = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
this.modalVisibleSubject.next(false);
|
||||
this.resolvePromise(true);
|
||||
this.resolvePromise({ accepted: true });
|
||||
}
|
||||
|
||||
reject(): void {
|
||||
reject(backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria): void {
|
||||
this.modalVisibleSubject.next(false);
|
||||
this.resolvePromise(false);
|
||||
this.resolvePromise({ accepted: false, criteria: backupCriteria });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
<div class="relative w-full max-w-4xl max-h-full">
|
||||
<div class="relative bg-white rounded-lg shadow">
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t">
|
||||
@if(criteria.criteriaType==='businessListings'){
|
||||
<h3 class="text-xl font-semibold text-gray-900">Business Listing Search</h3>
|
||||
<button (click)="modalService.reject()" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||
} @else if (criteria.criteriaType==='commercialPropertyListings'){
|
||||
<h3 class="text-xl font-semibold text-gray-900">Property Listing Search</h3>
|
||||
} @else {
|
||||
<h3 class="text-xl font-semibold text-gray-900">Professional Listing Search</h3>
|
||||
}
|
||||
<button (click)="close()" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ml-auto inline-flex justify-center items-center">
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
|
||||
</svg>
|
||||
@@ -13,17 +19,24 @@
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex space-x-4 mb-4">
|
||||
<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> -->
|
||||
<i data-tooltip-target="tooltip-light" class="fa-solid fa-trash-can flex self-center ml-2 hover:cursor-pointer text-blue-500" (click)="clearFilter()"></i>
|
||||
<div id="tooltip-light" role="tooltip" class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 tooltip">
|
||||
Clear all Filter
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
@if(criteria.criteriaType==='businessListings'){
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="state" class="block mb-2 text-sm font-medium text-gray-900">Location - State</label>
|
||||
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"> </ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<!-- <div>
|
||||
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
|
||||
|
||||
<ng-select
|
||||
@@ -42,7 +55,7 @@
|
||||
<ng-option [value]="city">{{ city.city }} - {{ city.state }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- New section for city search type -->
|
||||
<div *ngIf="criteria.city">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900">Search Type</label>
|
||||
@@ -77,61 +90,67 @@
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
<app-validated-price name="price-from" [(ngModel)]="criteria.minPrice" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||
<!-- <input
|
||||
type="number"
|
||||
id="price-from"
|
||||
[(ngModel)]="criteria.minPrice"
|
||||
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-1/2 p-2.5"
|
||||
placeholder="From"
|
||||
/>
|
||||
/> -->
|
||||
<span>-</span>
|
||||
<input
|
||||
<!-- <input
|
||||
type="number"
|
||||
id="price-to"
|
||||
[(ngModel)]="criteria.maxPrice"
|
||||
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-1/2 p-2.5"
|
||||
placeholder="To"
|
||||
/>
|
||||
/> -->
|
||||
<app-validated-price name="price-to" [(ngModel)]="criteria.maxPrice" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="salesRevenue" class="block mb-2 text-sm font-medium text-gray-900">Sales Revenue</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
<!-- <input
|
||||
type="number"
|
||||
id="salesRevenue-from"
|
||||
[(ngModel)]="criteria.minRevenue"
|
||||
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-1/2 p-2.5"
|
||||
placeholder="From"
|
||||
/>
|
||||
/> -->
|
||||
<app-validated-price name="salesRevenue-from" [(ngModel)]="criteria.minRevenue" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||
<span>-</span>
|
||||
<input
|
||||
<!-- <input
|
||||
type="number"
|
||||
id="salesRevenue-to"
|
||||
[(ngModel)]="criteria.maxRevenue"
|
||||
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-1/2 p-2.5"
|
||||
placeholder="To"
|
||||
/>
|
||||
/> -->
|
||||
<app-validated-price name="salesRevenue-to" [(ngModel)]="criteria.maxRevenue" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="cashflow" class="block mb-2 text-sm font-medium text-gray-900">Cashflow</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
<!-- <input
|
||||
type="number"
|
||||
id="cashflow-from"
|
||||
[(ngModel)]="criteria.minCashFlow"
|
||||
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-1/2 p-2.5"
|
||||
placeholder="From"
|
||||
/>
|
||||
/> -->
|
||||
<app-validated-price name="cashflow-from" [(ngModel)]="criteria.minCashFlow" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||
<span>-</span>
|
||||
<input
|
||||
<app-validated-price name="cashflow-to" [(ngModel)]="criteria.maxCashFlow" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||
<!-- <input
|
||||
type="number"
|
||||
id="cashflow-to"
|
||||
[(ngModel)]="criteria.maxCashFlow"
|
||||
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-1/2 p-2.5"
|
||||
placeholder="To"
|
||||
/>
|
||||
/> -->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -168,15 +187,33 @@
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900">Type of Property</label>
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
<input [(ngModel)]="criteria.realEstateChecked" type="radio" id="realEstateChecked" name="wbs" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500" checked />
|
||||
<input
|
||||
[(ngModel)]="criteria.realEstateChecked"
|
||||
(ngModelChange)="onCheckboxChange('realEstateChecked', $event)"
|
||||
type="checkbox"
|
||||
name="realEstateChecked"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label for="realEstateChecked" class="ml-2 text-sm font-medium text-gray-900">Real Estate</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input [(ngModel)]="criteria.leasedLocation" type="radio" id="leasedLocation" name="wbs" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500" />
|
||||
<input
|
||||
[(ngModel)]="criteria.leasedLocation"
|
||||
(ngModelChange)="onCheckboxChange('leasedLocation', $event)"
|
||||
type="checkbox"
|
||||
name="leasedLocation"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label for="leasedLocation" class="ml-2 text-sm font-medium text-gray-900">Leased Location</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input [(ngModel)]="criteria.franchiseResale" type="radio" id="franchiseResale" name="wbs" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500" />
|
||||
<input
|
||||
[(ngModel)]="criteria.franchiseResale"
|
||||
(ngModelChange)="onCheckboxChange('franchiseResale', $event)"
|
||||
type="checkbox"
|
||||
name="franchiseResale"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label for="franchiseResale" class="ml-2 text-sm font-medium text-gray-900">Franchise</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,7 +278,7 @@
|
||||
<ng-select class="custom" [items]="selectOptions?.states" bindLabel="name" bindValue="value" [ngModel]="criteria.state" (ngModelChange)="setState($event)" name="state"> </ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
|
||||
<!-- <label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[multiple]="false"
|
||||
@@ -257,7 +294,8 @@
|
||||
@for (city of cities$ | async; track city.id) {
|
||||
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</ng-select> -->
|
||||
<app-validated-city label="Location - City" name="city" [ngModel]="criteria.city" (ngModelChange)="setCity($event)" labelClasses="text-gray-900 font-medium" [state]="criteria.state"></app-validated-city>
|
||||
</div>
|
||||
<!-- New section for city search type -->
|
||||
<div *ngIf="criteria.city">
|
||||
@@ -292,21 +330,23 @@
|
||||
<div>
|
||||
<label for="price" class="block mb-2 text-sm font-medium text-gray-900">Price</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
<!-- <input
|
||||
type="number"
|
||||
id="price-from"
|
||||
[(ngModel)]="criteria.minPrice"
|
||||
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-1/2 p-2.5"
|
||||
placeholder="From"
|
||||
/>
|
||||
/> -->
|
||||
<app-validated-price name="price-from" [(ngModel)]="criteria.minPrice" placeholder="From" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||
<span>-</span>
|
||||
<input
|
||||
<app-validated-price name="price-to" [(ngModel)]="criteria.maxPrice" placeholder="To" inputClasses="bg-gray-50 text-sm !mt-0 p-2.5"></app-validated-price>
|
||||
<!-- <input
|
||||
type="number"
|
||||
id="price-to"
|
||||
[(ngModel)]="criteria.maxPrice"
|
||||
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-1/2 p-2.5"
|
||||
placeholder="To"
|
||||
/>
|
||||
/> -->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -363,13 +403,10 @@
|
||||
[typeahead]="countyInput$"
|
||||
[(ngModel)]="criteria.counties"
|
||||
>
|
||||
<!-- @for (county of counties$ | async; track county.id) {
|
||||
<ng-option [value]="city.city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||
} -->
|
||||
</ng-select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
|
||||
<!-- <label for="city" class="block mb-2 text-sm font-medium text-gray-900">Location - City</label>
|
||||
<ng-select
|
||||
class="custom"
|
||||
[multiple]="false"
|
||||
@@ -383,9 +420,17 @@
|
||||
(ngModelChange)="setCity($event)"
|
||||
>
|
||||
@for (city of cities$ | async; track city.id) {
|
||||
<ng-option [value]="city">{{ city.city }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||
<ng-option [value]="city">{{ city.name }} - {{ selectOptions.getStateInitials(city.state) }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</ng-select> -->
|
||||
<app-validated-city
|
||||
label="Company Location - City"
|
||||
name="city"
|
||||
[ngModel]="criteria.city"
|
||||
(ngModelChange)="setCity($event)"
|
||||
labelClasses="text-gray-900 font-medium"
|
||||
[state]="criteria.state"
|
||||
></app-validated-city>
|
||||
</div>
|
||||
<!-- New section for city search type -->
|
||||
<div *ngIf="criteria.city">
|
||||
@@ -401,6 +446,15 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="brokername" class="block mb-2 text-sm font-medium text-gray-900">Name of Professional</label>
|
||||
<input
|
||||
type="text"
|
||||
id="brokername"
|
||||
[(ngModel)]="criteria.brokerName"
|
||||
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||
/>
|
||||
</div>
|
||||
<!-- New section for radius selection -->
|
||||
<div *ngIf="criteria.city && criteria.searchType === 'radius'" class="space-y-2">
|
||||
<label class="block mb-2 text-sm font-medium text-gray-900">Select Radius (in miles)</label>
|
||||
@@ -447,7 +501,7 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="modalService.reject()"
|
||||
(click)="close()"
|
||||
class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10"
|
||||
>
|
||||
Cancel
|
||||
|
||||
@@ -10,25 +10,30 @@ import { ListingsService } from '../../services/listings.service';
|
||||
import { SelectOptionsService } from '../../services/select-options.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { SharedModule } from '../../shared/shared/shared.module';
|
||||
import { resetBusinessListingCriteria, resetCommercialPropertyListingCriteria, resetUserListingCriteria } from '../../utils/utils';
|
||||
import { ValidatedCityComponent } from '../validated-city/validated-city.component';
|
||||
import { ValidatedPriceComponent } from '../validated-price/validated-price.component';
|
||||
import { ModalService } from './modal.service';
|
||||
@UntilDestroy()
|
||||
@Component({
|
||||
selector: 'app-search-modal',
|
||||
standalone: true,
|
||||
imports: [SharedModule, AsyncPipe, NgIf, NgSelectModule],
|
||||
imports: [SharedModule, AsyncPipe, NgIf, NgSelectModule, ValidatedCityComponent, ValidatedPriceComponent],
|
||||
templateUrl: './search-modal.component.html',
|
||||
styleUrl: './search-modal.component.scss',
|
||||
})
|
||||
export class SearchModalComponent {
|
||||
cities$: Observable<GeoResult[]>;
|
||||
// cities$: Observable<GeoResult[]>;
|
||||
counties$: Observable<CountyResult[]>;
|
||||
cityLoading = false;
|
||||
// cityLoading = false;
|
||||
countyLoading = false;
|
||||
cityInput$ = new Subject<string>();
|
||||
// cityInput$ = new Subject<string>();
|
||||
countyInput$ = new Subject<string>();
|
||||
private criteriaChangeSubscription: Subscription;
|
||||
public criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
|
||||
backupCriteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
|
||||
numberOfResults$: Observable<number>;
|
||||
cancelDisable = false;
|
||||
constructor(
|
||||
public selectOptions: SelectOptionsService,
|
||||
public modalService: ModalService,
|
||||
@@ -41,6 +46,7 @@ export class SearchModalComponent {
|
||||
this.setupCriteriaChangeListener();
|
||||
this.modalService.message$.pipe(untilDestroyed(this)).subscribe(msg => {
|
||||
this.criteria = msg;
|
||||
this.backupCriteria = JSON.parse(JSON.stringify(msg));
|
||||
this.setTotalNumberOfResults();
|
||||
});
|
||||
this.modalService.modalVisible$.pipe(untilDestroyed(this)).subscribe(val => {
|
||||
@@ -49,7 +55,7 @@ export class SearchModalComponent {
|
||||
this.criteria.start = 0;
|
||||
}
|
||||
});
|
||||
this.loadCities();
|
||||
// this.loadCities();
|
||||
this.loadCounties();
|
||||
}
|
||||
|
||||
@@ -64,22 +70,6 @@ export class SearchModalComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
private loadCounties() {
|
||||
this.counties$ = concat(
|
||||
of([]), // default items
|
||||
@@ -98,7 +88,7 @@ export class SearchModalComponent {
|
||||
}
|
||||
setCity(city) {
|
||||
if (city) {
|
||||
this.criteria.city = city.city;
|
||||
this.criteria.city = city;
|
||||
this.criteria.state = city.state;
|
||||
} else {
|
||||
this.criteria.city = null;
|
||||
@@ -115,7 +105,10 @@ export class SearchModalComponent {
|
||||
}
|
||||
}
|
||||
private setupCriteriaChangeListener() {
|
||||
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => this.setTotalNumberOfResults());
|
||||
this.criteriaChangeSubscription = this.criteriaChangeService.criteriaChange$.pipe(debounceTime(400)).subscribe(() => {
|
||||
this.setTotalNumberOfResults();
|
||||
this.cancelDisable = true;
|
||||
});
|
||||
}
|
||||
trackByFn(item: GeoResult) {
|
||||
return item.id;
|
||||
@@ -147,4 +140,25 @@ export class SearchModalComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
clearFilter() {
|
||||
if (this.criteria.criteriaType === 'businessListings') {
|
||||
resetBusinessListingCriteria(this.criteria);
|
||||
} else if (this.criteria.criteriaType === 'commercialPropertyListings') {
|
||||
resetCommercialPropertyListingCriteria(this.criteria);
|
||||
} else {
|
||||
resetUserListingCriteria(this.criteria);
|
||||
}
|
||||
}
|
||||
close() {
|
||||
this.modalService.reject(this.backupCriteria);
|
||||
}
|
||||
onCheckboxChange(checkbox: string, value: boolean) {
|
||||
// Deaktivieren Sie alle Checkboxes
|
||||
(<BusinessListingCriteria>this.criteria).realEstateChecked = false;
|
||||
(<BusinessListingCriteria>this.criteria).leasedLocation = false;
|
||||
(<BusinessListingCriteria>this.criteria).franchiseResale = false;
|
||||
|
||||
// Aktivieren Sie nur die aktuell ausgewählte Checkbox
|
||||
this.criteria[checkbox] = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div
|
||||
[id]="id"
|
||||
role="tooltip"
|
||||
class="max-w-72 w-max absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700"
|
||||
class="max-w-72 w-max absolute z-50 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700"
|
||||
>
|
||||
{{ text }}
|
||||
<div class="tooltip-arrow" data-popper-arrow></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Component, Input, SimpleChanges } from '@angular/core';
|
||||
import { initFlowbite } from 'flowbite';
|
||||
|
||||
@Component({
|
||||
@@ -9,11 +9,36 @@ import { initFlowbite } from 'flowbite';
|
||||
templateUrl: './tooltip.component.html',
|
||||
})
|
||||
export class TooltipComponent {
|
||||
@Input() id;
|
||||
@Input() text;
|
||||
@Input() id: string;
|
||||
@Input() text: string;
|
||||
@Input() isVisible: boolean = false;
|
||||
|
||||
ngOnInit() {
|
||||
this.initializeTooltip();
|
||||
}
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes['isVisible']) {
|
||||
this.updateTooltipVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
private initializeTooltip() {
|
||||
setTimeout(() => {
|
||||
initFlowbite();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
private updateTooltipVisibility() {
|
||||
const tooltipElement = document.getElementById(this.id);
|
||||
if (tooltipElement) {
|
||||
if (this.isVisible) {
|
||||
tooltipElement.classList.remove('invisible', 'opacity-0');
|
||||
tooltipElement.classList.add('visible', 'opacity-100');
|
||||
} else {
|
||||
tooltipElement.classList.remove('visible', 'opacity-100');
|
||||
tooltipElement.classList.add('invisible', 'opacity-0');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<div>
|
||||
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit"
|
||||
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit {{ labelClasses }}"
|
||||
>{{ label }} @if(validationMessage){
|
||||
<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"
|
||||
(click)="toggleTooltip($event)"
|
||||
(touchstart)="toggleTooltip($event)"
|
||||
>
|
||||
!
|
||||
</div>
|
||||
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage"></app-tooltip>
|
||||
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
|
||||
}
|
||||
</label>
|
||||
<ng-select
|
||||
@@ -19,11 +21,11 @@
|
||||
[loading]="cityLoading"
|
||||
typeToSearchText="Please enter 2 or more characters"
|
||||
[typeahead]="cityInput$"
|
||||
ngModel="{{ value?.city }} {{ value ? '-' : '' }} {{ value?.state }}"
|
||||
ngModel="{{ value?.name }} {{ 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-option [value]="city">{{ city.name }} - {{ city.state }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
: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;
|
||||
// height: 42px;
|
||||
border-radius: 0.5rem;
|
||||
.ng-value-container .ng-input {
|
||||
top: 10px;
|
||||
|
||||
@@ -27,6 +27,8 @@ import { ValidationMessagesService } from '../validation-messages.service';
|
||||
})
|
||||
export class ValidatedCityComponent extends BaseInputComponent {
|
||||
@Input() items;
|
||||
@Input() labelClasses: string;
|
||||
@Input() state: string;
|
||||
cities$: Observable<GeoResult[]>;
|
||||
cityInput$ = new Subject<string>();
|
||||
countyInput$ = new Subject<string>();
|
||||
@@ -50,7 +52,7 @@ export class ValidatedCityComponent extends BaseInputComponent {
|
||||
distinctUntilChanged(),
|
||||
tap(() => (this.cityLoading = true)),
|
||||
switchMap(term =>
|
||||
this.geoService.findCitiesStartingWith(term).pipe(
|
||||
this.geoService.findCitiesStartingWith(term, this.state).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)),
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<div>
|
||||
@if(label){
|
||||
<label for="type" class="block text-sm font-bold text-gray-700 mb-1 relative w-fit {{ labelClasses }}"
|
||||
>{{ 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"
|
||||
(click)="toggleTooltip($event)"
|
||||
(touchstart)="toggleTooltip($event)"
|
||||
>
|
||||
!
|
||||
</div>
|
||||
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage" [isVisible]="isTooltipVisible"></app-tooltip>
|
||||
}
|
||||
</label>
|
||||
}
|
||||
<ng-select
|
||||
class="custom"
|
||||
[multiple]="false"
|
||||
[hideSelected]="true"
|
||||
[trackByFn]="trackByFn"
|
||||
[minTermLength]="2"
|
||||
[loading]="countyLoading"
|
||||
typeToSearchText="Please enter 2 or more characters"
|
||||
[typeahead]="countyInput$"
|
||||
ngModel="{{ value }}"
|
||||
(ngModelChange)="onInputChange($event)"
|
||||
[readonly]="readonly"
|
||||
>
|
||||
@for (county of counties$ | async; track county.id) {
|
||||
<ng-option [value]="county">{{ county }}</ng-option>
|
||||
}
|
||||
</ng-select>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user