Compare commits
82 Commits
e784b424b0
...
tailwind
| Author | SHA1 | Date | |
|---|---|---|---|
| 69b0a83b1e | |||
| 7df5d32cc4 | |||
| 1a77656d8a | |||
| b1f26fbf48 | |||
| 3795a5a30c | |||
| 8698aa3e66 | |||
| 398f8d29ca | |||
| 8b71b073be | |||
| 4c1b1fbc87 | |||
| f58448679d | |||
| c9305749d2 | |||
| 29f88d610f | |||
| 2955c034a0 | |||
| 55e800009e | |||
| 6348af8862 | |||
| a6ae643458 | |||
| 38e943c18e | |||
| acec14d372 | |||
| 9db23c2177 | |||
| abcde3991d | |||
| f88eebe8d3 | |||
| bdafb03165 | |||
| af982d19d8 | |||
| b7b34dacab | |||
| bf4bd69337 | |||
| b4644ea295 | |||
| 7bd5e1aaf8 | |||
| 08c179fa09 | |||
| 7f67b81242 | |||
| e0dbebb61c | |||
| 677b95c21c | |||
| 1534c14a68 | |||
| 5fa2dd60fa | |||
| 9228cbebbe | |||
| 1ccd1d174c | |||
| 958f0afd9b | |||
| 3d5b7e3f39 | |||
| d488f90f48 | |||
| 044f8efa0f | |||
| 24a3d210f0 | |||
| 2465b8966b | |||
| e87222d3c1 | |||
| 902ab9caed | |||
| 44acbcd4d0 | |||
| b4cf17b8ea | |||
| 226d2ebc1e | |||
| 0473f74241 | |||
| c9d94e973a | |||
| f9d9c6ad9e | |||
| 5dc893da38 | |||
| c471629c6d | |||
| 13fb3cd4b8 | |||
| d6768b3da9 | |||
| 7fdc87fb0b | |||
| 0b7e33612a | |||
| 8fba3aa832 | |||
| 214327031c | |||
| dc9adb151d | |||
| 747435bfba | |||
| 782c254a33 | |||
| df4e2b00e2 | |||
| 0684b9534f | |||
| e0ecea5af2 | |||
| 327aef0f21 | |||
| cb73daf863 | |||
| 08c53e2eb2 | |||
| 492c03c2be | |||
| f51a298227 | |||
| 474d7c63d5 | |||
| d2e5562602 | |||
| aff55c5433 | |||
| 5230ef1230 | |||
| d508415de4 | |||
| 6b61c19bd7 | |||
| bb5a408cdc | |||
| 9121ca1a69 | |||
| 4230867608 | |||
| 9e03620be7 | |||
| 7f0f21b598 | |||
| c90d6b72b7 | |||
| c4cdcf4505 | |||
| 7d10080069 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -48,6 +48,9 @@ public
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.prod
|
||||
.env.dev
|
||||
.env.test
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
@@ -62,7 +65,6 @@ pids
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
*.js
|
||||
*.map
|
||||
package-lock.json
|
||||
|
||||
|
||||
16
bizmatch-server/.editorconfig
Normal file
16
bizmatch-server/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
25
bizmatch-server/.eslintrc.js
Normal file
25
bizmatch-server/.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
45
bizmatch-server/.eslintrc.json
Normal file
45
bizmatch-server/.eslintrc.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"browser": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"airbnb-typescript",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"eslint-config-prettier",
|
||||
"plugin:cypress/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module",
|
||||
"project": ["./tsconfig.json"]
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"import/no-unresolved": ["off"],
|
||||
"import/prefer-default-export": ["off"],
|
||||
"no-useless-constructor": "off",
|
||||
"@typescript-eslint/no-useless-constructor": ["error"],
|
||||
"@typescript-eslint/lines-between-class-members": ["off"],
|
||||
"no-param-reassign": ["off"],
|
||||
"max-classes-per-file": ["off"],
|
||||
"no-shadow": ["off"],
|
||||
"class-methods-use-this": ["off"],
|
||||
"react/jsx-filename-extension": ["off"],
|
||||
"import/no-cycle": ["off"],
|
||||
"radix": ["off"],
|
||||
"no-promise-executor-return": ["off"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "enumMember",
|
||||
"format": ["UPPER_CASE", "PascalCase"]
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
|
||||
"spaced-comment": ["off"],
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
|
||||
}
|
||||
}
|
||||
4
bizmatch-server/.gitignore
vendored
4
bizmatch-server/.gitignore
vendored
@@ -56,3 +56,7 @@ pids
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
pictures
|
||||
pictures_base
|
||||
|
||||
src/*.js
|
||||
bun.lockb
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 220,
|
||||
"proseWrap": "always",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
54
bizmatch-server/.vscode/launch.json
vendored
54
bizmatch-server/.vscode/launch.json
vendored
@@ -6,17 +6,59 @@
|
||||
"request": "launch",
|
||||
"name": "Debug Nest Framework",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"start:debug",
|
||||
"--",
|
||||
"--inspect-brk"
|
||||
],
|
||||
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"restart": true,
|
||||
"sourceMaps": true,
|
||||
"stopOnEntry": false,
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"HOST_NAME": "localhost"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch TypeScript file with tsx",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["tsx", "--inspect"],
|
||||
"args": ["${workspaceFolder}/src/drizzle/import.ts"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/src/**/*.ts", "!**/node_modules/**"],
|
||||
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**/*.js"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch TypeScript file",
|
||||
"runtimeArgs": ["-r", "ts-node/register", "-r", "tsconfig-paths/register"],
|
||||
"args": ["${workspaceFolder}/src/drizzle/import.ts"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"protocol": "inspector",
|
||||
"outFiles": ["${workspaceFolder}/**/*.js"],
|
||||
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**/*.js"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "generateDefs",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
|
||||
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||
"sourceMaps": true,
|
||||
"smartStep": true
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "generateTypes",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
|
||||
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||
"sourceMaps": true,
|
||||
"smartStep": true
|
||||
}
|
||||
]
|
||||
}
|
||||
28
bizmatch-server/.vscode/settings.json
vendored
Normal file
28
bizmatch-server/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"editor.suggestSelection": "first",
|
||||
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
|
||||
"explorer.confirmDelete": false,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"prettier.printWidth": 240,
|
||||
"git.autofetch": false,
|
||||
"git.autorefresh": true
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
FT.CREATE listingsIndex ON JSON PREFIX 1 listings: SCHEMA $.location AS location TAG SORTABLE $.price AS price NUMERIC SORTABLE $.listingsCategory AS listingsCategory TAG SORTABLE $.type AS type TAG SORTABLE
|
||||
1326
bizmatch-server/broker.json
Normal file
1326
bizmatch-server/broker.json
Normal file
File diff suppressed because it is too large
Load Diff
1371
bizmatch-server/data/broker.json
Normal file
1371
bizmatch-server/data/broker.json
Normal file
File diff suppressed because it is too large
Load Diff
12493
bizmatch-server/data/businesses.json
Normal file
12493
bizmatch-server/data/businesses.json
Normal file
File diff suppressed because it is too large
Load Diff
2858
bizmatch-server/data/commercials.json
Normal file
2858
bizmatch-server/data/commercials.json
Normal file
File diff suppressed because it is too large
Load Diff
239
bizmatch-server/dbschema.ts
Normal file
239
bizmatch-server/dbschema.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/* 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,
|
||||
}
|
||||
12
bizmatch-server/drizzle.config.ts
Normal file
12
bizmatch-server/drizzle.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
export default defineConfig({
|
||||
schema: './src/drizzle/schema.ts',
|
||||
out: './src/drizzle/migrations',
|
||||
dialect: 'postgresql',
|
||||
// driver: 'pg',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
1075
bizmatch-server/importlog.txt
Normal file
1075
bizmatch-server/importlog.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,10 @@
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"assets": ["assets/**/*","**/*.hbs"],
|
||||
"assets": [
|
||||
"assets/**/*",
|
||||
"**/*.hbs"
|
||||
],
|
||||
"watchAssets": true
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,21 @@
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start": "HOST_NAME=localhost nest start",
|
||||
"start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"start:prod": "HOST_NAME=www.bizmatch.net node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"generate": "drizzle-kit generate",
|
||||
"drop": "drizzle-kit drop",
|
||||
"migrate": "tsx src/drizzle/migrate.ts",
|
||||
"import": "tsx src/drizzle/import.ts",
|
||||
"generateTypes": "tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.10.3",
|
||||
@@ -29,24 +34,35 @@
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"drizzle-orm": "^0.32.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"groq-sdk": "^0.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"ky": "^1.2.0",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"ky": "^1.4.0",
|
||||
"nest-winston": "^1.9.4",
|
||||
"nodemailer": "^6.9.10",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"openai": "^4.52.6",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"redis": "^4.6.13",
|
||||
"redis-om": "^0.4.3",
|
||||
"pg": "^8.11.5",
|
||||
"pgvector": "^0.2.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.2",
|
||||
"tsx": "^4.16.2",
|
||||
"urlcat": "^3.1.0",
|
||||
"winston": "^3.11.0"
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/traverse": "^7.24.1",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
@@ -58,19 +74,26 @@
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/pg": "^8.11.5",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"drizzle-kit": "^0.23.0",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"kysely-codegen": "^0.15.0",
|
||||
"pg-to-ts": "^4.1.1",
|
||||
"prettier": "^3.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
|
||||
12
bizmatch-server/src/ai/ai.controller.ts
Normal file
12
bizmatch-server/src/ai/ai.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AiService } from './ai.service.js';
|
||||
|
||||
@Controller('ai')
|
||||
export class AiController {
|
||||
constructor(private readonly aiService: AiService) {}
|
||||
|
||||
@Post()
|
||||
async getBusinessCriteria(@Body('query') query: string) {
|
||||
return this.aiService.getBusinessCriteria(query);
|
||||
}
|
||||
}
|
||||
9
bizmatch-server/src/ai/ai.module.ts
Normal file
9
bizmatch-server/src/ai/ai.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiController } from './ai.controller.js';
|
||||
import { AiService } from './ai.service.js';
|
||||
|
||||
@Module({
|
||||
controllers: [AiController],
|
||||
providers: [AiService],
|
||||
})
|
||||
export class AiModule {}
|
||||
94
bizmatch-server/src/ai/ai.service.ts
Normal file
94
bizmatch-server/src/ai/ai.service.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
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'",
|
||||
};
|
||||
@Injectable()
|
||||
export class AiService {
|
||||
private readonly openai: OpenAI;
|
||||
private readonly groq: Groq;
|
||||
constructor() {
|
||||
this.openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY, // Verwenden Sie Umgebungsvariablen für den API-Schlüssel
|
||||
});
|
||||
this.groq = new Groq({ apiKey: process.env.GROQ_API_KEY });
|
||||
}
|
||||
|
||||
async getBusinessCriteria(query: string): Promise<BusinessListingCriteria> {
|
||||
// const prompt = `
|
||||
// Dieses Objekt ist wie folgt definiert: ${JSON.stringify(businessListingCriteriaStructure)}.
|
||||
// Die Antwort darf nur das von dir befüllte JSON als unformatierten Text enthalten so das es von mir mit JSON.parse() einlesbar ist!!!!
|
||||
// Falls es Ortsangaben gibt, dann befülle City, County und State wenn möglich Die Suchanfrage des Users lautet: "${query}"`;
|
||||
const prompt = `The Search Query of the User is: "${query}"`;
|
||||
let response = null;
|
||||
try {
|
||||
// response = await this.openai.chat.completions.create({
|
||||
// model: 'gpt-4o-mini',
|
||||
// //model: 'gpt-3.5-turbo',
|
||||
// max_tokens: 300,
|
||||
// messages: [
|
||||
// {
|
||||
// role: 'system',
|
||||
// content: `Please create unformatted JSON Object from a user input.
|
||||
// The type is: ${JSON.stringify(businessListingCriteriaStructure)}.,
|
||||
// If location details available please fill city, county and state as State Code`,
|
||||
// },
|
||||
// ],
|
||||
// temperature: 0.5,
|
||||
// response_format: { type: 'json_object' },
|
||||
// });
|
||||
|
||||
response = await this.groq.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
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`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
model: 'llama-3.1-70b-versatile',
|
||||
//model: 'llama-3.1-8b-instant',
|
||||
temperature: 0.2,
|
||||
max_tokens: 300,
|
||||
response_format: { type: 'json_object' },
|
||||
});
|
||||
|
||||
const generatedCriteria = JSON.parse(response.choices[0]?.message?.content);
|
||||
return generatedCriteria;
|
||||
|
||||
// return response.choices[0]?.message?.content;
|
||||
} catch (error) {
|
||||
console.error(`Error calling GPT-4 API: ${response.choices[0]}`, error);
|
||||
throw new Error('Failed to generate business criteria');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
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';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
constructor(
|
||||
private readonly appService: AppService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
getHello(@Request() req): string {
|
||||
return req.user;
|
||||
//return 'dfgdf';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,57 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module } 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 * as winston from 'winston';
|
||||
import { AiModule } from './ai/ai.module.js';
|
||||
import { AppController } from './app.controller.js';
|
||||
import { AppService } from './app.service.js';
|
||||
import { FileService } from './file/file.service.js';
|
||||
import { AuthService } from './auth/auth.service.js';
|
||||
import { AuthController } from './auth/auth.controller.js';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { SelectOptionsController } from './select-options/select-options.controller.js';
|
||||
import { SelectOptionsService } from './select-options/select-options.service.js';
|
||||
import { SubscriptionsController } from './subscriptions/subscriptions.controller.js';
|
||||
import { RedisModule } from './redis/redis.module.js';
|
||||
import { ListingsService } from './listings/listings.service.js';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
import { MailModule } from './mail/mail.module.js';
|
||||
import { AuthModule } from './auth/auth.module.js';
|
||||
import { FileService } from './file/file.service.js';
|
||||
import { GeoModule } from './geo/geo.module.js';
|
||||
import { UserModule } from './user/user.module.js';
|
||||
import { ListingsModule } from './listings/listings.module.js';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
||||
import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js';
|
||||
import { ImageModule } from './image/image.module.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
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);
|
||||
|
||||
function loadEnvFiles() {
|
||||
// Load the .env file
|
||||
dotenv.config();
|
||||
console.log('Loaded .env file');
|
||||
|
||||
// Determine which additional env file to load
|
||||
let envFilePath = '';
|
||||
const host = process.env.HOST_NAME || '';
|
||||
|
||||
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();
|
||||
@Module({
|
||||
imports: [ConfigModule.forRoot(), MailModule, AuthModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '..', 'pictures'), // `public` ist das Verzeichnis, wo Ihre statischen Dateien liegen
|
||||
}),
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
MailModule,
|
||||
AuthModule,
|
||||
WinstonModule.forRoot({
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
@@ -51,10 +72,15 @@ const __dirname = path.dirname(__filename);
|
||||
UserModule,
|
||||
ListingsModule,
|
||||
SelectOptionsModule,
|
||||
RedisModule,
|
||||
ImageModule
|
||||
ImageModule,
|
||||
PassportModule,
|
||||
AiModule,
|
||||
],
|
||||
controllers: [AppController, SubscriptionsController],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, FileService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(RequestDurationMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
||||
3597
bizmatch-server/src/assets/counties.json
Normal file
3597
bizmatch-server/src/assets/counties.json
Normal file
File diff suppressed because it is too large
Load Diff
63211
bizmatch-server/src/assets/counties_raw.csv
Normal file
63211
bizmatch-server/src/assets/counties_raw.csv
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
119805
bizmatch-server/src/assets/geo1.json
Normal file
119805
bizmatch-server/src/assets/geo1.json
Normal file
File diff suppressed because it is too large
Load Diff
119811
bizmatch-server/src/assets/geo_.json
Normal file
119811
bizmatch-server/src/assets/geo_.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import path, { join } from 'path';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { AuthService } from './auth.service.js';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtStrategy } from '../jwt.strategy.js';
|
||||
import { AuthController } from './auth.controller.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
import { AuthService } from './auth.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
],
|
||||
providers: [AuthService],
|
||||
imports: [PassportModule],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
controllers: [AuthController],
|
||||
exports:[AuthService]
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
// import got from 'got';
|
||||
import ky from 'ky';
|
||||
import urlcat from 'urlcat';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
|
||||
public async getAccessToken() {
|
||||
const form = new FormData();
|
||||
form.append('grant_type', 'password');
|
||||
@@ -19,13 +17,15 @@ export class AuthService {
|
||||
params.append('password', process.env.password);
|
||||
const URL = `${process.env.host}${process.env.tokenURL}`;
|
||||
|
||||
const response = await ky.post(URL, {
|
||||
const response = await ky
|
||||
.post(URL, {
|
||||
body: params.toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization':'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3'
|
||||
Authorization: 'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3',
|
||||
},
|
||||
}).json();
|
||||
})
|
||||
.json();
|
||||
return (<any>response).access_token;
|
||||
} catch (error) {
|
||||
if (error.name === 'HTTPError') {
|
||||
@@ -40,68 +40,80 @@ export class AuthService {
|
||||
public async getUsers() {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.usersURL}`;
|
||||
const response = await ky.get(URL, {
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization':`Bearer ${token}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).json();
|
||||
return response
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async getUser(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host,process.env.userURL,{userid})
|
||||
const response = await ky.get(URL, {
|
||||
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}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).json();
|
||||
return response
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async getGroups() {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.groupsURL}`;
|
||||
const response = await ky.get(URL, {
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization':`Bearer ${token}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).json();
|
||||
return response
|
||||
})
|
||||
.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, {
|
||||
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}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).json();
|
||||
return response
|
||||
})
|
||||
.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, {
|
||||
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}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).json();
|
||||
return response
|
||||
})
|
||||
.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, {
|
||||
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}`
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}).json();
|
||||
return response
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
27
bizmatch-server/src/drizzle/drizzle.module.ts
Normal file
27
bizmatch-server/src/drizzle/drizzle.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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';
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: PG_CONNECTION,
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
const connectionString = configService.get<string>('DATABASE_URL');
|
||||
const pool = new Pool({
|
||||
connectionString,
|
||||
// ssl: true,
|
||||
});
|
||||
|
||||
return drizzle(pool, { schema, logger:true });
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [PG_CONNECTION],
|
||||
})
|
||||
export class DrizzleModule {}
|
||||
143
bizmatch-server/src/drizzle/generateTypes.ts
Normal file
143
bizmatch-server/src/drizzle/generateTypes.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import fs from 'fs';
|
||||
import ts from 'typescript';
|
||||
export const TABLE_BO_MAPPING = {
|
||||
'users':'User',
|
||||
'businesses':'BusinessListing',
|
||||
'commercials':'CommercialPropertyListing'
|
||||
}
|
||||
function generateInterfaceDefinitions(inputFile: string, outputFile: string): void {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
inputFile,
|
||||
fs.readFileSync(inputFile, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
let interfaceDefinitions = '';
|
||||
|
||||
ts.forEachChild(sourceFile, (node) => {
|
||||
if (ts.isVariableStatement(node)) {
|
||||
const variableDeclaration = node.declarationList.declarations[0];
|
||||
if (variableDeclaration.initializer && ts.isCallExpression(variableDeclaration.initializer)) {
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (initializer.expression.getText(sourceFile) === 'pgTable') {
|
||||
const tableName = initializer.arguments[0].getText(sourceFile).slice(1, -1); // Entfernen von zusätzlichen Anführungszeichen
|
||||
const tableDefinition = initializer.arguments[1];
|
||||
|
||||
const interfaceName = TABLE_BO_MAPPING[tableName] ? TABLE_BO_MAPPING[tableName] : tableName.charAt(0).toUpperCase() + tableName.slice(1);
|
||||
let interfaceDefinition = `export interface ${interfaceName} {\n`;
|
||||
|
||||
ts.forEachChild(tableDefinition, (propertyNode) => {
|
||||
if (ts.isPropertyAssignment(propertyNode)) {
|
||||
const propertyName = propertyNode.name.getText(sourceFile);
|
||||
const propertyDefinition = propertyNode.initializer;
|
||||
|
||||
if (ts.isCallExpression(propertyDefinition)) {
|
||||
const propertyType = getPropertyType(propertyDefinition, sourceFile);
|
||||
const isOptional = !hasNonOptionalModifier(propertyDefinition.expression);
|
||||
interfaceDefinition += ` ${propertyName}${isOptional ? '?' : ''}: ${propertyType};\n`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interfaceDefinition += '}\n\n';
|
||||
interfaceDefinitions += interfaceDefinition;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fs.writeFileSync(outputFile, interfaceDefinitions);
|
||||
console.log(`Interface definitions generated successfully. Output file: ${outputFile}`);
|
||||
}
|
||||
|
||||
function getPropertyType(propertyDefinition: ts.CallExpression, sourceFile: ts.SourceFile): string {
|
||||
const typeFunction = getTypeFunctionName(propertyDefinition.expression, sourceFile);
|
||||
|
||||
let propertyType = '';
|
||||
|
||||
switch (typeFunction) {
|
||||
case 'varchar':
|
||||
case 'char':
|
||||
case 'text':
|
||||
case 'uuid':
|
||||
propertyType = 'string';
|
||||
break;
|
||||
case 'integer':
|
||||
case 'numeric':
|
||||
case 'real':
|
||||
case 'doublePrecision':
|
||||
propertyType = 'number';
|
||||
break;
|
||||
case 'boolean':
|
||||
propertyType = 'boolean';
|
||||
break;
|
||||
case 'timestamp':
|
||||
propertyType = 'Date';
|
||||
break;
|
||||
default:
|
||||
propertyType = 'any';
|
||||
}
|
||||
|
||||
const isArray = hasArrayModifier(propertyDefinition.expression);
|
||||
return isArray ? `${propertyType}[]` : propertyType;
|
||||
}
|
||||
|
||||
function getTypeFunctionName(expression: ts.Expression, sourceFile: ts.SourceFile): string {
|
||||
// Prüfen, ob die aktuelle Expression ein Identifier ist (SyntaxKind.Identifier hat den Wert 80)
|
||||
if (expression.kind === ts.SyntaxKind.Identifier) {
|
||||
return expression.getText(sourceFile);
|
||||
}
|
||||
|
||||
// Wenn die Expression eine CallExpression oder eine PropertyAccessExpression ist,
|
||||
// gehe zur nächsten Expression-Ebene
|
||||
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
|
||||
return getTypeFunctionName(expression.expression, sourceFile);
|
||||
}
|
||||
|
||||
// Falls ein nicht unterstützter Expression-Typ vorliegt, gibt 'unknown' zurück
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function hasArrayModifier(expression: ts.Expression): boolean {
|
||||
// Prüfe, ob die aktuelle Expression eine CallExpression ist und der Funktionsname 'array' ist
|
||||
if (ts.isPropertyAccessExpression(expression) && expression.name.getText() === 'array') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wenn die Expression eine weitere CallExpression oder PropertyAccessExpression ist,
|
||||
// prüfe rekursiv die nächste Ebene
|
||||
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
|
||||
return hasArrayModifier(expression.expression);
|
||||
}
|
||||
|
||||
// Wenn keine weitere Ebene oder kein Array-Modifier gefunden wurde, gib false zurück
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasNonOptionalModifier(expression: ts.Expression): boolean {
|
||||
// Prüfe, ob die aktuelle Expression eine CallExpression ist und der Funktionsname 'notNull' ist
|
||||
if (ts.isPropertyAccessExpression(expression) && (expression.name.getText() === 'notNull' || expression.name.getText() === 'primaryKey')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wenn die Expression eine weitere CallExpression oder PropertyAccessExpression ist,
|
||||
// prüfe rekursiv die nächste Ebene
|
||||
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
|
||||
return hasNonOptionalModifier(expression.expression);
|
||||
}
|
||||
|
||||
// Wenn keine weitere Ebene oder kein NotNull-Modifier gefunden wurde, gib false zurück
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hauptprogramm
|
||||
const inputFile = process.argv[2]|| './src/drizzle/schema.ts';
|
||||
const outputFile = process.argv[3] || './model.ts';
|
||||
|
||||
if (!inputFile) {
|
||||
console.error('Please provide an input file.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
generateInterfaceDefinitions(inputFile, outputFile);
|
||||
345
bizmatch-server/src/drizzle/import.ts
Normal file
345
bizmatch-server/src/drizzle/import.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
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 { rimraf } from 'rimraf';
|
||||
import sharp from 'sharp';
|
||||
import { BusinessListingService } from 'src/listings/business-listing.service.js';
|
||||
import { CommercialPropertyService } from 'src/listings/commercial-property.service.js';
|
||||
import { Geo } from 'src/models/server.model.js';
|
||||
import winston from 'winston';
|
||||
import { 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';
|
||||
interface PropertyImportListing {
|
||||
id: string;
|
||||
userId: string;
|
||||
listingsCategory: 'commercialProperty';
|
||||
title: string;
|
||||
state: string;
|
||||
hasImages: boolean;
|
||||
price: number;
|
||||
city: string;
|
||||
description: string;
|
||||
type: number;
|
||||
imageOrder: any[];
|
||||
}
|
||||
interface BusinessImportListing {
|
||||
userId: string;
|
||||
listingsCategory: 'business';
|
||||
title: string;
|
||||
description: string;
|
||||
type: number;
|
||||
state: string;
|
||||
city: string;
|
||||
id: string;
|
||||
price: number;
|
||||
salesRevenue: number;
|
||||
leasedLocation: boolean;
|
||||
established: number;
|
||||
employees: number;
|
||||
reasonForSale: string;
|
||||
supportAndTraining: string;
|
||||
cashFlow: number;
|
||||
brokerLicencing: string;
|
||||
internalListingNumber: number;
|
||||
realEstateIncluded: boolean;
|
||||
franchiseResale: boolean;
|
||||
draft: boolean;
|
||||
internals: string;
|
||||
created: string;
|
||||
}
|
||||
const typesOfBusiness: Array<KeyValueStyle> = [
|
||||
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||
{ 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'] });
|
||||
});
|
||||
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 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 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;
|
||||
|
||||
commercial.email = user.email;
|
||||
commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercialJsonData[index].type)).value;
|
||||
commercial.title = commercialJsonData[index].title;
|
||||
commercial.description = commercialJsonData[index].description;
|
||||
try {
|
||||
const cityGeo = geos.states.find(s => s.state_code === commercialJsonData[index].state).cities.find(c => c.name === commercialJsonData[index].city);
|
||||
commercial.location = {};
|
||||
commercial.location.latitude = cityGeo.latitude;
|
||||
commercial.location.longitude = cityGeo.longitude;
|
||||
commercial.location.city = commercialJsonData[index].city;
|
||||
commercial.location.state = commercialJsonData[index].state;
|
||||
// console.log(JSON.stringify(commercial.location));
|
||||
} catch (e) {
|
||||
console.log(`----------------> ERROR ${commercialJsonData[index].state} - ${commercialJsonData[index].city}`);
|
||||
continue;
|
||||
}
|
||||
commercial.price = commercialJsonData[index].price;
|
||||
commercial.listingsCategory = 'commercialProperty';
|
||||
commercial.draft = false;
|
||||
commercial.imageOrder = getFilenames(id);
|
||||
commercial.imagePath = emailToDirName(user.email);
|
||||
const insertionDate = getRandomDateWithinLastYear();
|
||||
commercial.created = insertionDate;
|
||||
commercial.updated = insertionDate;
|
||||
|
||||
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.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;
|
||||
}
|
||||
|
||||
function getRandomItem<T>(arr: T[]): T {
|
||||
if (arr.length === 0) {
|
||||
throw new Error('The array is empty.');
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||
return arr[randomIndex];
|
||||
}
|
||||
function getFilenames(id: string): string[] {
|
||||
try {
|
||||
let filePath = `./pictures_base/property/${id}`;
|
||||
return readdirSync(filePath);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function getRandomDateWithinLastYear(): Date {
|
||||
const currentDate = new Date();
|
||||
const lastYear = new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), currentDate.getDate());
|
||||
|
||||
const timeDiff = currentDate.getTime() - lastYear.getTime();
|
||||
const randomTimeDiff = Math.random() * timeDiff;
|
||||
const randomDate = new Date(lastYear.getTime() + randomTimeDiff);
|
||||
|
||||
return randomDate;
|
||||
}
|
||||
async function storeProfilePicture(buffer: Buffer, userId: string) {
|
||||
let quality = 50;
|
||||
const output = await sharp(buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
||||
}
|
||||
|
||||
async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const output = await sharp(buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||
}
|
||||
|
||||
function deleteFilesOfDir(directoryPath) {
|
||||
// Überprüfen, ob das Verzeichnis existiert
|
||||
if (existsSync(directoryPath)) {
|
||||
// Den Inhalt des Verzeichnisses synchron löschen
|
||||
try {
|
||||
readdirSync(directoryPath).forEach(file => {
|
||||
const filePath = join(directoryPath, file);
|
||||
// Wenn es sich um ein Verzeichnis handelt, rekursiv löschen
|
||||
if (statSync(filePath).isDirectory()) {
|
||||
rimraf.sync(filePath);
|
||||
} else {
|
||||
// Wenn es sich um eine Datei handelt, direkt löschen
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
console.log('Der Inhalt des Verzeichnisses wurde erfolgreich gelöscht.');
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Löschen des Verzeichnisses:', err);
|
||||
}
|
||||
} else {
|
||||
console.log('Das Verzeichnis existiert nicht.');
|
||||
}
|
||||
}
|
||||
12
bizmatch-server/src/drizzle/migrate.ts
Normal file
12
bizmatch-server/src/drizzle/migrate.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pkg from 'pg';
|
||||
import * as schema from './schema.js';
|
||||
const { Pool } = pkg;
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
const pool = new Pool({ connectionString });
|
||||
const db = drizzle(pool, { schema });
|
||||
// This will run migrations on the database, skipping the ones already applied
|
||||
//await migrate(db, { migrationsFolder: './src/drizzle/migrations' });
|
||||
// Don't forget to close the connection, otherwise the script will hang
|
||||
//await pool.end();
|
||||
114
bizmatch-server/src/drizzle/migrations/0000_lean_marvex.sql
Normal file
114
bizmatch-server/src/drizzle/migrations/0000_lean_marvex.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
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 $$;
|
||||
541
bizmatch-server/src/drizzle/migrations/meta/0000_snapshot.json
Normal file
541
bizmatch-server/src/drizzle/migrations/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,541 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
13
bizmatch-server/src/drizzle/migrations/meta/_journal.json
Normal file
13
bizmatch-server/src/drizzle/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1723045357281,
|
||||
"tag": "0000_lean_marvex",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
92
bizmatch-server/src/drizzle/schema.ts
Normal file
92
bizmatch-server/src/drizzle/schema.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { boolean, char, doublePrecision, 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 customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
|
||||
|
||||
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 }),
|
||||
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 }),
|
||||
});
|
||||
21
bizmatch-server/src/drizzle/test.js
Normal file
21
bizmatch-server/src/drizzle/test.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Angenommen, du hast eine Datei `databaseModels.js` mit deinen pgTable-Definitionen
|
||||
const { users } = require('./schema.js');
|
||||
|
||||
function generateTypeScriptInterface(tableDefinition, tableName) {
|
||||
let interfaceString = `export interface ${tableName} {\n`;
|
||||
for (const [column, definition] of Object.entries(tableDefinition)) {
|
||||
// Du musst die Definition parsen, um den korrekten Typ zu extrahieren
|
||||
const tsType = definition.type === 'uuid' ? 'string' :
|
||||
definition.type.startsWith('varchar') || definition.type === 'text' ? 'string' :
|
||||
definition.type === 'boolean' ? 'boolean' : 'any';
|
||||
interfaceString += ` ${column}${definition.optional ? '?' : ''}: ${tsType};\n`;
|
||||
}
|
||||
interfaceString += '}\n';
|
||||
return interfaceString;
|
||||
}
|
||||
|
||||
const userModelInterface = generateTypeScriptInterface(users.columns, 'User');
|
||||
fs.writeFileSync(path.join(__dirname, 'UserInterface.ts'), userModelInterface);
|
||||
@@ -1,13 +1,12 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { fstat, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import { ImageProperty } from '../models/main.model.js';
|
||||
import sharp from 'sharp';
|
||||
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);
|
||||
|
||||
@@ -21,74 +20,86 @@ export class FileService {
|
||||
fs.ensureDirSync(`./pictures/logo`);
|
||||
fs.ensureDirSync(`./pictures/property`);
|
||||
}
|
||||
// ############
|
||||
// Subscriptions
|
||||
// ############
|
||||
private loadSubscriptions(): void {
|
||||
const filePath = join(__dirname, '..', 'assets', 'subscriptions.json');
|
||||
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
this.subscriptions = JSON.parse(rawData);
|
||||
}
|
||||
getSubscriptions() {
|
||||
return this.subscriptions
|
||||
getSubscriptions(): Subscription[] {
|
||||
return this.subscriptions;
|
||||
}
|
||||
async storeProfilePicture(file: Express.Multer.File, userId: string) {
|
||||
// ############
|
||||
// Profile
|
||||
// ############
|
||||
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const output = await sharp(file.buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
||||
await sharp(output).toFile(`./pictures/profile/${adjustedEmail}.avif`);
|
||||
}
|
||||
hasProfile(userId: string){
|
||||
return fs.existsSync(`./pictures/profile/${userId}.avif`)
|
||||
hasProfile(adjustedEmail: string) {
|
||||
return fs.existsSync(`./pictures/profile/${adjustedEmail}.avif`);
|
||||
}
|
||||
|
||||
async storeCompanyLogo(file: Express.Multer.File, userId: string) {
|
||||
// ############
|
||||
// Logo
|
||||
// ############
|
||||
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const output = await sharp(file.buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`./pictures/logo/${userId}.avif`); // Ersetze Dateierweiterung
|
||||
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||
}
|
||||
hasCompanyLogo(userId: string){
|
||||
return fs.existsSync(`./pictures/logo/${userId}.avif`)
|
||||
hasCompanyLogo(adjustedEmail: string) {
|
||||
return fs.existsSync(`./pictures/logo/${adjustedEmail}.avif`) ? true : false;
|
||||
}
|
||||
|
||||
async getPropertyImages(listingId: string): Promise<ImageProperty[]> {
|
||||
const result: ImageProperty[] = []
|
||||
const directory = `./pictures/property/${listingId}`
|
||||
// ############
|
||||
// Property
|
||||
// ############
|
||||
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||
if (fs.existsSync(directory)) {
|
||||
const files = await fs.readdir(directory);
|
||||
files.forEach(f => {
|
||||
const image: ImageProperty = { name: f, id: '', code: '' };
|
||||
result.push(image)
|
||||
})
|
||||
result.push(f);
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
return []
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async hasPropertyImages(listingId: string): Promise<boolean> {
|
||||
const result: ImageProperty[] = []
|
||||
const directory = `./pictures/property/${listingId}`
|
||||
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);
|
||||
return files.length>0
|
||||
return files.length > 0;
|
||||
} else {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async storePropertyPicture(file: Express.Multer.File, listingId: string) : Promise<string> {
|
||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg'
|
||||
const directory = `./pictures/property/${listingId}`
|
||||
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);
|
||||
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
||||
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
|
||||
return `${imageName}.avif`
|
||||
return `${imageName}.avif`;
|
||||
}
|
||||
// ############
|
||||
// utils
|
||||
// ############
|
||||
async getNextImageName(directory) {
|
||||
try {
|
||||
const files = await fs.readdir(directory);
|
||||
@@ -115,26 +126,24 @@ export class FileService {
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
|
||||
let timeTaken = Date.now() - start;
|
||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`)
|
||||
}
|
||||
getProfileImagesForUsers(userids:string){
|
||||
const ids = userids.split(',');
|
||||
let result = {};
|
||||
for (const id of ids){
|
||||
result = {...result,[id]:fs.existsSync(`./pictures/profile/${id}.avif`)}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
getCompanyLogosForUsers(userids:string){
|
||||
const ids = userids.split(',');
|
||||
let result = {};
|
||||
for (const id of ids){
|
||||
result = {...result,[id]:fs.existsSync(`./pictures/logo/${id}.avif`)}
|
||||
}
|
||||
return result;
|
||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
||||
}
|
||||
deleteImage(path: string) {
|
||||
fs.unlinkSync(path);
|
||||
}
|
||||
|
||||
deleteDirectoryIfExists(imagePath) {
|
||||
const dirPath = `pictures/property/${imagePath}`;
|
||||
try {
|
||||
const exists = fs.pathExistsSync();
|
||||
if (exists) {
|
||||
fs.removeSync(dirPath);
|
||||
console.log(`Directory ${dirPath} was deleted.`);
|
||||
} else {
|
||||
console.log(`Directory ${dirPath} does not exist.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error while deleting ${dirPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { CountyRequest } from 'src/models/server.model.js';
|
||||
import { GeoService } from './geo.service.js';
|
||||
|
||||
@Controller('geo')
|
||||
@@ -9,10 +10,18 @@ export class GeoController {
|
||||
findByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix);
|
||||
}
|
||||
@Get('citiesandstates/:prefix')
|
||||
findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesAndStatesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@Get(':prefix/:state')
|
||||
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix, state);
|
||||
}
|
||||
|
||||
@Post('counties')
|
||||
findByPrefixAndStates(@Body() countyRequest: CountyRequest): any {
|
||||
return this.geoService.findCountiesStartingWith(countyRequest.prefix, countyRequest.states);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
import { City, Geo, State } from 'src/models/server.model.js';
|
||||
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);
|
||||
@@ -10,31 +11,96 @@ const __dirname = path.dirname(__filename);
|
||||
@Injectable()
|
||||
export class GeoService {
|
||||
geo: Geo;
|
||||
counties: CountyData[];
|
||||
constructor() {
|
||||
this.loadGeo();
|
||||
}
|
||||
private loadGeo(): void {
|
||||
const filePath = join(__dirname,'..', 'assets', 'geo.json');
|
||||
const filePath = join(__dirname, '../..', 'assets', 'geo.json');
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
this.geo = JSON.parse(rawData);
|
||||
const countiesFilePath = join(__dirname, '../..', 'assets', 'counties.json');
|
||||
const rawCountiesData = readFileSync(countiesFilePath, 'utf8');
|
||||
this.counties = JSON.parse(rawCountiesData);
|
||||
}
|
||||
findCountiesStartingWith(prefix: string, states?: string[]) {
|
||||
let results: CountyResult[] = [];
|
||||
let idCounter = 1;
|
||||
|
||||
findCitiesStartingWith( prefix: string, state?:string): { city: string; state: string; state_code: string }[] {
|
||||
const result: { city: string; state: string; state_code: string }[] = [];
|
||||
this.counties.forEach(stateData => {
|
||||
if (!states || states.includes(stateData.state)) {
|
||||
stateData.counties.forEach(county => {
|
||||
if (county.startsWith(prefix.toUpperCase())) {
|
||||
results.push({
|
||||
id: idCounter++,
|
||||
name: county,
|
||||
state: stateData.state_full,
|
||||
state_code: stateData.state,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
findCitiesStartingWith(prefix: string, state?: string): GeoResult[] {
|
||||
const result: GeoResult[] = [];
|
||||
|
||||
this.geo.states.forEach((state: State) => {
|
||||
state.cities.forEach((city: City) => {
|
||||
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
result.push({
|
||||
id: city.id,
|
||||
city: city.name,
|
||||
state: state.name,
|
||||
state_code: state.state_code
|
||||
state: state.state_code,
|
||||
//state_code: state.state_code,
|
||||
latitude: city.latitude,
|
||||
longitude: city.longitude,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return state ? result.filter(e => e.state.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 }> = [];
|
||||
|
||||
return state ? result.filter(e=>e.state_code.toLowerCase()===state.toLowerCase()) :result;
|
||||
}
|
||||
const lowercasePrefix = prefix.toLowerCase();
|
||||
|
||||
//for (const country of this.geo) {
|
||||
// Suche nach passenden Staaten
|
||||
for (const state of this.geo.states) {
|
||||
if (state.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: state.id.toString(),
|
||||
name: state.name,
|
||||
type: 'state',
|
||||
state: state.state_code,
|
||||
});
|
||||
}
|
||||
|
||||
// Suche nach passenden Städten
|
||||
for (const city of state.cities) {
|
||||
if (city.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: city.id.toString(),
|
||||
name: city.name,
|
||||
type: 'city',
|
||||
state: state.state_code,
|
||||
});
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
getCityWithCoords(state: string, city: string): City {
|
||||
return this.geo.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,55 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
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 { ListingsService } from '../listings/listings.service.js';
|
||||
import { CommercialPropertyListing } from 'src/models/main.model.js';
|
||||
import { Entity, EntityData } from 'redis-om';
|
||||
|
||||
@Controller('image')
|
||||
export class ImageController {
|
||||
|
||||
constructor(private fileService:FileService,
|
||||
private listingService:ListingsService,
|
||||
constructor(
|
||||
private fileService: FileService,
|
||||
private listingService: CommercialPropertyService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
private selectOptions:SelectOptionsService) {
|
||||
private selectOptions: SelectOptionsService,
|
||||
) {}
|
||||
// ############
|
||||
// Property
|
||||
// ############
|
||||
@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);
|
||||
}
|
||||
|
||||
@Post('uploadPropertyPicture/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
const imagename = await this.fileService.storePropertyPicture(file,id);
|
||||
await this.listingService.addImage(id,imagename);
|
||||
@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}`);
|
||||
await this.listingService.deleteImage(imagePath, serial, imagename);
|
||||
}
|
||||
|
||||
@Post('uploadProfile/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
async uploadProfile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
await this.fileService.storeProfilePicture(file,id);
|
||||
// ############
|
||||
// Profile
|
||||
// ############
|
||||
@Post('uploadProfile/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeProfilePicture(file, adjustedEmail);
|
||||
}
|
||||
|
||||
@Post('uploadCompanyLogo/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
await this.fileService.storeCompanyLogo(file,id);
|
||||
@Delete('profile/:email/')
|
||||
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getPropertyImagesById(@Param('id') id:string): Promise<any> {
|
||||
const result = await this.listingService.getCommercialPropertyListingById(id);
|
||||
const listing = result as CommercialPropertyListing;
|
||||
if (listing.imageOrder){
|
||||
return listing.imageOrder
|
||||
} else {
|
||||
const imageOrder = await this.fileService.getPropertyImages(id);
|
||||
listing.imageOrder=imageOrder;
|
||||
this.listingService.saveListing(listing);
|
||||
return imageOrder;
|
||||
}
|
||||
}
|
||||
@Get('profileImages/:userids')
|
||||
async getProfileImagesForUsers(@Param('userids') userids:string): Promise<any> {
|
||||
return await this.fileService.getProfileImagesForUsers(userids);
|
||||
}
|
||||
@Get('companyLogos/:userids')
|
||||
async getCompanyLogosForUsers(@Param('userids') userids:string): Promise<any> {
|
||||
return await this.fileService.getCompanyLogosForUsers(userids);
|
||||
}
|
||||
|
||||
@Delete('propertyPicture/:listingid/:imagename')
|
||||
async deletePropertyImagesById(@Param('listingid') listingid:string,@Param('imagename') imagename:string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/property/${listingid}/${imagename}`);
|
||||
await this.listingService.deleteImage(listingid,imagename);
|
||||
}
|
||||
@Delete('logo/:userid/')
|
||||
async deleteLogoImagesById(@Param('id') id:string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/property//${id}`)
|
||||
}
|
||||
@Delete('profile/:userid/')
|
||||
async deleteProfileImagesById(@Param('id') id:string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/property//${id}`)
|
||||
// ############
|
||||
// Logo
|
||||
// ############
|
||||
@Post('uploadCompanyLogo/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeCompanyLogo(file, adjustedEmail);
|
||||
}
|
||||
@Delete('logo/:email/')
|
||||
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
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.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { ListingsService } from '../listings/listings.service.js';
|
||||
import { ListingsModule } from '../listings/listings.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [ListingsModule],
|
||||
controllers: [ImageController],
|
||||
providers: [ImageService,FileService,SelectOptionsService]
|
||||
providers: [ImageService, FileService, SelectOptionsService],
|
||||
})
|
||||
export class ImageModule {}
|
||||
|
||||
18
bizmatch-server/src/jwt-auth/jwt-auth.guard.ts
Normal file
18
bizmatch-server/src/jwt-auth/jwt-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 JwtAuthGuard 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) {
|
||||
throw err || new UnauthorizedException(info);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
13
bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts
Normal file
13
bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
|
||||
handleRequest(err, user, info) {
|
||||
// Wenn der Benutzer nicht authentifiziert ist, aber kein Fehler vorliegt, geben Sie null zurück
|
||||
if (err || !user) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
45
bizmatch-server/src/jwt.strategy.ts
Normal file
45
bizmatch-server/src/jwt.strategy.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { passportJwtSecret } from 'jwks-rsa';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { Logger } from 'winston';
|
||||
import { JwtPayload, JwtUser } from './models/main.model';
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {
|
||||
const realm = configService.get<string>('REALM');
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKeyProvider: passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
jwksUri: `https://auth.bizmatch.net/realms/${realm}/protocol/openid-connect/certs`,
|
||||
}),
|
||||
audience: 'account', // Keycloak Client ID
|
||||
authorize: '',
|
||||
issuer: `https://auth.bizmatch.net/realms/${realm}`,
|
||||
algorithms: ['RS256'],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload): Promise<JwtUser> {
|
||||
if (!payload) {
|
||||
this.logger.error('Invalid payload');
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
if (!payload.sub || !payload.preferred_username) {
|
||||
this.logger.error('Missing required claims');
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { convertStringToNullUndefined } from '../utils.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
import { Body, Controller, Inject, Post } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { UserListingCriteria } from 'src/models/main.model.js';
|
||||
import { Logger } from 'winston';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
|
||||
@Controller('listings/professionals_brokers')
|
||||
export class BrokerListingsController {
|
||||
|
||||
constructor(private readonly userService:UserService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
}
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@Post('search')
|
||||
find(@Body() criteria: any): any {
|
||||
return this.userService.findUser(criteria);
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
return this.userService.searchUserListings(criteria);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
226
bizmatch-server/src/listings/business-listing.service.ts
Normal file
226
bizmatch-server/src/listings/business-listing.service.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, 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';
|
||||
|
||||
@Injectable()
|
||||
export class BusinessListingService {
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private fileService?: FileService,
|
||||
private geoService?: GeoService,
|
||||
) {}
|
||||
|
||||
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
|
||||
}
|
||||
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
whereConditions.push(inArray(businesses.type, criteria.types));
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(eq(businesses.state, criteria.state));
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
whereConditions.push(gte(businesses.price, criteria.minPrice));
|
||||
}
|
||||
|
||||
if (criteria.maxPrice) {
|
||||
whereConditions.push(lte(businesses.price, criteria.maxPrice));
|
||||
}
|
||||
|
||||
if (criteria.minRevenue) {
|
||||
whereConditions.push(gte(businesses.salesRevenue, criteria.minRevenue));
|
||||
}
|
||||
|
||||
if (criteria.maxRevenue) {
|
||||
whereConditions.push(lte(businesses.salesRevenue, criteria.maxRevenue));
|
||||
}
|
||||
|
||||
if (criteria.minCashFlow) {
|
||||
whereConditions.push(gte(businesses.cashFlow, criteria.minCashFlow));
|
||||
}
|
||||
|
||||
if (criteria.maxCashFlow) {
|
||||
whereConditions.push(lte(businesses.cashFlow, criteria.maxCashFlow));
|
||||
}
|
||||
|
||||
if (criteria.minNumberEmployees) {
|
||||
whereConditions.push(gte(businesses.employees, criteria.minNumberEmployees));
|
||||
}
|
||||
|
||||
if (criteria.maxNumberEmployees) {
|
||||
whereConditions.push(lte(businesses.employees, criteria.maxNumberEmployees));
|
||||
}
|
||||
|
||||
if (criteria.establishedSince) {
|
||||
whereConditions.push(gte(businesses.established, criteria.establishedSince));
|
||||
}
|
||||
|
||||
if (criteria.establishedUntil) {
|
||||
whereConditions.push(lte(businesses.established, criteria.establishedUntil));
|
||||
}
|
||||
|
||||
if (criteria.realEstateChecked) {
|
||||
whereConditions.push(eq(businesses.realEstateIncluded, criteria.realEstateChecked));
|
||||
}
|
||||
|
||||
if (criteria.leasedLocation) {
|
||||
whereConditions.push(eq(businesses.leasedLocation, criteria.leasedLocation));
|
||||
}
|
||||
|
||||
if (criteria.franchiseResale) {
|
||||
whereConditions.push(eq(businesses.franchiseResale, criteria.franchiseResale));
|
||||
}
|
||||
|
||||
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}%`)));
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
|
||||
return whereConditions;
|
||||
}
|
||||
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn
|
||||
.select({
|
||||
business: businesses,
|
||||
brokerFirstName: schema.users.firstname,
|
||||
brokerLastName: schema.users.lastname,
|
||||
})
|
||||
.from(businesses)
|
||||
.leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
||||
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
|
||||
// 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));
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
async getBusinessListingsCount(criteria: BusinessListingCriteria): 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);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
countQuery.where(whereClause);
|
||||
}
|
||||
|
||||
const [{ value: totalCount }] = await countQuery;
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
|
||||
let result = await this.conn
|
||||
.select()
|
||||
.from(businesses)
|
||||
.where(and(sql`${businesses.id} = ${id}`));
|
||||
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
||||
return convertDrizzleBusinessToBusiness(result[0]) as BusinessListing;
|
||||
}
|
||||
|
||||
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
|
||||
const conditions = [];
|
||||
conditions.push(eq(businesses.imageName, emailToDirName(email)));
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||
conditions.push(ne(businesses.draft, true));
|
||||
}
|
||||
const listings = (await this.conn
|
||||
.select()
|
||||
.from(businesses)
|
||||
.where(and(...conditions))) as BusinessListing[];
|
||||
|
||||
return listings.map(l => convertDrizzleBusinessToBusiness(l));
|
||||
}
|
||||
|
||||
// #### CREATE ########################################
|
||||
async createListing(data: BusinessListing): Promise<BusinessListing> {
|
||||
try {
|
||||
data.created = 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);
|
||||
const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
|
||||
return convertDrizzleBusinessToBusiness(createdListing);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// #### UPDATE Business ########################################
|
||||
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing> {
|
||||
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);
|
||||
const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
|
||||
return convertDrizzleBusinessToBusiness(updateListing);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// #### DELETE ########################################
|
||||
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`);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,61 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { convertStringToNullUndefined } from '../utils.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
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';
|
||||
|
||||
@Controller('listings/business')
|
||||
export class BusinessListingsController {
|
||||
constructor(
|
||||
private readonly listingsService: BusinessListingService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll(): any {
|
||||
return this.listingsService.getAllBusinessListings();
|
||||
}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Param('id') id:string): any {
|
||||
return this.listingsService.getBusinessListingById(id);
|
||||
findById(@Request() req, @Param('id') id: string): any {
|
||||
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('user/:userid')
|
||||
findByUserId(@Param('userid') userid:string): any {
|
||||
return this.listingsService.getBusinessListingByUserId(userid);
|
||||
findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@Post('search')
|
||||
find(@Body() criteria: any): any {
|
||||
return this.listingsService.findBusinessListings(criteria);
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('find')
|
||||
find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||
}
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: BusinessListingCriteria): Promise<number> {
|
||||
return this.listingsService.getBusinessListingsCount(criteria);
|
||||
}
|
||||
// @UseGuards(OptionalJwtAuthGuard)
|
||||
// @Post('search')
|
||||
// search(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
// return this.listingsService.searchBusinessListings(criteria.prompt);
|
||||
// }
|
||||
|
||||
/**
|
||||
* @param listing creates a new listing
|
||||
*/
|
||||
@Post()
|
||||
save(@Body() listing: any){
|
||||
create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
this.listingsService.saveListing(listing)
|
||||
return this.listingsService.createListing(listing);
|
||||
}
|
||||
@Put()
|
||||
update(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return this.listingsService.updateBusinessListing(listing.id, listing);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id deletes a listing
|
||||
*/
|
||||
@Delete(':id')
|
||||
deleteById(@Param('id') id: string) {
|
||||
this.listingsService.deleteBusinessListing(id)
|
||||
this.listingsService.deleteListing(id);
|
||||
}
|
||||
|
||||
@Delete('deleteAll')
|
||||
deleteAll(){
|
||||
this.listingsService.deleteAllBusinessListings()
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,57 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
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 { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { CommercialPropertyListing, ImageProperty } from 'src/models/main.model.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { CommercialPropertyListing } from '../models/db.model';
|
||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model.js';
|
||||
import { CommercialPropertyService } from './commercial-property.service.js';
|
||||
|
||||
@Controller('listings/commercialProperty')
|
||||
export class CommercialPropertyListingsController {
|
||||
constructor(
|
||||
private readonly listingsService: CommercialPropertyService,
|
||||
private fileService: FileService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
constructor(private readonly listingsService:ListingsService,private fileService:FileService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
}
|
||||
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Param('id') id:string): any {
|
||||
return this.listingsService.getCommercialPropertyListingById(id);
|
||||
findById(@Request() req, @Param('id') id: string): any {
|
||||
return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@Post('search')
|
||||
find(@Body() criteria: any): any {
|
||||
return this.listingsService.findCommercialPropertyListings(criteria);
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('user/:email')
|
||||
findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@Put('imageOrder/:id')
|
||||
async changeImageOrder(@Param('id') id:string,@Body() imageOrder: ImageProperty[]) {
|
||||
this.listingsService.updateImageOrder(id, imageOrder)
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('find')
|
||||
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
|
||||
return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser);
|
||||
}
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
return this.listingsService.getCommercialPropertiesCount(criteria);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates();
|
||||
}
|
||||
/**
|
||||
* @param listing creates a new listing
|
||||
*/
|
||||
@Post()
|
||||
save(@Body() listing: any){
|
||||
async create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
this.listingsService.saveListing(listing)
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id deletes a listing
|
||||
*/
|
||||
@Delete(':id')
|
||||
deleteById(@Param('id') id:string){
|
||||
this.listingsService.deleteCommercialPropertyListing(id)
|
||||
@Put()
|
||||
async update(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
|
||||
}
|
||||
@Delete('deleteAll')
|
||||
deleteAll(){
|
||||
this.listingsService.deleteAllcommercialListings()
|
||||
@Delete(':id/:imagePath')
|
||||
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||
this.listingsService.deleteListing(id);
|
||||
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
199
bizmatch-server/src/listings/commercial-property.service.ts
Normal file
199
bizmatch-server/src/listings/commercial-property.service.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, 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';
|
||||
|
||||
@Injectable()
|
||||
export class CommercialPropertyService {
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private fileService?: FileService,
|
||||
private geoService?: GeoService,
|
||||
) {}
|
||||
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
|
||||
}
|
||||
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||
whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
whereConditions.push(inArray(schema.commercials.type, criteria.types));
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(eq(schema.commercials.state, criteria.state));
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
whereConditions.push(gte(schema.commercials.price, criteria.minPrice));
|
||||
}
|
||||
|
||||
if (criteria.maxPrice) {
|
||||
whereConditions.push(lte(schema.commercials.price, criteria.maxPrice));
|
||||
}
|
||||
|
||||
if (criteria.title) {
|
||||
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional')));
|
||||
return whereConditions;
|
||||
}
|
||||
// #### Find by criteria ########################################
|
||||
async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn.select({ commercial: commercials }).from(commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email));
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): 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);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
countQuery.where(whereClause);
|
||||
}
|
||||
|
||||
const [{ value: totalCount }] = await countQuery;
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
// #### Find by ID ########################################
|
||||
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
let 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;
|
||||
}
|
||||
|
||||
// #### Find by User EMail ########################################
|
||||
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
||||
const conditions = [];
|
||||
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
|
||||
if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||
conditions.push(ne(commercials.draft, true));
|
||||
}
|
||||
const listings = (await this.conn
|
||||
.select()
|
||||
.from(commercials)
|
||||
.where(and(...conditions))) as CommercialPropertyListing[];
|
||||
return listings.map(l => convertDrizzleCommercialToCommercial(l)) as CommercialPropertyListing[];
|
||||
}
|
||||
// #### Find by imagePath ########################################
|
||||
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
|
||||
const result = await this.conn
|
||||
.select()
|
||||
.from(commercials)
|
||||
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
|
||||
return convertDrizzleCommercialToCommercial(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);
|
||||
const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning();
|
||||
return convertDrizzleCommercialToCommercial(createdListing);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// #### UPDATE CommercialProps ########################################
|
||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
|
||||
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);
|
||||
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)));
|
||||
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 [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning();
|
||||
return convertDrizzleCommercialToCommercial(updateListing);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ##############################################################
|
||||
// Images for commercial Properties
|
||||
// ##############################################################
|
||||
async deleteImage(imagePath: string, serial: string, name: string) {
|
||||
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||
const index = listing.imageOrder.findIndex(im => im === name);
|
||||
if (index > -1) {
|
||||
listing.imageOrder.splice(index, 1);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||
}
|
||||
}
|
||||
async addImage(imagePath: string, serial: string, imagename: string) {
|
||||
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||
listing.imageOrder.push(imagename);
|
||||
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||
}
|
||||
// #### DELETE ########################################
|
||||
async deleteListing(id: string): Promise<void> {
|
||||
await this.conn.delete(commercials).where(eq(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`);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { BusinessListingsController } from './business-listings.controller.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
|
||||
import { RedisModule } from '../redis/redis.module.js';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { UnknownListingsController } from './unknown-listings.controller.js';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { BrokerListingsController } from './broker-listings.controller.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 { 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';
|
||||
|
||||
@Module({
|
||||
imports: [RedisModule],
|
||||
imports: [DrizzleModule, AuthModule, GeoModule],
|
||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||
providers: [ListingsService,FileService,UserService],
|
||||
exports: [ListingsService],
|
||||
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
||||
exports: [BusinessListingService, CommercialPropertyService],
|
||||
})
|
||||
export class ListingsModule {}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BusinessListing,
|
||||
CommercialPropertyListing,
|
||||
ListingCriteria,
|
||||
ListingType,
|
||||
ImageProperty,
|
||||
ListingCategory
|
||||
} from '../models/main.model.js';
|
||||
import { convertStringToNullUndefined } from '../utils.js';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { EntityData, EntityId, Repository, Schema, SchemaDefinition } from 'redis-om';
|
||||
import { REDIS_CLIENT } from '../redis/redis.module.js';
|
||||
|
||||
@Injectable()
|
||||
export class ListingsService {
|
||||
schemaNameBusiness:ListingCategory={name:'business'}
|
||||
schemaNameCommercial:ListingCategory={name:'commercialProperty'}
|
||||
businessListingRepository:Repository;
|
||||
commercialPropertyListingRepository:Repository;
|
||||
baseListingSchemaDef : SchemaDefinition = {
|
||||
id: { type: 'string' },
|
||||
userId: { type: 'string' },
|
||||
listingsCategory: { type: 'string' },
|
||||
title: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
country: { type: 'string' },
|
||||
state:{ type: 'string' },
|
||||
city:{ type: 'string' },
|
||||
zipCode: { type: 'number' },
|
||||
type: { type: 'string' },
|
||||
price: { type: 'number' },
|
||||
favoritesForUser:{ type: 'string[]' },
|
||||
hideImage:{ type: 'boolean' },
|
||||
draft:{ type: 'boolean' },
|
||||
created:{ type: 'date' },
|
||||
updated:{ type: 'date' }
|
||||
}
|
||||
businessListingSchemaDef : SchemaDefinition = {
|
||||
...this.baseListingSchemaDef,
|
||||
salesRevenue: { type: 'number' },
|
||||
cashFlow: { type: 'number' },
|
||||
employees: { type: 'number' },
|
||||
established: { type: 'number' },
|
||||
internalListingNumber: { type: 'number' },
|
||||
realEstateIncluded:{ type: 'boolean' },
|
||||
leasedLocation:{ type: 'boolean' },
|
||||
franchiseResale:{ type: 'boolean' },
|
||||
supportAndTraining: { type: 'string' },
|
||||
reasonForSale: { type: 'string' },
|
||||
brokerLicencing: { type: 'string' },
|
||||
internals: { type: 'string' },
|
||||
}
|
||||
commercialPropertyListingSchemaDef : SchemaDefinition = {
|
||||
...this.baseListingSchemaDef,
|
||||
imageNames:{ type: 'string[]' },
|
||||
}
|
||||
businessListingSchema = new Schema(this.schemaNameBusiness.name,this.businessListingSchemaDef, {
|
||||
dataStructure: 'JSON'
|
||||
})
|
||||
commercialPropertyListingSchema = new Schema(this.schemaNameCommercial.name,this.commercialPropertyListingSchemaDef, {
|
||||
dataStructure: 'JSON'
|
||||
})
|
||||
constructor(@Inject(REDIS_CLIENT) private readonly redis: any, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger){
|
||||
this.businessListingRepository = new Repository(this.businessListingSchema, redis);
|
||||
this.commercialPropertyListingRepository = new Repository(this.commercialPropertyListingSchema, redis)
|
||||
this.businessListingRepository.createIndex();
|
||||
this.commercialPropertyListingRepository.createIndex();
|
||||
}
|
||||
async saveListing(listing: BusinessListing | CommercialPropertyListing) {
|
||||
const repo=listing.listingsCategory==='business'?this.businessListingRepository:this.commercialPropertyListingRepository;
|
||||
let result
|
||||
if (listing.id){
|
||||
result = await repo.save(listing.id,listing as any)
|
||||
} else {
|
||||
result = await repo.save(listing as any)
|
||||
listing.id=result[EntityId];
|
||||
result = await repo.save(listing.id,listing as any)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async getCommercialPropertyListingById(id: string): Promise<CommercialPropertyListing>{
|
||||
return await this.commercialPropertyListingRepository.fetch(id) as unknown as CommercialPropertyListing;
|
||||
}
|
||||
async getBusinessListingById(id: string) {
|
||||
return await this.businessListingRepository.fetch(id)
|
||||
}
|
||||
async getBusinessListingByUserId(userid:string){
|
||||
return await this.businessListingRepository.search().where('userId').equals(userid).return.all()
|
||||
}
|
||||
async deleteBusinessListing(id: string){
|
||||
return await this.businessListingRepository.remove(id);
|
||||
}
|
||||
async deleteCommercialPropertyListing(id: string){
|
||||
return await this.commercialPropertyListingRepository.remove(id);
|
||||
}
|
||||
async getAllBusinessListings(start?: number, end?: number) {
|
||||
return await this.businessListingRepository.search().return.all()
|
||||
}
|
||||
async getAllCommercialListings(start?: number, end?: number) {
|
||||
return await this.commercialPropertyListingRepository.search().return.all()
|
||||
}
|
||||
async findBusinessListings(criteria:ListingCriteria): Promise<any> {
|
||||
// let listings = await this.getAllBusinessListings();
|
||||
// return this.find(criteria,listings);
|
||||
this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`);
|
||||
const result = await this.redis.ft.search('business:index','*',{LIMIT:{from:0,size:50}});
|
||||
this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`);
|
||||
return result.documents;
|
||||
}
|
||||
async findCommercialPropertyListings(criteria:ListingCriteria): Promise<any> {
|
||||
let listings = await this.getAllCommercialListings();
|
||||
return this.find(criteria,listings);
|
||||
}
|
||||
async deleteAllBusinessListings(){
|
||||
const ids = await this.getIdsForRepo(this.schemaNameBusiness.name);
|
||||
this.businessListingRepository.remove(ids);
|
||||
}
|
||||
async deleteAllcommercialListings(){
|
||||
const ids = await this.getIdsForRepo(this.schemaNameCommercial.name);
|
||||
this.commercialPropertyListingRepository.remove(ids);
|
||||
}
|
||||
async getIdsForRepo(repoName:string, maxcount=100000){
|
||||
let cursor = 0;
|
||||
let ids = [];
|
||||
do {
|
||||
const reply = await this.redis.scan(cursor, {
|
||||
MATCH: `${repoName}:*`,
|
||||
COUNT: maxcount
|
||||
});
|
||||
cursor = reply.cursor;
|
||||
// Extrahiere die ID aus jedem Schlüssel und füge sie zur Liste hinzu
|
||||
ids = ids.concat(reply.keys.map(key => key.split(':')[1]).filter(id=>id!='index'));
|
||||
} while (cursor !== 0);
|
||||
return ids;
|
||||
}
|
||||
|
||||
async find(criteria:ListingCriteria, listings: any[]): Promise<any> {
|
||||
listings=listings.filter(l=>l.listingsCategory===criteria.listingsCategory);
|
||||
if (convertStringToNullUndefined(criteria.type)){
|
||||
console.log(criteria.type);
|
||||
listings=listings.filter(l=>l.type===criteria.type);
|
||||
}
|
||||
if (convertStringToNullUndefined(criteria.state)){
|
||||
console.log(criteria.state);
|
||||
listings=listings.filter(l=>l.state===criteria.state);
|
||||
}
|
||||
if (convertStringToNullUndefined(criteria.minPrice)){
|
||||
console.log(criteria.minPrice);
|
||||
listings=listings.filter(l=>l.price>=Number(criteria.minPrice));
|
||||
}
|
||||
if (convertStringToNullUndefined(criteria.maxPrice)){
|
||||
console.log(criteria.maxPrice);
|
||||
listings=listings.filter(l=>l.price<=Number(criteria.maxPrice));
|
||||
}
|
||||
if (convertStringToNullUndefined(criteria.realEstateChecked)){
|
||||
console.log(criteria.realEstateChecked);
|
||||
listings=listings.filter(l=>l.realEstateIncluded);
|
||||
}
|
||||
if (convertStringToNullUndefined(criteria.category)){
|
||||
console.log(criteria.category);
|
||||
listings=listings.filter(l=>l.category===criteria.category);
|
||||
}
|
||||
return listings
|
||||
}
|
||||
|
||||
async updateImageOrder(id:string,imageOrder: ImageProperty[]){
|
||||
const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing
|
||||
listing.imageOrder=imageOrder;
|
||||
this.saveListing(listing);
|
||||
}
|
||||
async deleteImage(listingid:string,name:string,){
|
||||
const listing = await this.getCommercialPropertyListingById(listingid) as unknown as CommercialPropertyListing
|
||||
const index = listing.imageOrder.findIndex(im=>im.name===name);
|
||||
if (index>-1){
|
||||
listing.imageOrder.splice(index,1);
|
||||
this.saveListing(listing);
|
||||
}
|
||||
}
|
||||
async addImage(id:string,imagename: string){
|
||||
const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing
|
||||
listing.imageOrder.push({name:imagename,code:'',id:''});
|
||||
this.saveListing(listing);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,18 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { convertStringToNullUndefined } from '../utils.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
import { Controller, Inject } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
@Controller('listings/undefined')
|
||||
export class UnknownListingsController {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
|
||||
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
// @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);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@Get(':id')
|
||||
async findById(@Param('id') id:string): Promise<any> {
|
||||
const result = await this.listingsService.getBusinessListingById(id);
|
||||
if (result.id){
|
||||
return result
|
||||
} else {
|
||||
return await this.listingsService.getCommercialPropertyListingById(id);
|
||||
}
|
||||
}
|
||||
@Get('repo/:repo')
|
||||
async getAllByRepo(@Param('repo') repo:string): Promise<any> {
|
||||
return await this.listingsService.getIdsForRepo(repo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ErrorResponse, MailInfo } from '../models/main.model';
|
||||
import { MailService } from './mail.service.js';
|
||||
import { KeycloakUser, MailInfo } from 'src/models/main.model.js';
|
||||
|
||||
|
||||
@Controller('mail')
|
||||
export class MailController {
|
||||
constructor(private mailService:MailService){
|
||||
|
||||
}
|
||||
constructor(private mailService: MailService) {}
|
||||
@Post()
|
||||
sendEMail(@Body() mailInfo: MailInfo): Promise< KeycloakUser> {
|
||||
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
if (mailInfo.listing) {
|
||||
return this.mailService.sendInquiry(mailInfo);
|
||||
} else {
|
||||
return this.mailService.sendRequest(mailInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MailService } from './mail.service.js';
|
||||
import { MailController } from './mail.controller.js';
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import path, { join } from 'path';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
|
||||
import { Module } from '@nestjs/common';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
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;
|
||||
@Module({
|
||||
imports: [AuthModule,
|
||||
imports: [
|
||||
DrizzleModule,
|
||||
UserModule,
|
||||
GeoModule,
|
||||
MailerModule.forRoot({
|
||||
// transport: 'smtps://user@example.com:topsecret@smtp.example.com',
|
||||
// or
|
||||
transport: {
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
secure: false,
|
||||
port: 587,
|
||||
// auth: {
|
||||
// user: 'andreas.knuth@gmail.com',
|
||||
// pass: 'ksnh xjae dqbv xana',
|
||||
// },
|
||||
auth: {
|
||||
user: 'AKIAU6GDWVAQ2QNFLNWN',
|
||||
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
@@ -38,7 +42,7 @@ const __dirname = path.dirname(__filename);
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [MailService],
|
||||
controllers: [MailController]
|
||||
providers: [MailService, UserService, FileService, GeoService],
|
||||
controllers: [MailController],
|
||||
})
|
||||
export class MailModule {}
|
||||
|
||||
@@ -1,26 +1,84 @@
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthService } from '../auth/auth.service.js';
|
||||
import { KeycloakUser, MailInfo, User } from '../models/main.model.js';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
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);
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
constructor(private mailerService: MailerService, private authService:AuthService) {}
|
||||
constructor(
|
||||
private mailerService: MailerService,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
async sendInquiry(mailInfo: MailInfo):Promise<KeycloakUser> {
|
||||
const user = await this.authService.getUser(mailInfo.userId) as KeycloakUser;
|
||||
console.log(JSON.stringify(user));
|
||||
async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
const validatedSender = SenderSchema.parse(mailInfo.sender);
|
||||
} 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;
|
||||
}
|
||||
const user = await this.userService.getUserByMail(mailInfo.email);
|
||||
if (isEmpty(mailInfo.sender.name)) {
|
||||
return { fields: [{ fieldname: 'name', message: 'Required' }] };
|
||||
}
|
||||
await this.mailerService.sendMail({
|
||||
to: user.email,
|
||||
from: '"Bizmatch Team" <info@bizmatch.net>', // override default from
|
||||
subject: `Inquiry from ${mailInfo.sender.name}`,
|
||||
template: './inquiry', // `.hbs` extension is appended automatically
|
||||
context: { // ✏️ filling curly brackets with content
|
||||
name: user.firstName,
|
||||
inquiry:mailInfo.sender.comments
|
||||
//template: './inquiry', // `.hbs` extension is appended automatically
|
||||
template: join(__dirname, '../..', 'mail/templates/inquiry.hbs'),
|
||||
context: {
|
||||
// ✏️ filling curly brackets with content
|
||||
name: user.firstname,
|
||||
inquiry: mailInfo.sender.comments,
|
||||
internalListingNumber: mailInfo.listing.internalListingNumber,
|
||||
title: mailInfo.listing.title,
|
||||
iname: mailInfo.sender.name,
|
||||
phone: mailInfo.sender.phoneNumber,
|
||||
email: mailInfo.sender.email,
|
||||
id: mailInfo.listing.id,
|
||||
url: mailInfo.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
const validatedSender = SenderSchema.parse(mailInfo.sender);
|
||||
} 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: 'support@bizmatch.net',
|
||||
from: `"Bizmatch Support Team" <info@bizmatch.net>`,
|
||||
subject: `Support Request from ${mailInfo.sender.name}`,
|
||||
//template: './inquiry', // `.hbs` extension is appended automatically
|
||||
template: join(__dirname, '../..', 'mail/templates/request.hbs'),
|
||||
context: {
|
||||
// ✏️ filling curly brackets with content
|
||||
request: mailInfo.sender.comments,
|
||||
iname: mailInfo.sender.name,
|
||||
phone: mailInfo.sender.phoneNumber,
|
||||
email: mailInfo.sender.email,
|
||||
},
|
||||
});
|
||||
return user
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,104 @@
|
||||
<p>Hey {{ name }},</p>
|
||||
<p>You got an inquiry a</p>
|
||||
<p>
|
||||
{{inquiry}}
|
||||
</p>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notification: New Buyer Lead</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #333333;
|
||||
}
|
||||
.subheader {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #555555;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-title {
|
||||
color: #1E90FF;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #1E90FF;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.info {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.info:nth-child(even) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
.info-value {
|
||||
margin-left: 10px;
|
||||
color: #555555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">Notification: New buyer lead from the Bizmatch Network</div>
|
||||
<div class="subheader">Dear {{name}},</div>
|
||||
<p>You've received a message regarding your "{{title}}" listing.</p>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Buyer Information</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Name:</span>
|
||||
<span class="info-value">{{iname}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Email:</span>
|
||||
<span class="info-value">{{email}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Phone:</span>
|
||||
<span class="info-value">{{phone}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Comments:</span>
|
||||
<span class="info-value">{{inquiry}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Listing Information</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Headline:</span>
|
||||
<span class="info-value">{{title}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Listing ID:</span>
|
||||
<span class="info-value"><a href="{{url}}/listing/{{id}}">{{id}}</a></span>
|
||||
</div>
|
||||
{{#if internalListingNumber}}
|
||||
<div class="info">
|
||||
<span class="info-label">Ref ID:</span>
|
||||
<span class="info-value">{{internalListingNumber}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
83
bizmatch-server/src/mail/templates/request.hbs
Normal file
83
bizmatch-server/src/mail/templates/request.hbs
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notification: New User Request</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #333333;
|
||||
}
|
||||
.subheader {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #555555;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-title {
|
||||
color: #1E90FF;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #1E90FF;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.info {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.info:nth-child(even) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
.info-value {
|
||||
margin-left: 10px;
|
||||
color: #555555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">Notification: New request from a user of the Bizmatch Network</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Requester Information</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Name:</span>
|
||||
<span class="info-value">{{iname}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Email:</span>
|
||||
<span class="info-value">{{email}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Phone:</span>
|
||||
<span class="info-value">{{phone}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Question:</span>
|
||||
<span class="info-value">{{request}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +1,16 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import express from 'express';
|
||||
import { AppModule } from './app.module.js';
|
||||
|
||||
async function bootstrap() {
|
||||
const server = express();
|
||||
const app = await NestFactory.create(AppModule);
|
||||
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',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization',
|
||||
});
|
||||
//origin: 'http://localhost:4200',
|
||||
await app.listen(3000);
|
||||
|
||||
290
bizmatch-server/src/models/db.model.ts
Normal file
290
bizmatch-server/src/models/db.model.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface UserData {
|
||||
id?: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
description?: string;
|
||||
companyName?: string;
|
||||
companyOverview?: string;
|
||||
companyWebsite?: string;
|
||||
companyLocation?: string;
|
||||
offeredServices?: string;
|
||||
areasServed?: string[];
|
||||
hasProfile?: boolean;
|
||||
hasCompanyLogo?: boolean;
|
||||
licensedIn?: string[];
|
||||
gender?: 'male' | 'female';
|
||||
customerType?: 'buyer' | 'broker' | 'professional';
|
||||
customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
created?: Date;
|
||||
updated?: Date;
|
||||
}
|
||||
export type Gender = 'male' | 'female';
|
||||
export type CustomerType = 'buyer' | '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 CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);
|
||||
const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']);
|
||||
const TypeEnum = z.enum([
|
||||
'automotive',
|
||||
'industrialServices',
|
||||
'foodAndRestaurant',
|
||||
'realEstate',
|
||||
'retail',
|
||||
'oilfield',
|
||||
'service',
|
||||
'advertising',
|
||||
'agriculture',
|
||||
'franchise',
|
||||
'professional',
|
||||
'manufacturing',
|
||||
'uncategorized',
|
||||
]);
|
||||
|
||||
const USStates = z.enum([
|
||||
'AL',
|
||||
'AK',
|
||||
'AZ',
|
||||
'AR',
|
||||
'CA',
|
||||
'CO',
|
||||
'CT',
|
||||
'DC',
|
||||
'DE',
|
||||
'FL',
|
||||
'GA',
|
||||
'HI',
|
||||
'ID',
|
||||
'IL',
|
||||
'IN',
|
||||
'IA',
|
||||
'KS',
|
||||
'KY',
|
||||
'LA',
|
||||
'ME',
|
||||
'MD',
|
||||
'MA',
|
||||
'MI',
|
||||
'MN',
|
||||
'MS',
|
||||
'MO',
|
||||
'MT',
|
||||
'NE',
|
||||
'NV',
|
||||
'NH',
|
||||
'NJ',
|
||||
'NM',
|
||||
'NY',
|
||||
'NC',
|
||||
'ND',
|
||||
'OH',
|
||||
'OK',
|
||||
'OR',
|
||||
'PA',
|
||||
'RI',
|
||||
'SC',
|
||||
'SD',
|
||||
'TN',
|
||||
'TX',
|
||||
'UT',
|
||||
'VT',
|
||||
'VA',
|
||||
'WA',
|
||||
'WV',
|
||||
'WI',
|
||||
'WY',
|
||||
]);
|
||||
export const AreasServedSchema = z.object({
|
||||
county: z.string().nonempty('County is required'),
|
||||
state: z.string().nonempty('State is required'),
|
||||
});
|
||||
|
||||
export const LicensedInSchema = z.object({
|
||||
registerNo: z.string().nonempty('Registration number is required'),
|
||||
state: z.string().nonempty('State 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 UserSchema = z
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
firstname: z.string().min(2, { message: 'First name must contain at least 2 characters' }),
|
||||
lastname: z.string().min(2, { message: 'Last name must contain at least 2 characters' }),
|
||||
email: z.string().email({ message: 'Invalid email address' }),
|
||||
phoneNumber: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
companyName: z.string().optional().nullable(),
|
||||
companyOverview: z.string().optional().nullable(),
|
||||
companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
|
||||
companyLocation: GeoSchema.optional().nullable(),
|
||||
offeredServices: z.string().optional().nullable(),
|
||||
areasServed: z.array(AreasServedSchema).optional().nullable(),
|
||||
hasProfile: z.boolean().optional().nullable(),
|
||||
hasCompanyLogo: z.boolean().optional().nullable(),
|
||||
licensedIn: z.array(LicensedInSchema).optional().nullable(),
|
||||
gender: GenderEnum.optional().nullable(),
|
||||
customerType: CustomerTypeEnum,
|
||||
customerSubType: CustomerSubTypeEnum.optional().nullable(),
|
||||
created: z.date().optional().nullable(),
|
||||
updated: z.date().optional().nullable(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.customerType === 'professional') {
|
||||
if (!data.customerSubType) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Customer subtype is required for professional customers',
|
||||
path: ['customerSubType'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Phone number is required and must be in US format (XXX) XXX-XXXX for professional customers',
|
||||
path: ['phoneNumber'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.companyOverview || data.companyOverview.length < 10) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company overview must contain at least 10 characters for professional customers',
|
||||
path: ['companyOverview'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.description || data.description.length < 10) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Description must contain at least 10 characters for professional customers',
|
||||
path: ['description'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.offeredServices || data.offeredServices.length < 10) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Offered services must contain at least 10 characters for professional customers',
|
||||
path: ['offeredServices'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.companyLocation) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company location is required for professional customers',
|
||||
path: ['companyLocation'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.areasServed || data.areasServed.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'At least one area served is required for professional customers',
|
||||
path: ['areasServed'],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type AreasServed = z.infer<typeof AreasServedSchema>;
|
||||
export type LicensedIn = z.infer<typeof LicensedInSchema>;
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
|
||||
export const BusinessListingSchema = z.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
email: z.string().email(),
|
||||
type: z.string().refine(val => TypeEnum.safeParse(val).success, {
|
||||
message: 'Invalid type. Must be one of: ' + TypeEnum.options.join(', '),
|
||||
}),
|
||||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().max(1000000000),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
draft: z.boolean(),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
realEstateIncluded: z.boolean().optional().nullable(),
|
||||
leasedLocation: z.boolean().optional().nullable(),
|
||||
franchiseResale: z.boolean().optional().nullable(),
|
||||
salesRevenue: z.number().positive().max(100000000),
|
||||
cashFlow: z.number().positive().max(100000000),
|
||||
supportAndTraining: z.string().min(5),
|
||||
employees: z.number().int().positive().max(100000).optional().nullable(),
|
||||
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(),
|
||||
internals: z.string().min(5).optional().nullable(),
|
||||
imageName: z.string().optional().nullable(),
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
});
|
||||
export type BusinessListing = z.infer<typeof BusinessListingSchema>;
|
||||
|
||||
export const CommercialPropertyListingSchema = z
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
serialId: z.number().int().positive().optional().nullable(),
|
||||
email: z.string().email(),
|
||||
type: z.string().refine(val => PropertyTypeEnum.safeParse(val).success, {
|
||||
message: 'Invalid type. Must be one of: ' + PropertyTypeEnum.options.join(', '),
|
||||
}),
|
||||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().max(1000000000),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
draft: z.boolean(),
|
||||
imageOrder: z.array(z.string()),
|
||||
imagePath: z.string().nullable().optional(),
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CommercialPropertyListing = z.infer<typeof CommercialPropertyListingSchema>;
|
||||
|
||||
export const SenderSchema = z.object({
|
||||
name: z.string().min(6, { message: 'Name must be at least 6 characters long' }),
|
||||
email: z.string().email({ message: 'Invalid email address' }),
|
||||
phoneNumber: z.string().regex(/^(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/, {
|
||||
message: 'Invalid US phone number format',
|
||||
}),
|
||||
state: z.string().refine(val => USStates.safeParse(val).success, {
|
||||
message: 'Invalid state. Must be a valid 2-letter US state code.',
|
||||
}),
|
||||
comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }),
|
||||
});
|
||||
export type Sender = z.infer<typeof SenderSchema>;
|
||||
@@ -1 +0,0 @@
|
||||
../../../common-models/src/main.model.ts
|
||||
354
bizmatch-server/src/models/main.model.ts
Normal file
354
bizmatch-server/src/models/main.model.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
|
||||
|
||||
export interface StatesResult {
|
||||
state: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface KeyValue {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
export interface KeyValueRatio {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
export interface KeyValueStyle {
|
||||
name: string;
|
||||
value: string;
|
||||
oldValue?: string;
|
||||
icon: string;
|
||||
textColorClass: string;
|
||||
}
|
||||
export type SelectOption<T = number> = {
|
||||
value: T;
|
||||
label: string;
|
||||
};
|
||||
export type ImageType = {
|
||||
name: 'propertyPicture' | 'companyLogo' | 'profile';
|
||||
upload: string;
|
||||
delete: string;
|
||||
};
|
||||
export type ListingCategory = {
|
||||
name: 'business' | 'commercialProperty';
|
||||
};
|
||||
|
||||
export type ListingType = BusinessListing | CommercialPropertyListing;
|
||||
|
||||
export type ResponseBusinessListingArray = {
|
||||
results: BusinessListing[];
|
||||
totalCount: number;
|
||||
};
|
||||
export type ResponseBusinessListing = {
|
||||
data: BusinessListing;
|
||||
};
|
||||
export type ResponseCommercialPropertyListingArray = {
|
||||
results: CommercialPropertyListing[];
|
||||
totalCount: number;
|
||||
};
|
||||
export type ResponseCommercialPropertyListing = {
|
||||
data: CommercialPropertyListing;
|
||||
};
|
||||
export type ResponseUsersArray = {
|
||||
results: User[];
|
||||
totalCount: number;
|
||||
};
|
||||
export interface ListCriteria {
|
||||
start: number;
|
||||
length: number;
|
||||
page: number;
|
||||
types: string[];
|
||||
state: string;
|
||||
city: string;
|
||||
prompt: string;
|
||||
searchType: 'exact' | 'radius';
|
||||
// radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500';
|
||||
radius: number;
|
||||
criteriaType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
|
||||
}
|
||||
export interface BusinessListingCriteria extends ListCriteria {
|
||||
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;
|
||||
criteriaType: 'businessListings';
|
||||
}
|
||||
export interface CommercialPropertyListingCriteria extends ListCriteria {
|
||||
minPrice: number;
|
||||
maxPrice: number;
|
||||
title: string;
|
||||
criteriaType: 'commercialPropertyListings';
|
||||
}
|
||||
export interface UserListingCriteria extends ListCriteria {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
companyName: string;
|
||||
counties: string[];
|
||||
criteriaType: 'brokerListings';
|
||||
}
|
||||
|
||||
export interface KeycloakUser {
|
||||
id: string;
|
||||
createdTimestamp?: number;
|
||||
username?: string;
|
||||
enabled?: boolean;
|
||||
totp?: boolean;
|
||||
emailVerified?: boolean;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
disableableCredentialTypes?: any[];
|
||||
requiredActions?: any[];
|
||||
notBefore?: number;
|
||||
access?: Access;
|
||||
}
|
||||
export interface JwtUser {
|
||||
userId: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
roles: string[];
|
||||
}
|
||||
export interface Access {
|
||||
manageGroupMembership: boolean;
|
||||
view: boolean;
|
||||
mapRoles: boolean;
|
||||
impersonate: boolean;
|
||||
manage: boolean;
|
||||
}
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
userId: string;
|
||||
level: string;
|
||||
start: Date;
|
||||
modified: Date;
|
||||
end: Date;
|
||||
status: string;
|
||||
invoices: Array<Invoice>;
|
||||
}
|
||||
export interface Invoice {
|
||||
id: string;
|
||||
date: Date;
|
||||
price: number;
|
||||
}
|
||||
export interface JwtToken {
|
||||
exp: number;
|
||||
iat: number;
|
||||
auth_time: number;
|
||||
jti: string;
|
||||
iss: string;
|
||||
aud: string;
|
||||
sub: string;
|
||||
typ: string;
|
||||
azp: string;
|
||||
nonce: string;
|
||||
session_state: string;
|
||||
acr: string;
|
||||
realm_access: Realmaccess;
|
||||
resource_access: Resourceaccess;
|
||||
scope: string;
|
||||
sid: string;
|
||||
email_verified: boolean;
|
||||
name: string;
|
||||
preferred_username: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
email: string;
|
||||
user_id: string;
|
||||
}
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
preferred_username: string;
|
||||
realm_access?: {
|
||||
roles?: string[];
|
||||
};
|
||||
[key: string]: any; // für andere optionale Felder im JWT-Payload
|
||||
}
|
||||
interface Resourceaccess {
|
||||
account: Realmaccess;
|
||||
}
|
||||
interface Realmaccess {
|
||||
roles: string[];
|
||||
}
|
||||
export interface PageEvent {
|
||||
first: number;
|
||||
rows: number;
|
||||
page: number;
|
||||
pageCount: number;
|
||||
}
|
||||
export interface AutoCompleteCompleteEvent {
|
||||
originalEvent: Event;
|
||||
query: string;
|
||||
}
|
||||
export interface MailInfo {
|
||||
sender: Sender;
|
||||
email: string;
|
||||
url: string;
|
||||
listing?: BusinessListing;
|
||||
}
|
||||
// export interface Sender {
|
||||
// name?: string;
|
||||
// email?: string;
|
||||
// phoneNumber?: string;
|
||||
// state?: string;
|
||||
// comments?: string;
|
||||
// }
|
||||
export interface ImageProperty {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
export interface ErrorResponse {
|
||||
fields?: FieldError[];
|
||||
general?: string[];
|
||||
}
|
||||
export interface FieldError {
|
||||
fieldname: string;
|
||||
message: string;
|
||||
}
|
||||
export interface UploadParams {
|
||||
type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile';
|
||||
imagePath: string;
|
||||
serialId?: number;
|
||||
}
|
||||
export interface GeoResult {
|
||||
id: number;
|
||||
city: string;
|
||||
state: string;
|
||||
// state_code: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
export interface CityAndStateResult {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
state: string;
|
||||
}
|
||||
export interface CountyResult {
|
||||
id: number;
|
||||
name: string;
|
||||
state: string;
|
||||
state_code: string;
|
||||
}
|
||||
export function isEmpty(value: any): boolean {
|
||||
// Check for undefined or null
|
||||
if (value === undefined || value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for empty string or string with only whitespace
|
||||
if (typeof value === 'string') {
|
||||
return value.trim().length === 0;
|
||||
}
|
||||
|
||||
// Check for number and NaN
|
||||
if (typeof value === 'number') {
|
||||
return isNaN(value);
|
||||
}
|
||||
|
||||
// If it's not a string or number, it's not considered empty by this function
|
||||
return false;
|
||||
}
|
||||
export function emailToDirName(email: string): string {
|
||||
if (email === undefined || email === null) {
|
||||
return null;
|
||||
}
|
||||
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche
|
||||
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
|
||||
// Entferne führende und nachfolgende Unterstriche
|
||||
const trimmedEmail = sanitizedEmail.replace(/^_+|_+$/g, '');
|
||||
|
||||
// Ersetze mehrfache aufeinanderfolgende Unterstriche durch einen einzelnen Unterstrich
|
||||
const normalizedEmail = trimmedEmail.replace(/_+/g, '_');
|
||||
|
||||
return normalizedEmail;
|
||||
}
|
||||
export const LISTINGS_PER_PAGE = 12;
|
||||
export interface ValidationMessage {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
export function createDefaultUser(email: string, firstname: string, lastname: string): User {
|
||||
return {
|
||||
id: undefined,
|
||||
email,
|
||||
firstname,
|
||||
lastname,
|
||||
phoneNumber: null,
|
||||
description: null,
|
||||
companyName: null,
|
||||
companyOverview: null,
|
||||
companyWebsite: null,
|
||||
companyLocation: null,
|
||||
offeredServices: null,
|
||||
areasServed: [],
|
||||
hasProfile: false,
|
||||
hasCompanyLogo: false,
|
||||
licensedIn: [],
|
||||
gender: null,
|
||||
customerType: 'buyer',
|
||||
customerSubType: null,
|
||||
created: new Date(),
|
||||
updated: new Date(),
|
||||
};
|
||||
}
|
||||
export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
|
||||
return {
|
||||
id: undefined,
|
||||
serialId: undefined,
|
||||
email: null,
|
||||
type: null,
|
||||
title: null,
|
||||
description: null,
|
||||
location: null,
|
||||
price: null,
|
||||
favoritesForUser: [],
|
||||
draft: false,
|
||||
imageOrder: [],
|
||||
imagePath: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
listingsCategory: 'commercialProperty',
|
||||
};
|
||||
}
|
||||
export function createDefaultBusinessListing(): BusinessListing {
|
||||
return {
|
||||
id: undefined,
|
||||
email: null,
|
||||
type: null,
|
||||
title: null,
|
||||
description: null,
|
||||
location: null,
|
||||
price: null,
|
||||
favoritesForUser: [],
|
||||
draft: false,
|
||||
realEstateIncluded: false,
|
||||
leasedLocation: false,
|
||||
franchiseResale: false,
|
||||
salesRevenue: null,
|
||||
cashFlow: null,
|
||||
supportAndTraining: null,
|
||||
employees: null,
|
||||
established: null,
|
||||
internalListingNumber: null,
|
||||
reasonForSale: null,
|
||||
brokerLicencing: null,
|
||||
internals: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
listingsCategory: 'business',
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import { Entity } from "redis-om";
|
||||
import { UserBase } from "./main.model.js";
|
||||
|
||||
|
||||
export interface Geo {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -22,8 +18,8 @@ export interface Geo {
|
||||
nationality: string;
|
||||
timezones: Timezone[];
|
||||
translations: Translations;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
emoji: string;
|
||||
emojiU: string;
|
||||
states: State[];
|
||||
@@ -32,16 +28,16 @@ export interface State {
|
||||
id: number;
|
||||
name: string;
|
||||
state_code: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
type: string;
|
||||
cities: City[];
|
||||
}
|
||||
export interface City {
|
||||
id: number;
|
||||
name: string;
|
||||
latitude: string;
|
||||
longitude: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
export interface Translations {
|
||||
kr: string;
|
||||
@@ -65,6 +61,12 @@ export interface Timezone {
|
||||
abbreviation: string;
|
||||
tzName: string;
|
||||
}
|
||||
export interface UserEntity extends UserBase, Entity {
|
||||
licensedIn?: string[];
|
||||
export interface CountyData {
|
||||
state: string;
|
||||
state_full: string;
|
||||
counties: string[];
|
||||
}
|
||||
export interface CountyRequest {
|
||||
prefix: string;
|
||||
states: string[];
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// redis.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
{
|
||||
provide: 'REDIS_OPTIONS',
|
||||
useValue: {
|
||||
url: 'redis://localhost:6379'
|
||||
}
|
||||
},
|
||||
{
|
||||
inject: ['REDIS_OPTIONS'],
|
||||
provide: 'REDIS_CLIENT',
|
||||
useFactory: async (options: { url: string }) => {
|
||||
const client = createClient(options);
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
}],
|
||||
exports:['REDIS_CLIENT']
|
||||
})
|
||||
export class RedisModule {}
|
||||
export const REDIS_CLIENT = "REDIS_CLIENT";
|
||||
// redis.service.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { createClient } from 'redis';
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
@Injectable()
|
||||
export class RequestDurationMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger(RequestDurationMiddleware.name);
|
||||
|
||||
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`;
|
||||
|
||||
if (req.method === 'POST' || req.method === 'PUT') {
|
||||
const body = JSON.stringify(req.body);
|
||||
logMessage += ` - Body: ${body}`;
|
||||
}
|
||||
|
||||
this.logger.log(logMessage);
|
||||
});
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,11 @@ export class SelectOptionsController {
|
||||
typesOfBusiness: this.selectOptionsService.typesOfBusiness,
|
||||
prices: this.selectOptionsService.prices,
|
||||
listingCategories: this.selectOptionsService.listingCategories,
|
||||
categories:this.selectOptionsService.categories,
|
||||
customerTypes: this.selectOptionsService.customerTypes,
|
||||
locations: this.selectOptionsService.locations,
|
||||
typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty,
|
||||
}
|
||||
customerSubTypes: this.selectOptionsService.customerSubTypes,
|
||||
distances: this.selectOptionsService.distances,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,30 @@ import { ImageType, KeyValue, KeyValueStyle } from '../models/main.model.js';
|
||||
|
||||
@Injectable()
|
||||
export class SelectOptionsService {
|
||||
|
||||
constructor() {}
|
||||
public typesOfBusiness: Array<KeyValueStyle> = [
|
||||
{ name: 'Automotive', value: '1', icon:'fa-solid fa-car',bgColorClass:'bg-green-100',textColorClass:'text-green-600' },
|
||||
{ name: 'Industrial Services', value: '2', icon:'fa-solid fa-industry',bgColorClass:'bg-yellow-100',textColorClass:'text-yellow-600'},
|
||||
{ name: 'Real Estate', value: '3' , icon:'pi pi-building',bgColorClass:'bg-blue-100',textColorClass:'text-blue-600'},
|
||||
{ name: 'Uncategorized', value: '4' , icon:'pi pi-question',bgColorClass:'bg-cyan-100',textColorClass:'text-cyan-600'},
|
||||
{ name: 'Retail', value: '5' , icon:'fa-solid fa-money-bill-wave',bgColorClass:'bg-pink-100',textColorClass:'text-pink-600'},
|
||||
{ name: 'Oilfield SVE and MFG.', value: '6' , icon:'fa-solid fa-oil-well',bgColorClass:'bg-indigo-100',textColorClass:'text-indigo-600'},
|
||||
{ name: 'Service', value: '7' , icon:'fa-solid fa-umbrella',bgColorClass:'bg-teal-100',textColorClass:'text-teal-600'},
|
||||
{ name: 'Advertising', value: '8' , icon:'fa-solid fa-rectangle-ad',bgColorClass:'bg-orange-100',textColorClass:'text-orange-600'},
|
||||
{ name: 'Agriculture', value: '9' , icon:'fa-solid fa-wheat-awn',bgColorClass:'bg-bluegray-100',textColorClass:'text-bluegray-600'},
|
||||
{ name: 'Franchise', value: '10' , icon:'pi pi-star',bgColorClass:'bg-purple-100',textColorClass:'text-purple-600'},
|
||||
{ name: 'Professional', value: '11' , icon:'fa-solid fa-user-gear',bgColorClass:'bg-gray-100',textColorClass:'text-gray-600'},
|
||||
{ name: 'Manufacturing', value: '12' , icon:'fa-solid fa-industry',bgColorClass:'bg-red-100',textColorClass:'text-red-600'},
|
||||
{ name: 'Food and Restaurant', value: '13' , icon:'fa-solid fa-utensils',bgColorClass:'bg-primary-100',textColorClass:'text-primary-600'},
|
||||
{ name: 'Automotive', value: 'automotive', oldValue: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||
{ name: 'Industrial Services', value: 'industrialServices', oldValue: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||
{ name: 'Food and Restaurant', value: 'foodAndRestaurant', oldValue: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' },
|
||||
{ name: 'Real Estate', value: 'realEstate', oldValue: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
|
||||
{ name: 'Retail', value: 'retail', oldValue: '5', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' },
|
||||
{ name: 'Oilfield SVE and MFG.', value: 'oilfield', oldValue: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' },
|
||||
{ name: 'Service', value: 'service', oldValue: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' },
|
||||
{ name: 'Advertising', value: 'advertising', oldValue: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' },
|
||||
{ name: 'Agriculture', value: 'agriculture', oldValue: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' },
|
||||
{ name: 'Franchise', value: 'franchise', oldValue: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' },
|
||||
{ name: 'Professional', value: 'professional', oldValue: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' },
|
||||
{ name: 'Manufacturing', value: 'manufacturing', oldValue: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' },
|
||||
{ name: 'Uncategorized', value: 'uncategorized', oldValue: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' },
|
||||
];
|
||||
public typesOfCommercialProperty: Array<KeyValueStyle> = [
|
||||
{ name: 'Retail', value: '100' , icon:'fa-solid fa-money-bill-wave',bgColorClass:'bg-pink-100',textColorClass:'text-pink-600'},
|
||||
{ name: 'Land', value: '101' , icon:'pi pi-building',bgColorClass:'bg-blue-100',textColorClass:'text-blue-600'},
|
||||
{ name: 'Industrial', value: '102', icon:'fa-solid fa-industry',bgColorClass:'bg-yellow-100',textColorClass:'text-yellow-600'},
|
||||
{ name: 'Office', value: '103' , icon:'fa-solid fa-umbrella',bgColorClass:'bg-teal-100',textColorClass:'text-teal-600'},
|
||||
{ name: 'Mixed Use', value: '104' , icon:'fa-solid fa-rectangle-ad',bgColorClass:'bg-orange-100',textColorClass:'text-orange-600'},
|
||||
{ name: 'Multifamily', value: '105' , icon:'pi pi-star',bgColorClass:'bg-purple-100',textColorClass:'text-purple-600'},
|
||||
{ name: 'Uncategorized', value: '106' , icon:'pi pi-question',bgColorClass:'bg-cyan-100',textColorClass:'text-cyan-600'},
|
||||
{ name: 'Retail', value: 'retail', oldValue: '100', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' },
|
||||
{ name: 'Land', value: 'land', oldValue: '101', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
|
||||
{ name: 'Industrial', value: 'industrial', oldValue: '102', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||
{ name: 'Office', value: 'office', oldValue: '103', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' },
|
||||
{ name: 'Mixed Use', value: 'mixedUse', oldValue: '104', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' },
|
||||
{ name: 'Multifamily', value: 'multifamily', oldValue: '105', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' },
|
||||
{ name: 'Uncategorized', value: 'uncategorized', oldValue: '106', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' },
|
||||
];
|
||||
public prices: Array<KeyValue> = [
|
||||
{ name: '$100K', value: '100000' },
|
||||
@@ -36,23 +35,45 @@ export class SelectOptionsService {
|
||||
{ name: '$1M', value: '1000000' },
|
||||
{ name: '$5M', value: '5000000' },
|
||||
];
|
||||
|
||||
public distances: Array<KeyValue> = [
|
||||
{ name: '5 miles', value: '5' },
|
||||
{ name: '20 miles', value: '20' },
|
||||
{ name: '50 miles', value: '50' },
|
||||
{ name: '100 miles', value: '100' },
|
||||
{ name: '200 miles', value: '200' },
|
||||
{ name: '300 miles', value: '300' },
|
||||
{ name: '400 miles', value: '400' },
|
||||
{ name: '500 miles', value: '500' },
|
||||
];
|
||||
public listingCategories: Array<KeyValue> = [
|
||||
{ name: 'Business', value: 'business' },
|
||||
{ name: 'Commercial Property', value: 'commercialProperty' },
|
||||
]
|
||||
public categories: Array<KeyValueStyle> = [
|
||||
{ name: 'Broker', value: 'broker', icon:'pi-image',bgColorClass:'bg-green-100',textColorClass:'text-green-600' },
|
||||
{ name: 'Professional', value: 'professional', icon:'pi-globe',bgColorClass:'bg-yellow-100',textColorClass:'text-yellow-600' },
|
||||
]
|
||||
];
|
||||
public customerTypes: Array<KeyValue> = [
|
||||
{ name: 'Buyer', value: 'buyer' },
|
||||
{ name: 'Professional', value: 'professional' },
|
||||
];
|
||||
public customerSubTypes: Array<KeyValue> = [
|
||||
{ name: 'Broker', value: 'broker' },
|
||||
{ name: 'CPA', value: 'cpa' },
|
||||
{ name: 'Attorney', value: 'attorney' },
|
||||
{ name: 'Title Company', value: 'titleCompany' },
|
||||
{ name: 'Surveyor', value: 'surveyor' },
|
||||
{ name: 'Appraiser', value: 'appraiser' },
|
||||
];
|
||||
public gender: Array<KeyValue> = [
|
||||
{ name: 'Male', value: 'male' },
|
||||
{ name: 'Female', value: 'female' },
|
||||
];
|
||||
public imageTypes: ImageType[] = [
|
||||
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
|
||||
{ name: 'companyLogo', upload: 'uploadCompanyLogo', delete: 'logo' },
|
||||
{ name: 'profile', upload: 'uploadProfile', delete: 'profile' },
|
||||
]
|
||||
];
|
||||
private usStates = [
|
||||
{ name: 'ALABAMA', abbreviation: 'AL' },
|
||||
{ name: 'ALASKA', abbreviation: 'AK' },
|
||||
{ name: 'AMERICAN SAMOA', abbreviation: 'AS'},
|
||||
{ name: 'ARIZONA', abbreviation: 'AZ' },
|
||||
{ name: 'ARKANSAS', abbreviation: 'AR' },
|
||||
{ name: 'CALIFORNIA', abbreviation: 'CA' },
|
||||
@@ -60,7 +81,6 @@ export class SelectOptionsService {
|
||||
{ name: 'CONNECTICUT', abbreviation: 'CT' },
|
||||
{ name: 'DELAWARE', abbreviation: 'DE' },
|
||||
{ name: 'DISTRICT OF COLUMBIA', abbreviation: 'DC' },
|
||||
{ name: 'FEDERATED STATES OF MICRONESIA', abbreviation: 'FM'},
|
||||
{ name: 'FLORIDA', abbreviation: 'FL' },
|
||||
{ name: 'GEORGIA', abbreviation: 'GA' },
|
||||
{ name: 'GUAM', abbreviation: 'GU' },
|
||||
@@ -73,7 +93,6 @@ export class SelectOptionsService {
|
||||
{ name: 'KENTUCKY', abbreviation: 'KY' },
|
||||
{ name: 'LOUISIANA', abbreviation: 'LA' },
|
||||
{ name: 'MAINE', abbreviation: 'ME' },
|
||||
{ name: 'MARSHALL ISLANDS', abbreviation: 'MH'},
|
||||
{ name: 'MARYLAND', abbreviation: 'MD' },
|
||||
{ name: 'MASSACHUSETTS', abbreviation: 'MA' },
|
||||
{ name: 'MICHIGAN', abbreviation: 'MI' },
|
||||
@@ -89,13 +108,11 @@ export class SelectOptionsService {
|
||||
{ name: 'NEW YORK', abbreviation: 'NY' },
|
||||
{ name: 'NORTH CAROLINA', abbreviation: 'NC' },
|
||||
{ name: 'NORTH DAKOTA', abbreviation: 'ND' },
|
||||
{ name: 'NORTHERN MARIANA ISLANDS', abbreviation: 'MP'},
|
||||
{ name: 'OHIO', abbreviation: 'OH' },
|
||||
{ name: 'OKLAHOMA', abbreviation: 'OK' },
|
||||
{ name: 'OREGON', abbreviation: 'OR' },
|
||||
{ name: 'PALAU', abbreviation: 'PW' },
|
||||
{ name: 'PENNSYLVANIA', abbreviation: 'PA' },
|
||||
{ name: 'PUERTO RICO', abbreviation: 'PR'},
|
||||
{ name: 'RHODE ISLAND', abbreviation: 'RI' },
|
||||
{ name: 'SOUTH CAROLINA', abbreviation: 'SC' },
|
||||
{ name: 'SOUTH DAKOTA', abbreviation: 'SD' },
|
||||
@@ -103,13 +120,11 @@ export class SelectOptionsService {
|
||||
{ name: 'TEXAS', abbreviation: 'TX' },
|
||||
{ name: 'UTAH', abbreviation: 'UT' },
|
||||
{ name: 'VERMONT', abbreviation: 'VT' },
|
||||
{ name: 'VIRGIN ISLANDS', abbreviation: 'VI'},
|
||||
{ name: 'VIRGINIA', abbreviation: 'VA' },
|
||||
{ name: 'WASHINGTON', abbreviation: 'WA' },
|
||||
{ name: 'WEST VIRGINIA', abbreviation: 'WV' },
|
||||
{ name: 'WISCONSIN', abbreviation: 'WI' },
|
||||
{ name: 'WYOMING', abbreviation: 'WY' }
|
||||
]
|
||||
public locations:Array<any> = [...this.usStates.map(state=>({name:state.name, value:state.abbreviation}))].concat({name:'CANADA',value:"CA"});
|
||||
|
||||
{ name: 'WYOMING', abbreviation: 'WY' },
|
||||
];
|
||||
public locations: Array<any> = [...this.usStates.map(state => ({ name: state.name, value: state.abbreviation }))].concat({ name: 'CANADA', value: 'CA' });
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Controller('subscriptions')
|
||||
export class SubscriptionsController {
|
||||
constructor(private readonly fileService: FileService){}
|
||||
@Get()
|
||||
findAll(): any {
|
||||
return this.fileService.getSubscriptions();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +1,27 @@
|
||||
import { Body, Controller, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
||||
import { UserService } from './user.service.js';
|
||||
import { Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { UserEntity } from 'src/models/server.model.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { User } from '../models/db.model';
|
||||
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model.js';
|
||||
import { UserService } from './user.service.js';
|
||||
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
|
||||
constructor(private userService: UserService, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
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)}`);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findById(@Param('id') id: string): any {
|
||||
@@ -16,9 +30,8 @@ export class UserController {
|
||||
this.logger.info(`Found user: ${JSON.stringify(user)}`);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Post()
|
||||
save(@Body() user: any): Promise<UserEntity> {
|
||||
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)}`);
|
||||
@@ -26,11 +39,33 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Post('search')
|
||||
find(@Body() criteria: any): any {
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
|
||||
const foundUsers = this.userService.findUser(criteria);
|
||||
const foundUsers = this.userService.searchUserListings(criteria);
|
||||
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
|
||||
return foundUsers;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@Get('subscriptions/:id')
|
||||
async findSubscriptionsById(@Param('id') id: string): Promise<Subscription[]> {
|
||||
const subscriptions = this.fileService.getSubscriptions();
|
||||
const user = await this.userService.getUserById(id);
|
||||
subscriptions.forEach(s => {
|
||||
s.userId = user.id;
|
||||
s.start = user.created;
|
||||
s.modified = user.created;
|
||||
});
|
||||
return subscriptions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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 { RedisModule } from '../redis/redis.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [RedisModule],
|
||||
imports: [DrizzleModule, GeoModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService,FileService]
|
||||
providers: [UserService, FileService, GeoService],
|
||||
})
|
||||
export class UserModule {
|
||||
}
|
||||
export class UserModule {}
|
||||
|
||||
@@ -1,45 +1,156 @@
|
||||
import { Get, Inject, Injectable, Param } from '@nestjs/common';
|
||||
import { createClient } from 'redis';
|
||||
import { Entity, Repository, Schema } from 'redis-om';
|
||||
import { ListingCriteria, User } from '../models/main.model.js';
|
||||
import { REDIS_CLIENT } from '../redis/redis.module.js';
|
||||
import { UserEntity } from '../models/server.model.js';
|
||||
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 { 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';
|
||||
|
||||
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
userRepository:Repository;
|
||||
userSchema = new Schema('user',{
|
||||
id: { type: 'string' },
|
||||
firstname: { type: 'string' },
|
||||
lastname: { type: 'string' },
|
||||
email: { type: 'string' },
|
||||
phoneNumber: { type: 'string' },
|
||||
companyOverview:{ type: 'string' },
|
||||
companyWebsite:{ type: 'string' },
|
||||
companyLocation:{ type: 'string' },
|
||||
offeredServices:{ type: 'string' },
|
||||
areasServed:{ type: 'string[]' },
|
||||
names:{ type: 'string[]', path:'$.licensedIn.name' },
|
||||
values:{ type: 'string[]', path:'$.licensedIn.value' }
|
||||
}, {
|
||||
dataStructure: 'JSON'
|
||||
})
|
||||
constructor(@Inject(REDIS_CLIENT) private readonly redis: any,private fileService:FileService){
|
||||
this.userRepository = new Repository(this.userSchema, redis)
|
||||
this.userRepository.createIndex();
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private fileService: FileService,
|
||||
private geoService: GeoService,
|
||||
) {}
|
||||
|
||||
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
whereConditions.push(eq(schema.users.customerType, 'professional'));
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(schema.users.city, `%${criteria.city}%`));
|
||||
}
|
||||
async getUserById( id:string){
|
||||
const user = await this.userRepository.fetch(id) as UserEntity;
|
||||
user.hasCompanyLogo=this.fileService.hasCompanyLogo(id);
|
||||
user.hasProfile=this.fileService.hasProfile(id);
|
||||
return user;
|
||||
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||
whereConditions.push(sql`${getDistanceQuery(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
async saveUser(user:any):Promise<UserEntity>{
|
||||
return await this.userRepository.save(user.id,user) as UserEntity
|
||||
}
|
||||
async findUser(criteria:ListingCriteria){
|
||||
return await this.userRepository.search().return.all();
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
// whereConditions.push(inArray(schema.users.customerSubType, criteria.types));
|
||||
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.companyName) {
|
||||
whereConditions.push(ilike(schema.users.companyName, `%${criteria.companyName}%`));
|
||||
}
|
||||
|
||||
if (criteria.counties && criteria.counties.length > 0) {
|
||||
whereConditions.push(or(...criteria.counties.map(county => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'county' ILIKE ${`%${county}%`})`)));
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${criteria.state})`);
|
||||
}
|
||||
return whereConditions;
|
||||
}
|
||||
async searchUserListings(criteria: UserListingCriteria) {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn.select().from(schema.users);
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
|
||||
// Paginierung
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const data = await query;
|
||||
const results = data.map(r => convertDrizzleUserToUser(r));
|
||||
const totalCount = await this.getUserListingsCount(criteria);
|
||||
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
async getUserListingsCount(criteria: UserListingCriteria): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(schema.users);
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
countQuery.where(whereClause);
|
||||
}
|
||||
|
||||
const [{ value: totalCount }] = await countQuery;
|
||||
return totalCount;
|
||||
}
|
||||
async getUserByMail(email: string, jwtuser?: JwtUser) {
|
||||
const users = (await this.conn
|
||||
.select()
|
||||
.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);
|
||||
} else {
|
||||
const user = users[0];
|
||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||
return convertDrizzleUserToUser(user);
|
||||
}
|
||||
}
|
||||
async getUserById(id: string) {
|
||||
const users = (await this.conn
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(sql`id = ${id}`)) as User[];
|
||||
|
||||
const user = users[0];
|
||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||
return convertDrizzleUserToUser(user);
|
||||
}
|
||||
async saveUser(user: User): Promise<User> {
|
||||
try {
|
||||
user.updated = new Date();
|
||||
if (user.id) {
|
||||
user.created = new Date(user.created);
|
||||
} else {
|
||||
user.created = new Date();
|
||||
}
|
||||
const validatedUser = UserSchema.parse(user);
|
||||
const drizzleUser = convertUserToDrizzleUser(validatedUser);
|
||||
if (user.id) {
|
||||
const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning();
|
||||
return convertDrizzleUserToUser(updateUser) as User;
|
||||
} else {
|
||||
const [newUser] = await this.conn.insert(schema.users).values(drizzleUser).returning();
|
||||
return convertDrizzleUserToUser(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,3 +1,8 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { businesses, commercials, users } from './drizzle/schema.js';
|
||||
import { BusinessListing, CommercialPropertyListing, User } from './models/db.model.js';
|
||||
export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
|
||||
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
|
||||
export function convertStringToNullUndefined(value) {
|
||||
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
|
||||
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase();
|
||||
@@ -11,3 +16,110 @@ export function convertStringToNullUndefined(value) {
|
||||
// Gibt den Originalwert zurück, wenn es sich nicht um 'null' oder 'undefined' handelt
|
||||
return 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)
|
||||
))
|
||||
`;
|
||||
};
|
||||
|
||||
type DrizzleUser = typeof users.$inferSelect;
|
||||
type DrizzleBusinessListing = typeof businesses.$inferSelect;
|
||||
type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect;
|
||||
export function convertBusinessToDrizzleBusiness(businessListing: Partial<BusinessListing>): DrizzleBusinessListing {
|
||||
return flattenObject(businessListing);
|
||||
}
|
||||
export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): BusinessListing {
|
||||
const o = {
|
||||
location_city: drizzleBusinessListing.city,
|
||||
location_state: drizzleBusinessListing.state,
|
||||
location_latitude: drizzleBusinessListing.latitude,
|
||||
location_longitude: drizzleBusinessListing.longitude,
|
||||
...drizzleBusinessListing,
|
||||
};
|
||||
delete o.city;
|
||||
delete o.state;
|
||||
delete o.latitude;
|
||||
delete o.longitude;
|
||||
return unflattenObject(o);
|
||||
}
|
||||
export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial<CommercialPropertyListing>): DrizzleCommercialPropertyListing {
|
||||
return flattenObject(commercialPropertyListing);
|
||||
}
|
||||
export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): CommercialPropertyListing {
|
||||
const o = {
|
||||
location_city: drizzleCommercialPropertyListing.city,
|
||||
location_state: drizzleCommercialPropertyListing.state,
|
||||
location_latitude: drizzleCommercialPropertyListing.latitude,
|
||||
location_longitude: drizzleCommercialPropertyListing.longitude,
|
||||
...drizzleCommercialPropertyListing,
|
||||
};
|
||||
delete o.city;
|
||||
delete o.state;
|
||||
delete o.latitude;
|
||||
delete o.longitude;
|
||||
return unflattenObject(o);
|
||||
}
|
||||
export function convertUserToDrizzleUser(user: Partial<User>): DrizzleUser {
|
||||
return flattenObject(user);
|
||||
}
|
||||
|
||||
export function convertDrizzleUserToUser(drizzleUser: Partial<DrizzleUser>): User {
|
||||
const o = {
|
||||
companyLocation_city: drizzleUser.city,
|
||||
companyLocation_state: drizzleUser.state,
|
||||
companyLocation_latitude: drizzleUser.latitude,
|
||||
companyLocation_longitude: drizzleUser.longitude,
|
||||
...drizzleUser,
|
||||
};
|
||||
delete o.city;
|
||||
delete o.state;
|
||||
delete o.latitude;
|
||||
delete o.longitude;
|
||||
return unflattenObject(o);
|
||||
}
|
||||
function flattenObject(obj: any, res: any = {}): any {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const value = obj[key];
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
if (value instanceof Date) {
|
||||
res[key] = value;
|
||||
} else {
|
||||
flattenObject(value, res);
|
||||
}
|
||||
} else {
|
||||
res[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function unflattenObject(obj: any, separator: string = '_'): any {
|
||||
const result: any = {};
|
||||
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const keys = key.split(separator);
|
||||
keys.reduce((acc, curr, idx) => {
|
||||
if (idx === keys.length - 1) {
|
||||
acc[curr] = obj[key];
|
||||
} else {
|
||||
if (!acc[curr]) {
|
||||
acc[curr] = {};
|
||||
}
|
||||
}
|
||||
return acc[curr];
|
||||
}, result);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
68
bizmatch-server/src/utils/importCounties.ts
Normal file
68
bizmatch-server/src/utils/importCounties.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as fs from 'fs';
|
||||
import * as readline from 'readline';
|
||||
|
||||
interface CityData {
|
||||
city: string;
|
||||
stateShort: string;
|
||||
stateFull: string;
|
||||
county: string;
|
||||
cityAlias: string;
|
||||
}
|
||||
|
||||
interface StateCountyData {
|
||||
state: string;
|
||||
state_full: string;
|
||||
counties: string[];
|
||||
}
|
||||
|
||||
async function parseData(filePath: string): Promise<CityData[]> {
|
||||
const fileStream = fs.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
const data: CityData[] = [];
|
||||
let isFirstLine = true;
|
||||
|
||||
for await (const line of rl) {
|
||||
if (isFirstLine) {
|
||||
isFirstLine = false;
|
||||
continue; // Skip the first line
|
||||
}
|
||||
const [city, stateShort, stateFull, county, cityAlias] = line.split('|');
|
||||
data.push({ city, stateShort, stateFull, county, cityAlias });
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function transformData(data: CityData[]): StateCountyData[] {
|
||||
const stateMap: { [key: string]: { stateFull: string; counties: Set<string> } } = {};
|
||||
|
||||
data.forEach(item => {
|
||||
if (!stateMap[item.stateShort]) {
|
||||
stateMap[item.stateShort] = {
|
||||
stateFull: item.stateFull,
|
||||
counties: new Set(),
|
||||
};
|
||||
}
|
||||
stateMap[item.stateShort].counties.add(item.county);
|
||||
});
|
||||
|
||||
return Object.entries(stateMap).map(([state, value]) => ({
|
||||
state,
|
||||
state_full: value.stateFull,
|
||||
counties: Array.from(value.counties).sort(),
|
||||
}));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const filePath = './src/assets/counties_raw.csv'; // Ersetze diesen Pfad mit dem Pfad zu deiner Datei
|
||||
const cityData = await parseData(filePath);
|
||||
const stateCountyData = transformData(cityData);
|
||||
|
||||
console.log(JSON.stringify(stateCountyData, null, 2));
|
||||
}
|
||||
|
||||
main().catch(err => console.error(err));
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"],
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
@@ -16,7 +16,8 @@
|
||||
"strictNullChecks": false,
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": false,
|
||||
"esModuleInterop":true
|
||||
}
|
||||
}
|
||||
|
||||
18
bizmatch/.prettierrc.json
Normal file
18
bizmatch/.prettierrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 220,
|
||||
"proseWrap": "always",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
28
bizmatch/.vscode/settings.json
vendored
Normal file
28
bizmatch/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"editor.suggestSelection": "first",
|
||||
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
|
||||
"explorer.confirmDelete": false,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"prettier.printWidth": 240,
|
||||
"git.autofetch": false,
|
||||
"git.autorefresh": true
|
||||
}
|
||||
32
bizmatch/certs/cert.pem
Normal file
32
bizmatch/certs/cert.pem
Normal file
@@ -0,0 +1,32 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFgTCCA2mgAwIBAgIUa+QJdPmuRDNbuf/nzb2J+6ii5nwwDQYJKoZIhvcNAQEL
|
||||
BQAwUDELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzEQMA4GA1UEBwwHRVJLUkFU
|
||||
SDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTI0MDUxMDEy
|
||||
NTQxNVoXDTI1MDUxMDEyNTQxNVowUDELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05S
|
||||
VzEQMA4GA1UEBwwHRVJLUkFUSDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
|
||||
dHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxiTSQDCC/i3n
|
||||
X6bMKpl0baUgjbzYDc7ZrvIYfj/t25sdv0E/07ysbXNuldzCX6Rnva/1wVZS30zy
|
||||
vQm8cVM074oP9qy7wKeIU15nEwRe03P5zipix1WXGWXHY+ShZ2MHy/iDQ1XzpO3p
|
||||
xXs2vxuJZSUoz1M0c+/pTWBx2D790l5qNkt2sbk5NaHfPQDuw+y2cXDqmJqcCi9I
|
||||
rYbaQGhXeb/IRu8pW4UwasVsq7DxGlDX8k8Dva5O0Ixf+muqQELuMdeTtR/PoFxw
|
||||
2F+qOUlS2ujuyQkLOvVZOTalxRfWMuexaQzLlQO91MDehTrOFuMUBCKhYztgZKe2
|
||||
k9z0fTJmtLyxMPTQuZCv1Gnrw6hcVxjiFQ8YP2ni+ekb86dIA3llH8r+4xEGygfB
|
||||
QxHiBH9uO8Q9MFpfU2CPE7GxQoB17fu4KqaK0ucVnNM+rJcsNom9svixb5C4CkS/
|
||||
S1/KQVDi8mrYwQIOP+Y4YLuNvSvUlitZXq8h0ogVqNMl2+R0CYX4lk/mkOEeCeGW
|
||||
yG4ek2GQxZNLAnoMoLb+kHnVhPaV0SWW052wvXZzOrIMrlkSZK6yYim3JPsD8hc/
|
||||
284lNEFL3DknICPsVFd64LjwPxA0J5AqyhQAvpXyFVHUUA5+h2EATrBh/Fp9cw84
|
||||
AkEeVArMWOlx5cg7nAdgQaD5XUaBp7kCAwEAAaNTMFEwHQYDVR0OBBYEFMSO9FoT
|
||||
nqjHpniyExGf53tV/TAhMB8GA1UdIwQYMBaAFMSO9FoTnqjHpniyExGf53tV/TAh
|
||||
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBADbsIroXoCIe8adn
|
||||
zh/WNZUUXLDW3JU2QyyYKhnZgqE0Wqh5QLgNwd5ZfH3Iaqhf0xFH9jUeEAWWA17d
|
||||
hVy4rWsC200DRZ5BYOaffqdflpDE3yAk+8p3kAWjogaCX1wvBuLZ9BHWpuqQ72ec
|
||||
zYM2ag2wWfBpicXl4BaSnsx1xErxKvxgixgGy6BhcErmfnYtJDnU2Cl+MSMXb758
|
||||
7hAl9JiJAH8OuaAjvbkhSVTZQFjDgCGHHQ8mR8IksWdGGe/LN/yoWc4lk7lv7vmu
|
||||
YBfP/SNZvxBzZlw1fXcdz1Wirljy7yz3+s59Knkc2jysysFC4LkZlUy0unmGoPy0
|
||||
D1XdXyDMy05eoUaeRnM0rfzUxYfXMA3sQlsWw7he6fD8YylVedXxd3mcfK0jle0y
|
||||
VkDyreZ9+mc/4vmjW0KpOfFGvhhAS9L1D8K3bKpky3HoHSqK1Nb8Ymh/WkhOpHwg
|
||||
unUyIKdRHvGeWkUXQaLbRKI6w2BQwT7oKDOD60cJG26U3XcYarevz9qHsZX865tj
|
||||
4xZrp+IUr8OkYBnRrmx2TZ70goRXI77nHVzHmY+xHhjvPJOZOcUAvEHU+5VY3ucN
|
||||
0noEqiYzb77LcqVbbL3cywDLiyfdx9/x8TU1iYPA+IMwhYb/tLBFzFWmR7znw6On
|
||||
D775XK/EVryozX/6GmtG+XGZs+57
|
||||
-----END CERTIFICATE-----
|
||||
52
bizmatch/certs/key.pem
Normal file
52
bizmatch/certs/key.pem
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDGJNJAMIL+Ledf
|
||||
pswqmXRtpSCNvNgNztmu8hh+P+3bmx2/QT/TvKxtc26V3MJfpGe9r/XBVlLfTPK9
|
||||
CbxxUzTvig/2rLvAp4hTXmcTBF7Tc/nOKmLHVZcZZcdj5KFnYwfL+INDVfOk7enF
|
||||
eza/G4llJSjPUzRz7+lNYHHYPv3SXmo2S3axuTk1od89AO7D7LZxcOqYmpwKL0it
|
||||
htpAaFd5v8hG7ylbhTBqxWyrsPEaUNfyTwO9rk7QjF/6a6pAQu4x15O1H8+gXHDY
|
||||
X6o5SVLa6O7JCQs69Vk5NqXFF9Yy57FpDMuVA73UwN6FOs4W4xQEIqFjO2Bkp7aT
|
||||
3PR9Mma0vLEw9NC5kK/UaevDqFxXGOIVDxg/aeL56Rvzp0gDeWUfyv7jEQbKB8FD
|
||||
EeIEf247xD0wWl9TYI8TsbFCgHXt+7gqporS5xWc0z6slyw2ib2y+LFvkLgKRL9L
|
||||
X8pBUOLyatjBAg4/5jhgu429K9SWK1leryHSiBWo0yXb5HQJhfiWT+aQ4R4J4ZbI
|
||||
bh6TYZDFk0sCegygtv6QedWE9pXRJZbTnbC9dnM6sgyuWRJkrrJiKbck+wPyFz/b
|
||||
ziU0QUvcOScgI+xUV3rguPA/EDQnkCrKFAC+lfIVUdRQDn6HYQBOsGH8Wn1zDzgC
|
||||
QR5UCsxY6XHlyDucB2BBoPldRoGnuQIDAQABAoICAAazixrZqSyAj/E3unL8Yqgs
|
||||
rAevKd15r/oPPQ3UCq7hNaXYxphaKri+7TALWdWTQWD0eQrTaRUdTJ5hHGr2xfUO
|
||||
BdExcV4oLF+pczH89VoQc5Pp8hJMzkHxI8e4nU7aVhKrcoEOAKIE2+Guc6EOBN0T
|
||||
Xyh352++XvUbfG40XzBEujHg5oBHQ+yQ73RoOisNL/RxPbXwkLN1eu9Hfs0r2j2H
|
||||
Y3Yms47hV8xcpfq+jsD1mAAddQJuyUKbZMma55SpztWHtXqsO0Dwr25Z+e9bD/7Q
|
||||
XvcUo7kYQC7DruKWFkv9cw4a/S2qhTqTVVNLNFoozu3+39dz1CRDWdTxZaFwWXHX
|
||||
KWc/YRmi5gxjadF3F87Ur+DYBLUJT/Iz5iUfUOQ/OON4ARDUM0UNqC1DemNxxI53
|
||||
eiCw21GYwIA/59w7cWHOHJZImXKBMVJ3ko7hjI1eo4LWPvDfQHlG+5Fo9vUNPk9c
|
||||
GiIreWMIycc5yUm9y1zxTa5zJdOBS+dQD0T230++o9OPytAiz2wT4ChXBfm02WUs
|
||||
0UaU3uGSDmsvt4WI9SjlvSQi12EVekYc86lmlpgtMxc6zBpwJaFSrZa+N4ax9A5P
|
||||
hOOeO6jy5XjEHTcIW+Yhrb0PCEr/U2xSn+zfhclHGTuwQLTdB30SB1pPECtA1j2+
|
||||
rdcFx9+1asoIlQxO+k2pAoIBAQDsQQesFELq96/+7rTSgN1lm19AqKvm102WiOHT
|
||||
CgJhltH19PyOnO23MKQKoYabTsSH2kOpwaA3CKPTk1ecU0P6mptN1xxEN7NgkRKC
|
||||
Wr/pilXKTJg98A/o0zbxZkeYmyT3x6XwWVU1w4msR5lQ7bH4eR6MaQDghAKTRrAS
|
||||
XMRFM4WQBfWr6DLTtz6Am5o+dA5qr8iIdjqGn8CgzPPqFQ/1ItWRkR2eiaGBUUhY
|
||||
2Z8sL+3WW8h0u5qULqIrpY4EjLfl68FFbYgoxSLskQMpX4H/fCc8s9XPn527Ckiv
|
||||
UuoRP7wsdcpt3H3SGDB8ZePH2JAbeu7nauAYeoSuzmqLwsF9AoIBAQDWtF96PpHK
|
||||
FGQ8Epy79FDuJ95qaMFrfHRhx60erb2hHdwUYpBgGXHzN2icdP6Jg90ZyLdU97xH
|
||||
QSNrTLxeUYcCZf0pucPYMFvNdMhTXiYLVpfwkho7Wl4YmuXEOG5ZW4pp0+sgFF/X
|
||||
V8p2hu5QART6JwVsyBrO2T7EoGVDppbbhzF6tXnuLDV/JiP6/QcEYMxitQ06s6B2
|
||||
8MXIqqNbidaCoALmeDgzderSKmiSGHWcAO6mef+xh0qZMfpOjwVLRTQyheiHJaub
|
||||
DkBbtNu91kPJoyyn5+dCbuuK+tOii3FSANBAFH19esZJfcuZWo9x9dqZsT0HZ0lE
|
||||
tlUDXhGBrVPtAoIBAHfD86a5Ur8YtyCOVB5Oc23Z2OzHVPWd+dgxJgG9Fj3wnhmI
|
||||
iyukxCFUyCQXhExhHuIbtKdu39BmUd6k2AoIb/Kvw8EvJkYy0n1GrdJlPNqgZSM7
|
||||
twXXF8mYoUa46dyj8ZamoCl6r+akbLtoRIGxLcJfbCwT4vzuDvwoHoQAgQLvvmqn
|
||||
isYN3Q5U25uIxiWY4eIVoJwFC2BJxfX+UDw/VyqW8RttLE29SaFr2jgogjd9SJ2d
|
||||
Q75hiFhMV6u2rosB5wvoer6+awL4BN9WF/s2Tol8n8t3AxHQwb4a1YQDjWMXI0aK
|
||||
pAcTerkxyAqYAGPEFjHIHSo1lMrz+SVAwOR+42UCggEAUbVxRId9WidqggYfSdRP
|
||||
3GKl3V8ihPJnJDMmai96pE9Fyyg7g6cLW6ExmaFYoSLiyQY+5wIk0AU1IoeghFCI
|
||||
jdwcfX2pz6OPvF/+QOPqnJQG3NHtU7svZjPEz2kebblNsrqol5vJYZ2SeosdNKtE
|
||||
vXKOOPjqYuAAaDoWb6l9bexEY0ufLIn8jfgI52LWAc+I2OPINhfYMIuu6ZAu/Q42
|
||||
6Z1VnToRQVxV0ke7ZiYS1Bzytb5mFbzEIgsIFE+PlzauB7A4bv5iEW9aBMyOd++L
|
||||
+rezre6ubvThhRGx6wEgTjHrDwf9Pfy0a5GJI0J4pskGuUjfTer70j+FmPN6vBwn
|
||||
fQKCAQANeREfOILt9Unwpbo9Vj48BMfVYvJN7Gk4K6LGWN0rE0jxtpAzBiI3BqI0
|
||||
+oj1gy+6Nn9n4hbeqDSyVB5uivCxmFIXpPO1s8Xu+EpEm90Po/551wWBvePOe8YK
|
||||
vJK+UqUXDDcG+CUKsY8quOrNFIbSu4vOB81lgELh/cfhF/C5yOCsJx5pk5TJFwl8
|
||||
3mAlV6KKTcacqEB/kKg+3sY1sv31EdsvpwOcEmXRXhI6hv4yENk0+cEFpEgJ7gkH
|
||||
gzJ5IYYSEAhfy9lPDOhwTG3VC8Fr/z6gld6V6hym9cv2emd6ifjnP4rivsGg8d77
|
||||
qs7lw2IbVhzRkVryySXsCXn2O1iu
|
||||
-----END PRIVATE KEY-----
|
||||
95
bizmatch/dbschema.ts
Normal file
95
bizmatch/dbschema.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/* 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 -s public
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export type Json = unknown;
|
||||
|
||||
// Table businesses
|
||||
export interface Businesses {
|
||||
id: string;
|
||||
userId: string | null;
|
||||
type: number | null;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
price: number | null;
|
||||
favoritesForUser: string[] | null;
|
||||
draft: boolean | null;
|
||||
listingsCategory: string | 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;
|
||||
created: Date | null;
|
||||
updated: Date | null;
|
||||
visits: number | null;
|
||||
lastVisit: Date | null;
|
||||
}
|
||||
export interface BusinessesInput {
|
||||
id?: string;
|
||||
userId?: string | null;
|
||||
type?: number | null;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
price?: number | null;
|
||||
favoritesForUser?: string[] | null;
|
||||
draft?: boolean | null;
|
||||
listingsCategory?: string | 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;
|
||||
created?: Date | null;
|
||||
updated?: Date | null;
|
||||
visits?: number | null;
|
||||
lastVisit?: Date | null;
|
||||
}
|
||||
const businesses = {
|
||||
tableName: 'businesses',
|
||||
columns: ['id', 'userId', 'type', 'title', 'description', 'city', 'state', 'price', 'favoritesForUser', 'draft', 'listingsCategory', 'realEstateIncluded', 'leasedLocation', 'franchiseResale', 'salesRevenue', 'cashFlow', 'supportAndTraining', 'employees', 'established', 'internalListingNumber', 'reasonForSale', 'brokerLicencing', 'internals', 'created', 'updated', 'visits', 'lastVisit'],
|
||||
requiredForInsert: [],
|
||||
primaryKey: 'id',
|
||||
foreignKeys: { userId: { table: 'users', column: 'id', $type: null as unknown /* users */ }, },
|
||||
$type: null as unknown as Businesses,
|
||||
$input: null as unknown as BusinessesInput
|
||||
} as const;
|
||||
|
||||
|
||||
export interface TableTypes {
|
||||
businesses: {
|
||||
select: Businesses;
|
||||
input: BusinessesInput;
|
||||
};
|
||||
}
|
||||
|
||||
export const tables = {
|
||||
businesses,
|
||||
}
|
||||
36
bizmatch/live-server.js
Normal file
36
bizmatch/live-server.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const liveServer = require('live-server');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const spdy = require('spdy');
|
||||
|
||||
const options = {
|
||||
port: 5000,
|
||||
root: '../bizmatch-server/pictures',
|
||||
open: false,
|
||||
https: {
|
||||
cert: fs.readFileSync('certs/cert.pem'),
|
||||
key: fs.readFileSync('certs/key.pem'),
|
||||
},
|
||||
middleware: []
|
||||
};
|
||||
|
||||
spdy.createServer(options.https, (req, res) => {
|
||||
liveServer.middleware(options.middleware)(req, res, () => {
|
||||
const filePath = path.join(options.root, req.url);
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(200);
|
||||
res.end(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).listen(options.port, (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
} else {
|
||||
console.log(`Live server is running on https://localhost:${options.port}`);
|
||||
}
|
||||
});
|
||||
@@ -3,64 +3,72 @@
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"build.dev": "ng build --configuration dev",
|
||||
"start": "ng serve --host 0.0.0.0 & http-server ../bizmatch-server",
|
||||
"prebuild": "node version.js",
|
||||
"build": "node version.js && ng build",
|
||||
"build.dev": "node version.js && ng build --configuration dev --output-hashing=all",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test",
|
||||
"serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.3.3",
|
||||
"@angular/cdk": "^17.3.2",
|
||||
"@angular/common": "^17.3.3",
|
||||
"@angular/compiler": "^17.3.3",
|
||||
"@angular/core": "^17.3.3",
|
||||
"@angular/forms": "^17.3.3",
|
||||
"@angular/platform-browser": "^17.3.3",
|
||||
"@angular/platform-browser-dynamic": "^17.3.3",
|
||||
"@angular/platform-server": "^17.3.3",
|
||||
"@angular/router": "^17.3.3",
|
||||
"@fortawesome/angular-fontawesome": "^0.14.1",
|
||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"angular-cropperjs": "^14.0.1",
|
||||
"angular-mixed-cdk-drag-drop": "^2.2.3",
|
||||
"@angular/animations": "^18.1.3",
|
||||
"@angular/cdk": "^18.0.6",
|
||||
"@angular/common": "^18.1.3",
|
||||
"@angular/compiler": "^18.1.3",
|
||||
"@angular/core": "^18.1.3",
|
||||
"@angular/forms": "^18.1.3",
|
||||
"@angular/platform-browser": "^18.1.3",
|
||||
"@angular/platform-browser-dynamic": "^18.1.3",
|
||||
"@angular/platform-server": "^18.1.3",
|
||||
"@angular/router": "^18.1.3",
|
||||
"@fortawesome/angular-fontawesome": "^0.15.0",
|
||||
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@ng-select/ng-select": "^13.4.1",
|
||||
"@ngneat/until-destroy": "^10.0.0",
|
||||
"@types/cropperjs": "^1.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"browser-bunyan": "^1.8.0",
|
||||
"cropperjs": "^1.6.1",
|
||||
"dayjs": "^1.11.11",
|
||||
"express": "^4.18.2",
|
||||
"flowbite": "^2.4.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"keycloak-js": "^23.0.7",
|
||||
"keycloak-angular": "^16.0.1",
|
||||
"keycloak-js": "^25.0.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"ngx-currency": "^18.0.0",
|
||||
"ngx-image-cropper": "^8.0.0",
|
||||
"ngx-mask": "^18.0.0",
|
||||
"ngx-quill": "^26.0.5",
|
||||
"on-change": "^5.0.1",
|
||||
"primeflex": "^3.3.1",
|
||||
"primeicons": "^6.0.1",
|
||||
"primeng": "^17.10.0",
|
||||
"quill": "^1.3.7",
|
||||
"rxjs": "~7.8.1",
|
||||
"tslib": "^2.3.0",
|
||||
"tslib": "^2.6.3",
|
||||
"urlcat": "^3.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
"zone.js": "~0.14.4"
|
||||
"uuid": "^10.0.0",
|
||||
"zone.js": "~0.14.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.3.3",
|
||||
"@angular/cli": "^17.3.3",
|
||||
"@angular/compiler-cli": "^17.3.3",
|
||||
"@angular-devkit/build-angular": "^18.1.3",
|
||||
"@angular/cli": "^18.1.3",
|
||||
"@angular/compiler-cli": "^18.1.3",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jasmine": "~5.1.4",
|
||||
"@types/node": "^20.11.20",
|
||||
"@types/node": "^20.14.9",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"http-server": "^14.1.1",
|
||||
"jasmine-core": "~5.1.2",
|
||||
"karma": "~6.4.2",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.3.3"
|
||||
"postcss": "^8.4.39",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "~5.4.5"
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,9 @@
|
||||
"/api": {
|
||||
"target": "http://localhost:3000",
|
||||
"secure": false
|
||||
},
|
||||
"/pictures": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,55 @@
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<!-- <div class="container"> -->
|
||||
<div [ngClass]="{ 'bg-slate-100': actualRoute !== 'home' }">
|
||||
@if (actualRoute !=='home' && actualRoute !=='pricing'){
|
||||
<header></header>
|
||||
}
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
<footer></footer>
|
||||
</div>
|
||||
<!-- @if (loadingService.isLoading$ | async) { -->
|
||||
<!-- <div class="progress-spinner flex h-full align-items-center justify-content-center">
|
||||
<div class="spinner-text">Please wait - we're processing your image...</div>
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
</div> -->
|
||||
<!-- } -->
|
||||
|
||||
@if (loadingService.isLoading$ | async) {
|
||||
<div class="spinner-overlay">
|
||||
<div class="spinner-container">
|
||||
<p-progressSpinner></p-progressSpinner>
|
||||
<div class="spinner-text" *ngIf="loadingService.loadingText$ | async as loadingText">{{loadingText}}</div>
|
||||
@let loadingText = (loadingService.loadingText$ | async); @if(loadingText){
|
||||
<div class="spinner-text">{{ loadingText }}</div>
|
||||
}
|
||||
<div role="status">
|
||||
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<!-- <span class="sr-only">Loading ...</span> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- <div *ngIf="loadingService.isLoading$ | async" class="spinner-overlay">
|
||||
<div class="spinner-container">
|
||||
<ng-container *ngIf="loadingService.loadingText$ | async as loadingText">
|
||||
<div *ngIf="loadingText" class="spinner-text">{{ loadingText }}</div>
|
||||
</ng-container>
|
||||
<div role="status">
|
||||
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<app-message-container></app-message-container>
|
||||
<app-search-modal></app-search-modal>
|
||||
<app-confirmation></app-confirmation>
|
||||
|
||||
@@ -1,39 +1,29 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.content {
|
||||
flex: 1;
|
||||
/* Optional: Padding für den Inhalt, um sicherzustellen, dass er nicht direkt am Footer klebt */
|
||||
// padding-bottom: 20px;
|
||||
}
|
||||
.progress-spinner {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
// .progress-spinner {
|
||||
// position: fixed;
|
||||
// z-index: 999;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// bottom: 0;
|
||||
// right: 0;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// align-items: center;
|
||||
// }
|
||||
|
||||
.progress-spinner:before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
// .progress-spinner:before {
|
||||
// content: '';
|
||||
// display: block;
|
||||
// position: fixed;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// background-color: rgba(0, 0, 0, 0.3);
|
||||
// }
|
||||
.spinner-text {
|
||||
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
|
||||
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */
|
||||
color: #FFF;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 8px rgba(255, 255, 255, 0.6); /* Hinzufügen eines leichten Glows */
|
||||
font-weight: bold; /* Macht den Text fett */
|
||||
}
|
||||
@@ -56,9 +46,3 @@
|
||||
align-items: center;
|
||||
/* Keine Hintergrundfarbe hier, um Transparenz nur im Overlay zu haben */
|
||||
}
|
||||
|
||||
// .spinner-text {
|
||||
// margin-top: 10px; /* Abstand zwischen Spinner und Text anpassen */
|
||||
// font-size: 16px; /* Schriftgröße nach Bedarf anpassen */
|
||||
// color: #FFF; /* Schriftfarbe für bessere Lesbarkeit auf dunklem Hintergrund */
|
||||
// }
|
||||
@@ -1,48 +1,63 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||
import { HeaderComponent } from './components/header/header.component';
|
||||
import { ProgressSpinnerModule } from 'primeng/progressspinner';
|
||||
import { ToastModule } from 'primeng/toast';
|
||||
import { LoadingService } from './services/loading.service';
|
||||
import { HomeComponent } from './pages/home/home.component';
|
||||
import { KeycloakService } from 'keycloak-angular';
|
||||
|
||||
import { filter } from 'rxjs/operators';
|
||||
import build from '../build';
|
||||
import { ConfirmationComponent } from './components/confirmation/confirmation.component';
|
||||
import { ConfirmationService } from './components/confirmation/confirmation.service';
|
||||
import { FooterComponent } from './components/footer/footer.component';
|
||||
import { KeycloakService } from './services/keycloak.service';
|
||||
import { KeycloakEventType } from './models/keycloak-event';
|
||||
import { createGenericObject } from './utils/utils';
|
||||
import onChange from 'on-change';
|
||||
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 { LoadingService } from './services/loading.service';
|
||||
import { UserService } from './services/user.service';
|
||||
import {ListingCriteria, User} from '../../../common-models/src/main.model'
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [CommonModule, RouterOutlet, HeaderComponent, ProgressSpinnerModule, FooterComponent],
|
||||
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
|
||||
providers: [],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
styleUrl: './app.component.scss',
|
||||
})
|
||||
export class AppComponent {
|
||||
build = build;
|
||||
title = 'bizmatch';
|
||||
actualRoute = '';
|
||||
user:User;
|
||||
listingCriteria:ListingCriteria = onChange(createGenericObject<ListingCriteria>(),(path, value, previousValue, applyData)=>{
|
||||
sessionStorage.setItem('criteria',JSON.stringify(value));
|
||||
});
|
||||
public constructor(public loadingService: LoadingService, private router: Router,private activatedRoute: ActivatedRoute, private keycloakService:KeycloakService,private userService:UserService) {
|
||||
this.router.events.pipe(
|
||||
filter(event => event instanceof NavigationEnd)
|
||||
).subscribe(() => {
|
||||
|
||||
public constructor(
|
||||
public loadingService: LoadingService,
|
||||
private router: Router,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private keycloakService: KeycloakService,
|
||||
private userService: UserService,
|
||||
private confirmationService: ConfirmationService,
|
||||
) {
|
||||
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
|
||||
let currentRoute = this.activatedRoute.root;
|
||||
while (currentRoute.children[0] !== undefined) {
|
||||
currentRoute = currentRoute.children[0];
|
||||
}
|
||||
// Hier haben Sie Zugriff auf den aktuellen Route-Pfad
|
||||
this.actualRoute=currentRoute.snapshot.url[0].path
|
||||
this.actualRoute = currentRoute.snapshot.url[0].path;
|
||||
});
|
||||
}
|
||||
ngOnInit(){
|
||||
this.user = this.userService.getUser();
|
||||
ngOnInit() {}
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
showVersionDialog() {
|
||||
this.confirmationService.showConfirmation({ message: `App Version: ${this.build.timestamp}`, buttons: 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
import { APP_INITIALIZER, ApplicationConfig, LOCALE_ID, importProvidersFrom } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import { provideClientHydration } from '@angular/platform-browser';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withFetch, withInterceptors, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
|
||||
import { provideQuillConfig } from 'ngx-quill';
|
||||
import { environment } from '../environments/environment';
|
||||
import { SelectOptionsService } from './services/select-options.service';
|
||||
import { KeycloakService } from './services/keycloak.service';
|
||||
import { UserService } from './services/user.service';
|
||||
import { customKeycloakAdapter } from '../keycloak';
|
||||
import { routes } from './app.routes';
|
||||
import { LoadingInterceptor } from './interceptors/loading.interceptor';
|
||||
import { KeycloakInitializerService } from './services/keycloak-initializer.service';
|
||||
import { SelectOptionsService } from './services/select-options.service';
|
||||
import { createLogger } from './utils/utils';
|
||||
// provideClientHydration()
|
||||
const logger = createLogger('ApplicationConfig');
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
{ provide: KeycloakService },
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeKeycloak,
|
||||
// useFactory: initializeKeycloak,
|
||||
//useFactory: initializeKeycloak,
|
||||
useFactory: initializeKeycloak3,
|
||||
multi: true,
|
||||
deps: [KeycloakService],
|
||||
//deps: [KeycloakService],
|
||||
deps: [KeycloakInitializerService],
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
@@ -30,26 +36,68 @@ export const appConfig: ApplicationConfig = {
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: LoadingInterceptor,
|
||||
multi:true
|
||||
multi: true,
|
||||
},
|
||||
provideRouter(routes),provideAnimations(),
|
||||
// {provide: LOCALE_ID, useValue: 'en-US' }
|
||||
]
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: KeycloakBearerInterceptor,
|
||||
multi: true,
|
||||
},
|
||||
provideRouter(
|
||||
routes,
|
||||
withEnabledBlockingInitialNavigation(),
|
||||
withInMemoryScrolling({
|
||||
scrollPositionRestoration: 'enabled',
|
||||
anchorScrolling: 'enabled',
|
||||
}),
|
||||
),
|
||||
provideAnimations(),
|
||||
provideQuillConfig({
|
||||
modules: {
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'], // Einige Standardoptionen
|
||||
[{ header: [1, 2, 3, false] }], // Benutzerdefinierte Header
|
||||
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||
[{ color: [] }], // Dropdown mit Standardfarben
|
||||
['clean'], // Entfernt Formatierungen
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
function initUserService(userService:UserService) {
|
||||
return () => {
|
||||
//selectOptions.init();
|
||||
}
|
||||
}
|
||||
function initServices(selectOptions: SelectOptionsService) {
|
||||
return () => {
|
||||
selectOptions.init();
|
||||
return async () => {
|
||||
await selectOptions.init();
|
||||
};
|
||||
}
|
||||
export function initializeKeycloak3(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 () =>
|
||||
keycloak.init({
|
||||
return async () => {
|
||||
logger.info(`###>calling keycloakService init ...`);
|
||||
const authenticated = await keycloak.init({
|
||||
config: {
|
||||
url: environment.keycloak.url,
|
||||
realm: environment.keycloak.realm,
|
||||
@@ -57,7 +105,13 @@ function initializeKeycloak(keycloak: KeycloakService) {
|
||||
},
|
||||
initOptions: {
|
||||
onLoad: 'check-sso',
|
||||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html'
|
||||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||
},
|
||||
bearerExcludedUrls: ['/assets'],
|
||||
shouldUpdateToken(request) {
|
||||
return !request.headers.get('token-update') === false;
|
||||
},
|
||||
});
|
||||
logger.info(`+++>${authenticated}`);
|
||||
};
|
||||
}
|
||||
@@ -1,84 +1,132 @@
|
||||
import { Routes } from '@angular/router';
|
||||
import { ListingsComponent } from './pages/listings/listings.component';
|
||||
import { HomeComponent } from './pages/home/home.component';
|
||||
import { DetailsListingComponent } from './pages/details/details-listing/details-listing.component';
|
||||
import { AccountComponent } from './pages/subscription/account/account.component';
|
||||
import { EditListingComponent } from './pages/subscription/edit-listing/edit-listing.component';
|
||||
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
|
||||
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
|
||||
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
|
||||
import { authGuard } from './guards/auth.guard';
|
||||
import { PricingComponent } from './pages/pricing/pricing.component';
|
||||
import { LogoutComponent } from './components/logout/logout.component';
|
||||
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
|
||||
import { NotFoundComponent } from './components/not-found/not-found.component';
|
||||
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { ListingCategoryGuard } from './guards/listing-category.guard';
|
||||
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';
|
||||
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 { 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';
|
||||
import { EditCommercialPropertyListingComponent } from './pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component';
|
||||
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';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: 'listings/:type',
|
||||
component: ListingsComponent,
|
||||
path: 'businessListings',
|
||||
component: BusinessListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
// Umleitung von /listing zu /listing/business
|
||||
{
|
||||
path: 'listings',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'listings/business',
|
||||
runGuardsAndResolvers:'always'
|
||||
path: 'commercialPropertyListings',
|
||||
component: CommercialPropertyListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'brokerListings',
|
||||
component: BrokerListingsComponent,
|
||||
runGuardsAndResolvers: 'always',
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
component: HomeComponent,
|
||||
},
|
||||
// #########
|
||||
// Listings Details
|
||||
{
|
||||
path: 'details-listing/:type/:id',
|
||||
component: DetailsListingComponent,
|
||||
path: 'details-business-listing/:id',
|
||||
component: DetailsBusinessListingComponent,
|
||||
},
|
||||
{
|
||||
path: 'details-listing/:type/:id',
|
||||
component: DetailsListingComponent,
|
||||
path: 'details-commercial-property-listing/:id',
|
||||
component: DetailsCommercialPropertyListingComponent,
|
||||
},
|
||||
{
|
||||
path: 'listing/:id',
|
||||
canActivate: [ListingCategoryGuard],
|
||||
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||
},
|
||||
// #########
|
||||
// User Details
|
||||
{
|
||||
path: 'details-user/:id',
|
||||
component: DetailsUserComponent,
|
||||
},
|
||||
// #########
|
||||
// User edit
|
||||
{
|
||||
path: 'account',
|
||||
component: AccountComponent,
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'account/:id',
|
||||
component: AccountComponent,
|
||||
canActivate: [authGuard],
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// Create, Update Listings
|
||||
{
|
||||
path: 'editBusinessListing/:id',
|
||||
component: EditBusinessListingComponent,
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'editListing/:id',
|
||||
component: EditListingComponent,
|
||||
canActivate: [authGuard],
|
||||
path: 'createBusinessListing',
|
||||
component: EditBusinessListingComponent,
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'createListing',
|
||||
component: EditListingComponent,
|
||||
canActivate: [authGuard],
|
||||
path: 'editCommercialPropertyListing/:id',
|
||||
component: EditCommercialPropertyListingComponent,
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
{
|
||||
path: 'createCommercialPropertyListing',
|
||||
component: EditCommercialPropertyListingComponent,
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// My Listings
|
||||
{
|
||||
path: 'myListings',
|
||||
component: MyListingComponent,
|
||||
canActivate: [authGuard],
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// My Favorites
|
||||
{
|
||||
path: 'myFavorites',
|
||||
component: FavoritesComponent,
|
||||
canActivate: [authGuard],
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// EMAil Us
|
||||
{
|
||||
path: 'emailUs',
|
||||
component: EmailUsComponent,
|
||||
canActivate: [authGuard],
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// Logout
|
||||
{
|
||||
path: 'logout',
|
||||
component: LogoutComponent,
|
||||
canActivate: [authGuard],
|
||||
canActivate: [AuthGuard],
|
||||
},
|
||||
// #########
|
||||
// Pricing
|
||||
{
|
||||
path: 'pricing',
|
||||
component: PricingComponent
|
||||
component: PricingComponent,
|
||||
},
|
||||
{ path: '**', redirectTo: 'home' },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { ControlValueAccessor } from '@angular/forms';
|
||||
import { initFlowbite } from 'flowbite';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { ValidationMessagesService } from '../validation-messages.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-base-input',
|
||||
template: ``,
|
||||
standalone: true,
|
||||
imports: [],
|
||||
})
|
||||
export abstract class BaseInputComponent implements ControlValueAccessor {
|
||||
@Input() value: any = '';
|
||||
validationMessage: string = '';
|
||||
onChange: any = () => {};
|
||||
onTouched: any = () => {};
|
||||
subscription: Subscription | null = null;
|
||||
@Input() label: string = '';
|
||||
// @Input() id: string = '';
|
||||
@Input() name: string = '';
|
||||
constructor(protected validationMessagesService: ValidationMessagesService) {}
|
||||
ngOnInit() {
|
||||
this.subscription = this.validationMessagesService.messages$.subscribe(() => {
|
||||
this.updateValidationMessage();
|
||||
});
|
||||
setTimeout(() => {
|
||||
initFlowbite();
|
||||
}, 10);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.subscription) {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
writeValue(value: any): void {
|
||||
if (value !== undefined) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
updateValidationMessage(): void {
|
||||
this.validationMessage = this.validationMessagesService.getMessage(this.name);
|
||||
}
|
||||
setDisabledState?(isDisabled: boolean): void {}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { ConfirmationService } from './confirmation.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-confirmation',
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, NgIf],
|
||||
template: `
|
||||
<div *ngIf="confirmationService.modalVisible$ | async" class="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">
|
||||
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
<button
|
||||
(click)="confirmationService.reject()"
|
||||
type="button"
|
||||
class="absolute top-3 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" 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 modal</span>
|
||||
</button>
|
||||
<div class="p-4 md:p-5 text-center">
|
||||
<svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 11V6m0 8h.01M19 10a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
||||
</svg>
|
||||
@let confirmation = (confirmationService.confirmation$ | async);
|
||||
<h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400">{{ confirmation.message }}</h3>
|
||||
@if(confirmation.buttons==='both'){
|
||||
<button
|
||||
(click)="confirmationService.accept()"
|
||||
type="button"
|
||||
class="text-white bg-red-600 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 dark:focus:ring-red-800 font-medium rounded-lg text-sm inline-flex items-center px-5 py-2.5 text-center mr-2"
|
||||
>
|
||||
Yes, I'm sure
|
||||
</button>
|
||||
<button
|
||||
(click)="confirmationService.reject()"
|
||||
type="button"
|
||||
class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||
>
|
||||
No, cancel
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class ConfirmationComponent {
|
||||
constructor(public confirmationService: ConfirmationService) {}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
export interface Confirmation {
|
||||
message: string;
|
||||
buttons?: 'both' | 'none';
|
||||
button_accept_label?: string;
|
||||
button_reject_label?: string;
|
||||
}
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConfirmationService {
|
||||
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
|
||||
private confirmationSubject = new BehaviorSubject<Confirmation>({ message: '' });
|
||||
private resolvePromise!: (value: boolean) => void;
|
||||
|
||||
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
|
||||
confirmation$: Observable<Confirmation> = this.confirmationSubject.asObservable();
|
||||
|
||||
showConfirmation(confirmation: Confirmation): Promise<boolean> {
|
||||
confirmation.buttons = confirmation.buttons ? confirmation.buttons : 'both';
|
||||
this.confirmationSubject.next(confirmation);
|
||||
this.modalVisibleSubject.next(true);
|
||||
return new Promise<boolean>(resolve => {
|
||||
this.resolvePromise = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
accept(): void {
|
||||
this.modalVisibleSubject.next(false);
|
||||
this.resolvePromise(true);
|
||||
}
|
||||
|
||||
reject(): void {
|
||||
this.modalVisibleSubject.next(false);
|
||||
this.resolvePromise(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<div #_container class="container">
|
||||
<!-- <div
|
||||
*ngFor="let item of items"
|
||||
cdkDrag
|
||||
(cdkDragEnded)="dragEnded($event)"
|
||||
(cdkDragStarted)="dragStarted()"
|
||||
(cdkDragMoved)="dragMoved($event)"
|
||||
class="item"
|
||||
[class.animation]="isAnimationActive"
|
||||
[class.large]="item === 3"
|
||||
>
|
||||
Drag Item {{ item }}
|
||||
</div> -->
|
||||
<div *ngFor="let item of items" cdkDrag (cdkDragEnded)="dragEnded($event)" (cdkDragStarted)="dragStarted()" (cdkDragMoved)="dragMoved($event)" [class.animation]="isAnimationActive" class="grid-item item">
|
||||
<div class="image-box hover:cursor-pointer">
|
||||
<img [src]="getImageUrl(item)" class="w-full h-full object-cover rounded-lg shadow-md" />
|
||||
<div class="absolute top-2 right-2 bg-white rounded-full p-1 shadow-md" (click)="imageToDelete.emit(item)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="w-4 h-4 text-gray-600">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
// max-width: 200px;
|
||||
max-height: 150px;
|
||||
// background-color: blanchedalmond;
|
||||
}
|
||||
|
||||
.animation.cdk-drag:not(.cdk-drag-dragging) {
|
||||
transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.large {
|
||||
width: 150px;
|
||||
}
|
||||
// --------------------
|
||||
.image-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
aspect-ratio: 16 / 9;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 8px;
|
||||
transition: box-shadow 200ms cubic-bezier(0, 0, 0.2, 1);
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.grid-item:active {
|
||||
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
import { CdkDrag, CdkDragEnd, CdkDragMove, DragDropModule, DragRef, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { _getShadowRoot } from '@angular/cdk/platform';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, ElementRef, Input, input, output, QueryList, ViewChild, ViewChildren } from '@angular/core';
|
||||
import { CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
@Component({
|
||||
selector: 'app-drag-drop-mixed',
|
||||
standalone: true,
|
||||
imports: [CommonModule, DragDropModule],
|
||||
templateUrl: './drag-drop-mixed.component.html',
|
||||
styleUrl: './drag-drop-mixed.component.scss',
|
||||
})
|
||||
export class DragDropMixedComponent {
|
||||
@ViewChild('_container') _container!: ElementRef<HTMLDivElement>;
|
||||
@ViewChildren(CdkDrag) _drags!: QueryList<CdkDrag>;
|
||||
@Input() ts: number;
|
||||
listing = input<CommercialPropertyListing>();
|
||||
imageOrderChanged = output<string[]>();
|
||||
imageToDelete = output<string>();
|
||||
env = environment;
|
||||
items: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
private _cachedItems: string[] = []; //[1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
private _itemPositions: CachedItemPosition<DragRef>[] = [];
|
||||
private _rootNode: DocumentOrShadowRoot | undefined;
|
||||
private _activeItems: DragRef[] = [];
|
||||
private _previousSwap = {
|
||||
drag: null as DragRef | null,
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
overlaps: false,
|
||||
};
|
||||
private _containerStyle: CSSStyleDeclaration | null = null;
|
||||
public isAnimationActive = false;
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
ngOnChanges() {
|
||||
this.items = this.listing()?.imageOrder;
|
||||
this._cachedItems = this.items.slice();
|
||||
}
|
||||
ngAfterViewInit() {
|
||||
// Führen Sie einen zusätzlichen Change Detection-Zyklus durch
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
getImageUrl(image: string): string {
|
||||
return `${this.env.imageBaseUrl}/pictures/property/${this.listing().imagePath}/${this.listing().serialId}/${image}?_ts=${this.ts}`;
|
||||
}
|
||||
|
||||
dragStarted() {
|
||||
this.start();
|
||||
}
|
||||
|
||||
dragMoved(event: CdkDragMove) {
|
||||
const item = event.source._dragRef;
|
||||
this.sort(item, event.pointerPosition.x, event.pointerPosition.y, event.delta);
|
||||
}
|
||||
|
||||
dragEnded(event: CdkDragEnd) {
|
||||
this.imageOrderChanged.emit(this._cachedItems);
|
||||
this.reset();
|
||||
}
|
||||
|
||||
start() {
|
||||
const dragRefs: DragRef[] = [];
|
||||
|
||||
this._drags.forEach(drag => {
|
||||
dragRefs.push(drag._dragRef);
|
||||
});
|
||||
|
||||
this._activeItems = dragRefs;
|
||||
this._cacheItemPosition();
|
||||
this.isAnimationActive = true;
|
||||
}
|
||||
|
||||
sort(item: DragRef, pointerX: number, pointerY: number, pointerDelta: { x: number; y: number }) {
|
||||
const siblings = this._itemPositions.slice();
|
||||
const newIndex = this._getItemIndexFromPointerPosition(item, pointerX, pointerY);
|
||||
|
||||
const previousSwap = this._previousSwap;
|
||||
|
||||
if (newIndex === -1 || this._activeItems[newIndex] === item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toSwapWith = this._activeItems[newIndex];
|
||||
|
||||
if (previousSwap.drag === toSwapWith && previousSwap.overlaps && previousSwap.deltaX === pointerDelta.x && previousSwap.deltaY === pointerDelta.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
const previousIndex = this.getItemIndex(item);
|
||||
const siblingAtNewPosition = siblings[newIndex];
|
||||
const previousPosition = siblings[previousIndex].clientRect;
|
||||
const newPosition = siblingAtNewPosition.clientRect;
|
||||
|
||||
const delta = this.getDelta(newPosition.top, previousPosition.top, pointerDelta);
|
||||
|
||||
if (delta === 0) return;
|
||||
if (delta === 1 && previousIndex > newIndex) return;
|
||||
if (delta === -1 && previousIndex < newIndex) return;
|
||||
|
||||
const startIndex = Math.min(previousIndex, newIndex);
|
||||
const endIndex = Math.max(previousIndex, newIndex);
|
||||
|
||||
let itemPositions = this._itemPositions.slice();
|
||||
|
||||
if (delta === 1) {
|
||||
for (let i = startIndex; i < endIndex; i++) {
|
||||
itemPositions = this._updateItemPosition(i, itemPositions, delta);
|
||||
|
||||
const newIndex = i + 1;
|
||||
moveItemInArray(itemPositions, i, newIndex);
|
||||
}
|
||||
} else if (delta === -1) {
|
||||
for (let i = endIndex; i > startIndex; i--) {
|
||||
itemPositions = this._updateItemPosition(i, itemPositions, delta);
|
||||
|
||||
const newIndex = i - 1;
|
||||
moveItemInArray(itemPositions, i, newIndex);
|
||||
}
|
||||
}
|
||||
|
||||
const threshold = getMutableClientRect(this._container.nativeElement).right;
|
||||
|
||||
let currentTop = itemPositions[0].clientRect.top;
|
||||
|
||||
for (let i = 0; i < itemPositions.length; i++) {
|
||||
const itemPosition = itemPositions[i];
|
||||
if (Math.round(itemPosition.clientRect.right) > Math.round(threshold)) {
|
||||
const nextPosition = itemPositions[i + 1];
|
||||
if (nextPosition) {
|
||||
currentTop = nextPosition.clientRect.top;
|
||||
}
|
||||
itemPositions = this._updateItemPositionToDown(itemPositions, i);
|
||||
} else if (itemPosition.clientRect.top !== currentTop) {
|
||||
currentTop = itemPosition.clientRect.top;
|
||||
itemPositions = this._updateItemPositionToUp(itemPositions, i);
|
||||
}
|
||||
}
|
||||
|
||||
const oldOrder = this._itemPositions.slice();
|
||||
this._itemPositions = itemPositions;
|
||||
moveItemInArray(this._activeItems, previousIndex, newIndex);
|
||||
moveItemInArray(this._cachedItems, previousIndex, newIndex);
|
||||
|
||||
itemPositions.forEach((sibling, index) => {
|
||||
if (oldOrder[index] === sibling) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDraggedItem = sibling.drag === item;
|
||||
if (isDraggedItem) return;
|
||||
const elementToOffset = sibling.drag.getRootElement();
|
||||
|
||||
elementToOffset.style.transform = `translate3d(${Math.round(sibling.transform.x)}px, ${Math.round(sibling.transform.y)}px, 0)`;
|
||||
});
|
||||
|
||||
previousSwap.deltaX = pointerDelta.x;
|
||||
previousSwap.deltaY = pointerDelta.y;
|
||||
previousSwap.drag = toSwapWith;
|
||||
previousSwap.overlaps = isInsideClientRect(newPosition, pointerX, pointerY);
|
||||
}
|
||||
|
||||
reset() {
|
||||
// ignore animation
|
||||
this.isAnimationActive = false;
|
||||
const previousSwap = this._previousSwap;
|
||||
this.items = this._cachedItems.slice();
|
||||
this._activeItems.forEach(item => {
|
||||
item.reset();
|
||||
});
|
||||
this._itemPositions = [];
|
||||
this._activeItems = [];
|
||||
previousSwap.drag = null;
|
||||
previousSwap.deltaX = previousSwap.deltaY = 0;
|
||||
previousSwap.overlaps = false;
|
||||
}
|
||||
|
||||
getItemIndex(item: DragRef): number {
|
||||
return this._activeItems.indexOf(item);
|
||||
}
|
||||
|
||||
getDelta(newTop: number, previousTop: number, pointerDelta: { x: number; y: number }) {
|
||||
if (newTop === previousTop) {
|
||||
return pointerDelta.x;
|
||||
}
|
||||
|
||||
return newTop > previousTop ? 1 : -1;
|
||||
}
|
||||
|
||||
private _getRootNode(): DocumentOrShadowRoot {
|
||||
if (!this._rootNode) {
|
||||
this._rootNode = _getShadowRoot(this._container.nativeElement) || document;
|
||||
}
|
||||
return this._rootNode;
|
||||
}
|
||||
|
||||
private _cacheItemPosition() {
|
||||
this._itemPositions = this._activeItems.map(drag => {
|
||||
const elementToMeasure = drag.getRootElement();
|
||||
return {
|
||||
drag,
|
||||
clientRect: getMutableClientRect(elementToMeasure),
|
||||
transform: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this._containerStyle = getComputedStyle(this._container.nativeElement);
|
||||
}
|
||||
|
||||
private _updateItemPosition(currentIndex: number, siblings: CachedItemPosition<DragRef>[], delta: number) {
|
||||
let siblingsUpdated = siblings.slice();
|
||||
const offsetVertical = this._getOffset(currentIndex, siblingsUpdated, delta, false);
|
||||
const offsetHorizontal = this._getOffset(currentIndex, siblingsUpdated, delta, true);
|
||||
|
||||
const immediateIndex = currentIndex + delta * 1;
|
||||
const currentItem = siblingsUpdated[currentIndex];
|
||||
const immediateSibling = siblingsUpdated[immediateIndex];
|
||||
|
||||
const currentItemUpdated: CachedItemPosition<DragRef> = {
|
||||
...currentItem,
|
||||
clientRect: {
|
||||
...currentItem.clientRect,
|
||||
x: currentItem.clientRect.x + offsetHorizontal.itemOffset,
|
||||
left: currentItem.clientRect.left + offsetHorizontal.itemOffset,
|
||||
right: currentItem.clientRect.right + offsetHorizontal.itemOffset,
|
||||
y: currentItem.clientRect.y + offsetVertical.itemOffset,
|
||||
top: currentItem.clientRect.top + offsetVertical.itemOffset,
|
||||
bottom: currentItem.clientRect.bottom + offsetVertical.itemOffset,
|
||||
},
|
||||
transform: {
|
||||
x: currentItem.transform.x + offsetHorizontal.itemOffset,
|
||||
y: currentItem.transform.y + offsetVertical.itemOffset,
|
||||
},
|
||||
};
|
||||
|
||||
const immediateSiblingUpdated: CachedItemPosition<DragRef> = {
|
||||
...immediateSibling,
|
||||
clientRect: {
|
||||
...immediateSibling.clientRect,
|
||||
x: immediateSibling.clientRect.x + offsetHorizontal.siblingOffset,
|
||||
left: immediateSibling.clientRect.left + offsetHorizontal.siblingOffset,
|
||||
right: immediateSibling.clientRect.right + offsetHorizontal.siblingOffset,
|
||||
y: immediateSibling.clientRect.y + offsetVertical.siblingOffset,
|
||||
top: immediateSibling.clientRect.top + offsetVertical.siblingOffset,
|
||||
bottom: immediateSibling.clientRect.bottom + offsetVertical.siblingOffset,
|
||||
},
|
||||
transform: {
|
||||
x: immediateSibling.transform.x + offsetHorizontal.siblingOffset,
|
||||
y: immediateSibling.transform.y + offsetVertical.siblingOffset,
|
||||
},
|
||||
};
|
||||
|
||||
if (offsetVertical.itemOffset !== offsetVertical.siblingOffset) {
|
||||
const offset = (currentItemUpdated.clientRect.right - immediateSibling.clientRect.right) * delta;
|
||||
const top = delta === 1 ? immediateSibling.clientRect.top : currentItem.clientRect.top;
|
||||
|
||||
const ignoreItem = delta === 1 ? immediateSibling.drag : currentItem.drag;
|
||||
|
||||
siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, top, offset, ignoreItem);
|
||||
}
|
||||
siblingsUpdated[currentIndex] = currentItemUpdated;
|
||||
siblingsUpdated[immediateIndex] = immediateSiblingUpdated;
|
||||
|
||||
return siblingsUpdated;
|
||||
}
|
||||
|
||||
private _updateItemPositionToUp(siblings: CachedItemPosition<DragRef>[], currentIndex: number) {
|
||||
let siblingsUpdated = siblings.slice();
|
||||
const immediateSibling = siblingsUpdated[currentIndex - 1];
|
||||
const currentItem = siblingsUpdated[currentIndex];
|
||||
|
||||
const nextEmptySlotLeft = immediateSibling.clientRect.right + this.getContainerGapPixel();
|
||||
|
||||
const threshold = getMutableClientRect(this._container.nativeElement).right;
|
||||
if (nextEmptySlotLeft + currentItem.clientRect.right - currentItem.clientRect.left <= threshold) {
|
||||
const offsetLeft = nextEmptySlotLeft - currentItem.clientRect.left;
|
||||
const offsetTop = immediateSibling.clientRect.top - currentItem.clientRect.top;
|
||||
|
||||
const nextSibling = siblingsUpdated[currentIndex + 1];
|
||||
if (nextSibling) {
|
||||
const offset = currentItem.clientRect.left - nextSibling.clientRect.left;
|
||||
siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, currentItem.clientRect.top, offset, currentItem.drag);
|
||||
}
|
||||
|
||||
siblingsUpdated[currentIndex] = {
|
||||
...currentItem,
|
||||
clientRect: {
|
||||
...currentItem.clientRect,
|
||||
x: nextEmptySlotLeft,
|
||||
left: nextEmptySlotLeft,
|
||||
right: currentItem.clientRect.right - currentItem.clientRect.left + nextEmptySlotLeft,
|
||||
y: immediateSibling.clientRect.y,
|
||||
top: immediateSibling.clientRect.top,
|
||||
bottom: currentItem.clientRect.bottom - currentItem.clientRect.top + immediateSibling.clientRect.top,
|
||||
},
|
||||
transform: {
|
||||
x: currentItem.transform.x + offsetLeft,
|
||||
y: currentItem.transform.y + offsetTop,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return siblingsUpdated;
|
||||
}
|
||||
|
||||
private _updateItemPositionToDown(siblings: CachedItemPosition<DragRef>[], currentIndex: number) {
|
||||
let siblingsUpdated = siblings.slice();
|
||||
const currentItem = siblingsUpdated[currentIndex];
|
||||
const immediateSibling = siblingsUpdated[currentIndex + 1];
|
||||
let offsetLeft = 0;
|
||||
let offsetTop = 0;
|
||||
|
||||
if (immediateSibling) {
|
||||
offsetLeft = immediateSibling.clientRect.left - currentItem.clientRect.left;
|
||||
offsetTop = immediateSibling.clientRect.top - currentItem.clientRect.top;
|
||||
} else {
|
||||
const firstSibling = siblings.find(item => item.clientRect.top === currentItem.clientRect.top);
|
||||
|
||||
if (firstSibling) {
|
||||
offsetLeft = firstSibling.clientRect.left - currentItem.clientRect.left;
|
||||
}
|
||||
|
||||
offsetTop = currentItem.clientRect.bottom - currentItem.clientRect.top + this.getContainerGapPixel();
|
||||
}
|
||||
|
||||
const currentItemUpdated: CachedItemPosition<DragRef> = {
|
||||
...currentItem,
|
||||
clientRect: {
|
||||
...currentItem.clientRect,
|
||||
x: currentItem.clientRect.x + offsetLeft,
|
||||
left: currentItem.clientRect.left + offsetLeft,
|
||||
right: currentItem.clientRect.right + offsetLeft,
|
||||
y: currentItem.clientRect.y + offsetTop,
|
||||
top: currentItem.clientRect.top + offsetTop,
|
||||
bottom: currentItem.clientRect.bottom + offsetTop,
|
||||
},
|
||||
transform: {
|
||||
x: currentItem.transform.x + offsetLeft,
|
||||
y: currentItem.transform.y + offsetTop,
|
||||
},
|
||||
};
|
||||
|
||||
if (immediateSibling) {
|
||||
const offset = currentItemUpdated.clientRect.right - immediateSibling.clientRect.left + this.getContainerGapPixel();
|
||||
|
||||
siblingsUpdated = this._updateItemPositionHorizontalOnRow(siblingsUpdated, immediateSibling.clientRect.top, offset);
|
||||
}
|
||||
|
||||
siblingsUpdated[currentIndex] = currentItemUpdated;
|
||||
return siblingsUpdated;
|
||||
}
|
||||
|
||||
private _updateItemPositionHorizontalOnRow(siblings: CachedItemPosition<DragRef>[], top: number, offset: number, ignoreItem?: DragRef) {
|
||||
const siblingsUpdated = siblings.slice();
|
||||
|
||||
siblingsUpdated
|
||||
.filter(item => (!ignoreItem || item.drag !== ignoreItem) && item.clientRect.top === top)
|
||||
.forEach(currentItem => {
|
||||
const index = siblingsUpdated.findIndex(item => item.drag === currentItem.drag);
|
||||
|
||||
siblingsUpdated[index] = {
|
||||
...siblingsUpdated[index],
|
||||
clientRect: {
|
||||
...siblingsUpdated[index].clientRect,
|
||||
x: siblingsUpdated[index].clientRect.x + offset,
|
||||
left: siblingsUpdated[index].clientRect.left + offset,
|
||||
right: siblingsUpdated[index].clientRect.right + offset,
|
||||
},
|
||||
transform: {
|
||||
...siblingsUpdated[index].transform,
|
||||
x: siblingsUpdated[index].transform.x + offset,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return siblingsUpdated;
|
||||
}
|
||||
|
||||
private _getItemIndexFromPointerPosition(item: DragRef, pointerX: number, pointerY: number) {
|
||||
const elementAtPoints = this._getRootNode().elementsFromPoint(Math.floor(pointerX), Math.floor(pointerY));
|
||||
|
||||
const elementAtPoint = elementAtPoints.find(element => {
|
||||
// ignore element is transiting
|
||||
const animations = element.getAnimations();
|
||||
const isTransitionRunning = animations.length > 0;
|
||||
|
||||
return !isTransitionRunning && this._itemPositions.some(item => item.drag.getRootElement() === element) && element !== item.getRootElement();
|
||||
});
|
||||
|
||||
const index = elementAtPoint
|
||||
? this._itemPositions.findIndex(({ drag }) => {
|
||||
// Skip the item itself.
|
||||
if (drag === item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const root = drag.getRootElement();
|
||||
return elementAtPoint === root || root.contains(elementAtPoint);
|
||||
})
|
||||
: -1;
|
||||
return index;
|
||||
}
|
||||
|
||||
private _getOffset(currentIndex: number, siblings: CachedItemPosition<DragRef>[], delta: number, isHorizontal: boolean) {
|
||||
const currentPosition = siblings[currentIndex].clientRect;
|
||||
const immediateSibling = siblings[currentIndex + delta].clientRect;
|
||||
|
||||
let itemOffset = 0;
|
||||
let siblingOffset = 0;
|
||||
|
||||
if (immediateSibling) {
|
||||
const start = isHorizontal ? 'left' : 'top';
|
||||
const end = isHorizontal ? 'right' : 'bottom';
|
||||
|
||||
if (delta === 1) {
|
||||
itemOffset = immediateSibling[end] - currentPosition[end];
|
||||
siblingOffset = currentPosition[start] - immediateSibling[start];
|
||||
|
||||
if (isHorizontal && immediateSibling[end] < currentPosition[end]) {
|
||||
itemOffset = immediateSibling[start] - currentPosition[start];
|
||||
}
|
||||
} else {
|
||||
itemOffset = immediateSibling[start] - currentPosition[start];
|
||||
siblingOffset = currentPosition[end] - immediateSibling[end];
|
||||
|
||||
if (isHorizontal && immediateSibling[end] > currentPosition[end]) {
|
||||
siblingOffset = currentPosition[start] - immediateSibling[start];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
itemOffset,
|
||||
siblingOffset,
|
||||
};
|
||||
}
|
||||
|
||||
private getContainerGapPixel() {
|
||||
if (this._containerStyle && (this._containerStyle.display === 'flex' || this._containerStyle.display === 'grid')) {
|
||||
return this._containerStyle.gap ? +this._containerStyle.gap.split('px')[0] : 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const getMutableClientRect = (element: Element): DOMRect => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
left: rect.left,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
x: rect.x,
|
||||
y: rect.y,
|
||||
} as DOMRect;
|
||||
};
|
||||
|
||||
const isInsideClientRect = (clientRect: DOMRect, x: number, y: number) => {
|
||||
const { top, bottom, left, right } = clientRect;
|
||||
return y >= top && y <= bottom && x >= left && x <= right;
|
||||
};
|
||||
|
||||
interface CachedItemPosition<T> {
|
||||
drag: T;
|
||||
clientRect: DOMRect;
|
||||
transform: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<p>dropdown works!</p>
|
||||
129
bizmatch/src/app/components/dropdown/dropdown.component.ts
Normal file
129
bizmatch/src/app/components/dropdown/dropdown.component.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { createPopper, Instance as PopperInstance } from '@popperjs/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dropdown',
|
||||
template: `
|
||||
<div #targetEl [class.hidden]="!isVisible" class="z-50">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
`,
|
||||
standalone: true,
|
||||
})
|
||||
export class DropdownComponent implements AfterViewInit, OnDestroy {
|
||||
@ViewChild('targetEl') targetEl!: ElementRef<HTMLElement>;
|
||||
@Input() triggerEl!: HTMLElement;
|
||||
|
||||
@Input() placement: any = 'bottom';
|
||||
@Input() triggerType: 'click' | 'hover' = 'click';
|
||||
@Input() offsetSkidding: number = 0;
|
||||
@Input() offsetDistance: number = 10;
|
||||
@Input() delay: number = 300;
|
||||
@Input() ignoreClickOutsideClass: string | false = false;
|
||||
|
||||
@HostBinding('class.hidden') isHidden: boolean = true;
|
||||
|
||||
private popperInstance: PopperInstance | null = null;
|
||||
isVisible: boolean = false;
|
||||
private clickOutsideListener: any;
|
||||
private hoverShowListener: any;
|
||||
private hoverHideListener: any;
|
||||
|
||||
ngAfterViewInit() {
|
||||
if (!this.triggerEl) {
|
||||
console.error('Trigger element is not provided to the dropdown component.');
|
||||
return;
|
||||
}
|
||||
this.initializePopper();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroyPopper();
|
||||
this.removeEventListeners();
|
||||
}
|
||||
|
||||
private initializePopper() {
|
||||
this.popperInstance = createPopper(this.triggerEl, this.targetEl.nativeElement, {
|
||||
placement: this.placement,
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.offsetSkidding, this.offsetDistance],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private setupEventListeners() {
|
||||
if (this.triggerType === 'click') {
|
||||
this.triggerEl.addEventListener('click', () => this.toggle());
|
||||
} else if (this.triggerType === 'hover') {
|
||||
this.hoverShowListener = () => this.show();
|
||||
this.hoverHideListener = () => this.hide();
|
||||
this.triggerEl.addEventListener('mouseenter', this.hoverShowListener);
|
||||
this.triggerEl.addEventListener('mouseleave', this.hoverHideListener);
|
||||
this.targetEl.nativeElement.addEventListener('mouseenter', this.hoverShowListener);
|
||||
this.targetEl.nativeElement.addEventListener('mouseleave', this.hoverHideListener);
|
||||
}
|
||||
|
||||
this.clickOutsideListener = (event: MouseEvent) => this.handleClickOutside(event);
|
||||
document.addEventListener('click', this.clickOutsideListener);
|
||||
}
|
||||
|
||||
private removeEventListeners() {
|
||||
if (this.triggerType === 'click') {
|
||||
this.triggerEl.removeEventListener('click', () => this.toggle());
|
||||
} else if (this.triggerType === 'hover') {
|
||||
this.triggerEl.removeEventListener('mouseenter', this.hoverShowListener);
|
||||
this.triggerEl.removeEventListener('mouseleave', this.hoverHideListener);
|
||||
this.targetEl.nativeElement.removeEventListener('mouseenter', this.hoverShowListener);
|
||||
this.targetEl.nativeElement.removeEventListener('mouseleave', this.hoverHideListener);
|
||||
}
|
||||
|
||||
document.removeEventListener('click', this.clickOutsideListener);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isVisible ? this.hide() : this.show();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.isVisible = true;
|
||||
this.isHidden = false;
|
||||
this.targetEl.nativeElement.classList.remove('hidden');
|
||||
this.popperInstance?.update();
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.isVisible = false;
|
||||
this.isHidden = true;
|
||||
this.targetEl.nativeElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
private handleClickOutside(event: MouseEvent) {
|
||||
if (!this.isVisible) return;
|
||||
|
||||
const clickedElement = event.target as HTMLElement;
|
||||
if (this.ignoreClickOutsideClass) {
|
||||
const ignoredElements = document.querySelectorAll(`.${this.ignoreClickOutsideClass}`);
|
||||
const arr = Array.from(ignoredElements);
|
||||
for (const el of arr) {
|
||||
if (el.contains(clickedElement)) return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.targetEl.nativeElement.contains(clickedElement) && !this.triggerEl.contains(clickedElement)) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private destroyPopper() {
|
||||
if (this.popperInstance) {
|
||||
this.popperInstance.destroy();
|
||||
this.popperInstance = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user