Compare commits
71 Commits
| 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 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -48,6 +48,9 @@ public
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.prod
|
||||||
|
.env.dev
|
||||||
|
.env.test
|
||||||
|
|
||||||
# temp directory
|
# temp directory
|
||||||
.temp
|
.temp
|
||||||
@@ -62,7 +65,6 @@ pids
|
|||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
*.js
|
|
||||||
*.map
|
*.map
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
|
|||||||
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
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
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
pictures
|
pictures
|
||||||
|
pictures_base
|
||||||
|
|
||||||
|
src/*.js
|
||||||
|
bun.lockb
|
||||||
|
|||||||
75
bizmatch-server/.vscode/launch.json
vendored
75
bizmatch-server/.vscode/launch.json
vendored
@@ -6,60 +6,59 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug Nest Framework",
|
"name": "Debug Nest Framework",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||||
"run",
|
|
||||||
"start:debug",
|
|
||||||
"--",
|
|
||||||
"--inspect-brk"
|
|
||||||
],
|
|
||||||
"autoAttachChildProcesses": true,
|
"autoAttachChildProcesses": true,
|
||||||
"restart": true,
|
"restart": true,
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"stopOnEntry": false,
|
"stopOnEntry": false,
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
"env": {
|
||||||
|
"HOST_NAME": "localhost"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug Current TS File",
|
"name": "Launch TypeScript file with tsx",
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
|
"runtimeExecutable": "npx",
|
||||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
"runtimeArgs": ["tsx", "--inspect"],
|
||||||
"outFiles": [
|
"args": ["${workspaceFolder}/src/drizzle/import.ts"],
|
||||||
"${workspaceFolder}/out/**/*.js"
|
"cwd": "${workspaceFolder}",
|
||||||
],
|
"outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"],
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"smartStep": true,
|
"resolveSourceMapLocations": ["${workspaceFolder}/src/**/*.ts", "!**/node_modules/**"],
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
"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",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "generateDefs",
|
"name": "generateDefs",
|
||||||
"skipFiles": [
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"<node_internals>/**"
|
|
||||||
],
|
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
|
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
|
||||||
"outFiles": [
|
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||||
"${workspaceFolder}/dist/src/drizzle/**/*.js"
|
|
||||||
],
|
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"smartStep": true,
|
"smartStep": true
|
||||||
|
},
|
||||||
},
|
{
|
||||||
{
|
"type": "node",
|
||||||
"type": "node",
|
"request": "launch",
|
||||||
"request": "launch",
|
"name": "generateTypes",
|
||||||
"name": "generateTypes",
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"skipFiles": [
|
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
|
||||||
"<node_internals>/**"
|
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||||
],
|
"sourceMaps": true,
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
|
"smartStep": true
|
||||||
"outFiles": [
|
}
|
||||||
"${workspaceFolder}/dist/src/drizzle/**/*.js"
|
|
||||||
],
|
|
||||||
"sourceMaps": true,
|
|
||||||
"smartStep": true,
|
|
||||||
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -698,7 +698,7 @@
|
|||||||
"realEstateIncluded": true,
|
"realEstateIncluded": true,
|
||||||
"franchiseResale": false,
|
"franchiseResale": false,
|
||||||
"draft": false,
|
"draft": false,
|
||||||
"internals": "",
|
"internals": null,
|
||||||
"created": "2023-11-18T13:00:00.000Z"
|
"created": "2023-11-18T13:00:00.000Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -11922,7 +11922,7 @@
|
|||||||
"description": "<h3>Thriving Caribbean Restaurant in the Heart of Washington, D.C.</h3><p>Well-established Caribbean restaurant with a loyal customer base. Known for its authentic cuisine and lively atmosphere.</p><p>Fully equipped kitchen and dining area. Experienced staff and management team in place. Strong financials and consistent growth. Perfect opportunity for someone in the food service industry or looking for a profitable investment.</p>",
|
"description": "<h3>Thriving Caribbean Restaurant in the Heart of Washington, D.C.</h3><p>Well-established Caribbean restaurant with a loyal customer base. Known for its authentic cuisine and lively atmosphere.</p><p>Fully equipped kitchen and dining area. Experienced staff and management team in place. Strong financials and consistent growth. Perfect opportunity for someone in the food service industry or looking for a profitable investment.</p>",
|
||||||
"type": 13,
|
"type": 13,
|
||||||
"state": "DC",
|
"state": "DC",
|
||||||
"city": "Washington",
|
"city": "Washington D.C.",
|
||||||
"id": "b148ad7f-7f4a-4319-bc8c-df23cf85be3b",
|
"id": "b148ad7f-7f4a-4319-bc8c-df23cf85be3b",
|
||||||
"price": 1500000,
|
"price": 1500000,
|
||||||
"salesRevenue": 2200000,
|
"salesRevenue": 2200000,
|
||||||
@@ -12322,7 +12322,7 @@
|
|||||||
"description": "<h3>Thriving Indian Restaurant in the Heart of Washington, D.C.</h3><p>Well-established Indian restaurant with a loyal customer base. Known for its authentic cuisine and inviting atmosphere.</p><p>Fully equipped kitchen and dining area. Experienced staff and management team in place. Strong financials and consistent growth. Perfect opportunity for someone in the food service industry or looking for a profitable investment.</p>",
|
"description": "<h3>Thriving Indian Restaurant in the Heart of Washington, D.C.</h3><p>Well-established Indian restaurant with a loyal customer base. Known for its authentic cuisine and inviting atmosphere.</p><p>Fully equipped kitchen and dining area. Experienced staff and management team in place. Strong financials and consistent growth. Perfect opportunity for someone in the food service industry or looking for a profitable investment.</p>",
|
||||||
"type": 13,
|
"type": 13,
|
||||||
"state": "DC",
|
"state": "DC",
|
||||||
"city": "Washington",
|
"city": "Washington D.C.",
|
||||||
"id": "d8d0f4e3-fdbf-4c4b-b2bd-d8847f0d248f",
|
"id": "d8d0f4e3-fdbf-4c4b-b2bd-d8847f0d248f",
|
||||||
"price": 1500000,
|
"price": 1500000,
|
||||||
"salesRevenue": 2200000,
|
"salesRevenue": 2200000,
|
||||||
|
|||||||
@@ -344,7 +344,7 @@
|
|||||||
"state": "HI",
|
"state": "HI",
|
||||||
"hasImages": true,
|
"hasImages": true,
|
||||||
"price": 50000000,
|
"price": 50000000,
|
||||||
"city": "Maui",
|
"city": "Hana",
|
||||||
"description": "<p>Luxurious beachfront resort and spa on the island of Maui. This iconic property offers a once-in-a-lifetime opportunity to acquire a premier hospitality asset in a world-renowned destination.</p><h3>Resort Features:</h3><p>- 200 elegantly appointed guest rooms and suites<br>- Full-service spa and fitness center<br>- Multiple dining options, including fine dining and casual fare<br>- Infinity pool and direct beach access<br>- Event space for weddings and conferences</p><p>With a prime location on one of Maui's most stunning beaches, this resort and spa offers unparalleled potential for investors seeking a trophy asset in the highly sought-after Hawaiian market.</p>",
|
"description": "<p>Luxurious beachfront resort and spa on the island of Maui. This iconic property offers a once-in-a-lifetime opportunity to acquire a premier hospitality asset in a world-renowned destination.</p><h3>Resort Features:</h3><p>- 200 elegantly appointed guest rooms and suites<br>- Full-service spa and fitness center<br>- Multiple dining options, including fine dining and casual fare<br>- Infinity pool and direct beach access<br>- Event space for weddings and conferences</p><p>With a prime location on one of Maui's most stunning beaches, this resort and spa offers unparalleled potential for investors seeking a trophy asset in the highly sought-after Hawaiian market.</p>",
|
||||||
"type": 105,
|
"type": 105,
|
||||||
"imageOrder": []
|
"imageOrder": []
|
||||||
@@ -932,7 +932,7 @@
|
|||||||
"state": "HI",
|
"state": "HI",
|
||||||
"hasImages": true,
|
"hasImages": true,
|
||||||
"price": 75000000,
|
"price": 75000000,
|
||||||
"city": "Maui",
|
"city": "Hana",
|
||||||
"description": "<h2>Oceanfront Luxury Resort and Spa</h2><p>Exquisite luxury resort and spa situated on the pristine shores of Maui. This world-class property features elegant accommodations, top-tier amenities, and unparalleled ocean views, attracting discerning travelers from around the globe.</p><p>Resort Features:</p><p>- 200 beautifully appointed guest rooms and suites<br>- Full-service spa, fitness center, and infinity pool<br>- Gourmet dining options showcasing local cuisine<br>- Extensive meeting and event spaces<br>- Prime beachfront location with direct access to water activities</p><p>With a strong brand reputation, consistent occupancy, and a truly idyllic setting, this luxury resort and spa presents an exceptional opportunity for investors seeking a trophy asset in one of the world's most desirable destinations.</p>",
|
"description": "<h2>Oceanfront Luxury Resort and Spa</h2><p>Exquisite luxury resort and spa situated on the pristine shores of Maui. This world-class property features elegant accommodations, top-tier amenities, and unparalleled ocean views, attracting discerning travelers from around the globe.</p><p>Resort Features:</p><p>- 200 beautifully appointed guest rooms and suites<br>- Full-service spa, fitness center, and infinity pool<br>- Gourmet dining options showcasing local cuisine<br>- Extensive meeting and event spaces<br>- Prime beachfront location with direct access to water activities</p><p>With a strong brand reputation, consistent occupancy, and a truly idyllic setting, this luxury resort and spa presents an exceptional opportunity for investors seeking a trophy asset in one of the world's most desirable destinations.</p>",
|
||||||
"type": 104,
|
"type": 104,
|
||||||
"imageOrder": []
|
"imageOrder": []
|
||||||
@@ -2278,7 +2278,7 @@
|
|||||||
"state": "NY",
|
"state": "NY",
|
||||||
"hasImages": true,
|
"hasImages": true,
|
||||||
"price": 500000000,
|
"price": 500000000,
|
||||||
"city": "New York",
|
"city": "New York City",
|
||||||
"description": "<p>Acquire this premier Class A office tower located in the heart of Midtown Manhattan, New York City. The property features state-of-the-art amenities, efficient floor plates, and unparalleled views of the city skyline, offering a prestigious address for top-tier tenants.</p><h3>Class A office tower features:</h3><p>- 1,000,000 square feet of Class A office space<br>- 95% occupancy rate with a diverse mix of creditworthy tenants<br>- LEED Platinum certified with advanced sustainability features<br>- Expansive lobby with 24/7 concierge and security services<br>- Multiple high-speed elevators and advanced building systems<br>- Prime Midtown location with access to transportation and amenities</p><p>Invest in this exceptional Class A office tower and benefit from the strong tenant demand and long-term value appreciation potential in the highly coveted Midtown Manhattan office market.</p>",
|
"description": "<p>Acquire this premier Class A office tower located in the heart of Midtown Manhattan, New York City. The property features state-of-the-art amenities, efficient floor plates, and unparalleled views of the city skyline, offering a prestigious address for top-tier tenants.</p><h3>Class A office tower features:</h3><p>- 1,000,000 square feet of Class A office space<br>- 95% occupancy rate with a diverse mix of creditworthy tenants<br>- LEED Platinum certified with advanced sustainability features<br>- Expansive lobby with 24/7 concierge and security services<br>- Multiple high-speed elevators and advanced building systems<br>- Prime Midtown location with access to transportation and amenities</p><p>Invest in this exceptional Class A office tower and benefit from the strong tenant demand and long-term value appreciation potential in the highly coveted Midtown Manhattan office market.</p>",
|
||||||
"type": 103,
|
"type": 103,
|
||||||
"imageOrder": []
|
"imageOrder": []
|
||||||
@@ -2434,7 +2434,7 @@
|
|||||||
"state": "DC",
|
"state": "DC",
|
||||||
"hasImages": true,
|
"hasImages": true,
|
||||||
"price": 225000000,
|
"price": 225000000,
|
||||||
"city": "Washington",
|
"city": "Washington D.C.",
|
||||||
"description": "<p>Acquire this stunning historic office building located in the heart of downtown Washington, D.C. The property features classic architecture, modern amenities, and a prime location just steps from the White House, offering a prestigious address for government relations and lobbying firms.</p><h3>Historic office building features:</h3><p>- 250,000 square feet of beautifully renovated office space<br>- Elegant lobbies and common areas with historic details<br>- State-of-the-art building systems and infrastructure<br>- Rooftop terrace with panoramic views of the Washington Monument<br>- On-site retail and dining amenities<br>- Unparalleled access to government agencies, embassies, and influential organizations</p><p>Invest in this exceptional historic office building and benefit from the strong and stable demand for prestigious office space in the heart of the nation's capital.</p>",
|
"description": "<p>Acquire this stunning historic office building located in the heart of downtown Washington, D.C. The property features classic architecture, modern amenities, and a prime location just steps from the White House, offering a prestigious address for government relations and lobbying firms.</p><h3>Historic office building features:</h3><p>- 250,000 square feet of beautifully renovated office space<br>- Elegant lobbies and common areas with historic details<br>- State-of-the-art building systems and infrastructure<br>- Rooftop terrace with panoramic views of the Washington Monument<br>- On-site retail and dining amenities<br>- Unparalleled access to government agencies, embassies, and influential organizations</p><p>Invest in this exceptional historic office building and benefit from the strong and stable demand for prestigious office space in the heart of the nation's capital.</p>",
|
||||||
"type": 103,
|
"type": 103,
|
||||||
"imageOrder": []
|
"imageOrder": []
|
||||||
@@ -2668,7 +2668,7 @@
|
|||||||
"state": "DC",
|
"state": "DC",
|
||||||
"hasImages": true,
|
"hasImages": true,
|
||||||
"price": 400000000,
|
"price": 400000000,
|
||||||
"city": "Washington",
|
"city": "Washington D.C.",
|
||||||
"description": "<p>Acquire this iconic trophy office building located in the heart of Washington, D.C.'s central business district. The property features a stunning architectural design, premium amenities, and unparalleled views of the nation's capital, offering a prestigious address for top-tier tenants.</p><h3>Trophy office building features:</h3><p>- 600,000 square feet of Class A+ office space<br>- LEED Platinum certification for sustainability and energy efficiency<br>- Grand lobby with 24/7 concierge and security services<br>- Rooftop terrace with panoramic views of the Washington Monument and Capitol Building<br>- Private club with fine dining, fitness center, and conference facilities<br>- Direct access to multiple Metro lines and nearby amenities</p><p>Invest in this exceptional trophy office building and benefit from the strong and stable demand for premier office space in the nation's capital, driven by the presence of government agencies, lobbyists, and prestigious private sector tenants.</p>",
|
"description": "<p>Acquire this iconic trophy office building located in the heart of Washington, D.C.'s central business district. The property features a stunning architectural design, premium amenities, and unparalleled views of the nation's capital, offering a prestigious address for top-tier tenants.</p><h3>Trophy office building features:</h3><p>- 600,000 square feet of Class A+ office space<br>- LEED Platinum certification for sustainability and energy efficiency<br>- Grand lobby with 24/7 concierge and security services<br>- Rooftop terrace with panoramic views of the Washington Monument and Capitol Building<br>- Private club with fine dining, fitness center, and conference facilities<br>- Direct access to multiple Metro lines and nearby amenities</p><p>Invest in this exceptional trophy office building and benefit from the strong and stable demand for premier office space in the nation's capital, driven by the presence of government agencies, lobbyists, and prestigious private sector tenants.</p>",
|
||||||
"type": 103,
|
"type": 103,
|
||||||
"imageOrder": []
|
"imageOrder": []
|
||||||
|
|||||||
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,
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { defineConfig } from 'drizzle-kit'
|
import { defineConfig } from 'drizzle-kit';
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "./src/drizzle/schema.ts",
|
schema: './src/drizzle/schema.ts',
|
||||||
out: "./src/drizzle/migrations",
|
out: './src/drizzle/migrations',
|
||||||
driver: 'pg',
|
dialect: 'postgresql',
|
||||||
dbCredentials: {
|
// driver: 'pg',
|
||||||
connectionString: process.env.DATABASE_URL,
|
dbCredentials: {
|
||||||
},
|
url: process.env.DATABASE_URL,
|
||||||
verbose: true,
|
},
|
||||||
strict: true,
|
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
@@ -9,17 +9,17 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "HOST_NAME=localhost nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
|
||||||
"start:debug": "nest start --debug --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",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:cov": "jest --coverage",
|
"test:cov": "jest --coverage",
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
"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:pg",
|
"generate": "drizzle-kit generate",
|
||||||
"drop": "drizzle-kit drop",
|
"drop": "drizzle-kit drop",
|
||||||
"migrate": "tsx src/drizzle/migrate.ts",
|
"migrate": "tsx src/drizzle/migrate.ts",
|
||||||
"import": "tsx src/drizzle/import.ts",
|
"import": "tsx src/drizzle/import.ts",
|
||||||
@@ -36,25 +36,29 @@
|
|||||||
"@nestjs/serve-static": "^4.0.1",
|
"@nestjs/serve-static": "^4.0.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-orm": "^0.30.8",
|
"drizzle-orm": "^0.32.0",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"groq-sdk": "^0.5.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"ky": "^1.2.0",
|
"jwks-rsa": "^3.1.0",
|
||||||
|
"ky": "^1.4.0",
|
||||||
"nest-winston": "^1.9.4",
|
"nest-winston": "^1.9.4",
|
||||||
"nodemailer": "^6.9.10",
|
"nodemailer": "^6.9.10",
|
||||||
"nodemailer-smtp-transport": "^2.7.4",
|
"nodemailer-smtp-transport": "^2.7.4",
|
||||||
|
"openai": "^4.52.6",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pg": "^8.11.5",
|
"pg": "^8.11.5",
|
||||||
"redis": "^4.6.13",
|
"pgvector": "^0.2.0",
|
||||||
"redis-om": "^0.4.3",
|
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
"tsx": "^4.7.2",
|
"tsx": "^4.16.2",
|
||||||
"urlcat": "^3.1.0",
|
"urlcat": "^3.1.0",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/parser": "^7.24.4",
|
"@babel/parser": "^7.24.4",
|
||||||
@@ -75,7 +79,8 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"drizzle-kit": "^0.20.16",
|
"drizzle-kit": "^0.23.0",
|
||||||
|
"esbuild-register": "^3.5.0",
|
||||||
"eslint": "^8.42.0",
|
"eslint": "^8.42.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
@@ -88,7 +93,7 @@
|
|||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"ts-loader": "^9.4.3",
|
"ts-loader": "^9.4.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.1.3"
|
"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 { AppService } from './app.service.js';
|
||||||
|
import { AuthService } from './auth/auth.service.js';
|
||||||
|
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(
|
||||||
|
private readonly appService: AppService,
|
||||||
|
private authService: AuthService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
getHello(): string {
|
getHello(@Request() req): string {
|
||||||
return this.appService.getHello();
|
return req.user;
|
||||||
|
//return 'dfgdf';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
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 { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import * as winston from 'winston';
|
import * as winston from 'winston';
|
||||||
|
import { AiModule } from './ai/ai.module.js';
|
||||||
import { AppController } from './app.controller.js';
|
import { AppController } from './app.controller.js';
|
||||||
import { AppService } from './app.service.js';
|
import { AppService } from './app.service.js';
|
||||||
import { AuthModule } from './auth/auth.module.js';
|
import { AuthModule } from './auth/auth.module.js';
|
||||||
@@ -14,12 +16,37 @@ import { ListingsModule } from './listings/listings.module.js';
|
|||||||
import { MailModule } from './mail/mail.module.js';
|
import { MailModule } from './mail/mail.module.js';
|
||||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
||||||
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
||||||
import { SubscriptionsController } from './subscriptions/subscriptions.controller.js';
|
|
||||||
import { UserModule } from './user/user.module.js';
|
import { UserModule } from './user/user.module.js';
|
||||||
|
// const __filename = fileURLToPath(import.meta.url);
|
||||||
|
// const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
function loadEnvFiles() {
|
||||||
const __dirname = path.dirname(__filename);
|
// 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
@@ -46,8 +73,10 @@ const __dirname = path.dirname(__filename);
|
|||||||
ListingsModule,
|
ListingsModule,
|
||||||
SelectOptionsModule,
|
SelectOptionsModule,
|
||||||
ImageModule,
|
ImageModule,
|
||||||
|
PassportModule,
|
||||||
|
AiModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController, SubscriptionsController],
|
controllers: [AppController],
|
||||||
providers: [AppService, FileService],
|
providers: [AppService, FileService],
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {
|
||||||
|
|||||||
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 { Module } from '@nestjs/common';
|
||||||
import { MailerModule } from '@nestjs-modules/mailer';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import path, { join } from 'path';
|
import { JwtStrategy } from '../jwt.strategy.js';
|
||||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { AuthService } from './auth.service.js';
|
|
||||||
import { AuthController } from './auth.controller.js';
|
import { AuthController } from './auth.controller.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
import { AuthService } from './auth.service.js';
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [PassportModule],
|
||||||
],
|
providers: [AuthService, JwtStrategy],
|
||||||
providers: [AuthService],
|
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports:[AuthService]
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
// import got from 'got';
|
|
||||||
import ky from 'ky';
|
import ky from 'ky';
|
||||||
import urlcat from 'urlcat';
|
import urlcat from 'urlcat';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
|
|
||||||
public async getAccessToken() {
|
public async getAccessToken() {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append('grant_type', 'password');
|
form.append('grant_type', 'password');
|
||||||
@@ -19,13 +17,15 @@ export class AuthService {
|
|||||||
params.append('password', process.env.password);
|
params.append('password', process.env.password);
|
||||||
const URL = `${process.env.host}${process.env.tokenURL}`;
|
const URL = `${process.env.host}${process.env.tokenURL}`;
|
||||||
|
|
||||||
const response = await ky.post(URL, {
|
const response = await ky
|
||||||
body: params.toString(),
|
.post(URL, {
|
||||||
headers: {
|
body: params.toString(),
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
headers: {
|
||||||
'Authorization':'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3'
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
Authorization: 'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3',
|
||||||
}).json();
|
},
|
||||||
|
})
|
||||||
|
.json();
|
||||||
return (<any>response).access_token;
|
return (<any>response).access_token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'HTTPError') {
|
if (error.name === 'HTTPError') {
|
||||||
@@ -37,71 +37,83 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUsers(){
|
public async getUsers() {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const URL = `${process.env.host}${process.env.usersURL}`;
|
const URL = `${process.env.host}${process.env.usersURL}`;
|
||||||
const response = await ky.get(URL, {
|
const response = await ky
|
||||||
headers: {
|
.get(URL, {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
headers: {
|
||||||
'Authorization':`Bearer ${token}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
Authorization: `Bearer ${token}`,
|
||||||
}).json();
|
},
|
||||||
return response
|
})
|
||||||
|
.json();
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
public async getUser(userid:string){
|
public async getUser(userid: string) {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const URL = urlcat(process.env.host,process.env.userURL,{userid})
|
const URL = urlcat(process.env.host, process.env.userURL, { userid });
|
||||||
const response = await ky.get(URL, {
|
const response = await ky
|
||||||
headers: {
|
.get(URL, {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
headers: {
|
||||||
'Authorization':`Bearer ${token}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
Authorization: `Bearer ${token}`,
|
||||||
}).json();
|
},
|
||||||
return response
|
})
|
||||||
|
.json();
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
public async getGroups(){
|
public async getGroups() {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const URL = `${process.env.host}${process.env.groupsURL}`;
|
const URL = `${process.env.host}${process.env.groupsURL}`;
|
||||||
const response = await ky.get(URL, {
|
const response = await ky
|
||||||
headers: {
|
.get(URL, {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
headers: {
|
||||||
'Authorization':`Bearer ${token}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
Authorization: `Bearer ${token}`,
|
||||||
}).json();
|
},
|
||||||
return response
|
})
|
||||||
|
.json();
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getGroupsForUser(userid:string){
|
public async getGroupsForUser(userid: string) {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const URL = urlcat(process.env.host,process.env.userGroupsURL,{userid})
|
const URL = urlcat(process.env.host, process.env.userGroupsURL, { userid });
|
||||||
const response = await ky.get(URL, {
|
const response = await ky
|
||||||
headers: {
|
.get(URL, {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
headers: {
|
||||||
'Authorization':`Bearer ${token}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
Authorization: `Bearer ${token}`,
|
||||||
}).json();
|
},
|
||||||
return response
|
})
|
||||||
|
.json();
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
public async getLastLogin(userid:string){
|
public async getLastLogin(userid: string) {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const URL = urlcat(process.env.host,process.env.lastLoginURL,{userid})
|
const URL = urlcat(process.env.host, process.env.lastLoginURL, { userid });
|
||||||
const response = await ky.get(URL, {
|
const response = await ky
|
||||||
headers: {
|
.get(URL, {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
headers: {
|
||||||
'Authorization':`Bearer ${token}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
Authorization: `Bearer ${token}`,
|
||||||
}).json();
|
},
|
||||||
return response
|
})
|
||||||
|
.json();
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
public async addUser2Group(userid:string,groupid:string){
|
public async addUser2Group(userid: string, groupid: string) {
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const URL = urlcat(process.env.host,process.env.addUser2GroupURL,{userid,groupid})
|
const URL = urlcat(process.env.host, process.env.addUser2GroupURL, { userid, groupid });
|
||||||
const response = await ky.put(URL, {
|
const response = await ky
|
||||||
headers: {
|
.put(URL, {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
headers: {
|
||||||
'Authorization':`Bearer ${token}`
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
Authorization: `Bearer ${token}`,
|
||||||
}).json();
|
},
|
||||||
return response
|
})
|
||||||
|
.json();
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,104 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import OpenAI from 'openai';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import pkg from 'pg';
|
import pkg from 'pg';
|
||||||
import { rimraf } from 'rimraf';
|
import { rimraf } from 'rimraf';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { BusinessListing, CommercialPropertyListing, User } from 'src/models/db.model.js';
|
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';
|
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 { 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 connectionString = process.env.DATABASE_URL;
|
||||||
// const pool = new Pool({connectionString})
|
// const pool = new Pool({connectionString})
|
||||||
const client = new Pool({ connectionString });
|
const client = new Pool({ connectionString });
|
||||||
const db = drizzle(client, { schema, logger: true });
|
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
|
//Delete Content
|
||||||
await db.delete(schema.commercials);
|
await db.delete(schema.commercials);
|
||||||
await db.delete(schema.businesses);
|
await db.delete(schema.businesses);
|
||||||
await db.delete(schema.users);
|
await db.delete(schema.users);
|
||||||
|
let filePath = `./src/assets/geo.json`;
|
||||||
|
const rawData = readFileSync(filePath, 'utf8');
|
||||||
|
const geos = JSON.parse(rawData) as Geo;
|
||||||
|
|
||||||
|
const sso = new SelectOptionsService();
|
||||||
//Broker
|
//Broker
|
||||||
let filePath = `./data/broker.json`;
|
filePath = `./data/broker.json`;
|
||||||
let data: string = readFileSync(filePath, 'utf8');
|
let data: string = readFileSync(filePath, 'utf8');
|
||||||
const userData: User[] = JSON.parse(data); // Erwartet ein Array von Objekten
|
const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten
|
||||||
const generatedUserData = [];
|
const generatedUserData = [];
|
||||||
console.log(userData.length);
|
console.log(usersData.length);
|
||||||
let i = 0,
|
let i = 0,
|
||||||
male = 0,
|
male = 0,
|
||||||
female = 0;
|
female = 0;
|
||||||
@@ -32,55 +106,173 @@ const targetPathProfile = `./pictures/profile`;
|
|||||||
deleteFilesOfDir(targetPathProfile);
|
deleteFilesOfDir(targetPathProfile);
|
||||||
const targetPathLogo = `./pictures/logo`;
|
const targetPathLogo = `./pictures/logo`;
|
||||||
deleteFilesOfDir(targetPathLogo);
|
deleteFilesOfDir(targetPathLogo);
|
||||||
for (const user of userData) {
|
const targetPathProperty = `./pictures/property`;
|
||||||
delete user.id;
|
deleteFilesOfDir(targetPathProperty);
|
||||||
user.licensedIn = user.licensedIn.map(l => `${l['name']}|${l['value']}`);
|
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.hasCompanyLogo = true;
|
||||||
user.hasProfile = true;
|
user.hasProfile = true;
|
||||||
const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender });
|
user.firstname = userData.firstname;
|
||||||
generatedUserData.push(u[0].insertedId);
|
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++;
|
i++;
|
||||||
|
logger.info(`user_${index} inserted`);
|
||||||
if (u[0].gender === 'male') {
|
if (u[0].gender === 'male') {
|
||||||
male++;
|
male++;
|
||||||
const data = readFileSync(`./pictures/profile_base/Mann_${male}.jpg`);
|
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
|
||||||
await storeProfilePicture(data, u[0].insertedId);
|
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||||
} else {
|
} else {
|
||||||
female++;
|
female++;
|
||||||
const data = readFileSync(`./pictures/profile_base/Frau_${male}.jpg`);
|
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
|
||||||
await storeProfilePicture(data, u[0].insertedId);
|
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||||
}
|
}
|
||||||
const data = readFileSync(`./pictures/logos_base/${i}.jpg`);
|
const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
|
||||||
await storeCompanyLogo(data, u[0].insertedId);
|
await storeCompanyLogo(data, emailToDirName(u[0].email));
|
||||||
}
|
}
|
||||||
//Business Listings
|
|
||||||
filePath = `./data/businesses.json`;
|
|
||||||
data = readFileSync(filePath, 'utf8');
|
|
||||||
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
|
|
||||||
|
|
||||||
for (const business of businessJsonData) {
|
|
||||||
delete business.id;
|
|
||||||
business.created = new Date(business.created);
|
|
||||||
business.userId = getRandomItem(generatedUserData);
|
|
||||||
await db.insert(schema.businesses).values(business);
|
|
||||||
}
|
|
||||||
//Corporate Listings
|
//Corporate Listings
|
||||||
filePath = `./data/commercials.json`;
|
filePath = `./data/commercials.json`;
|
||||||
data = readFileSync(filePath, 'utf8');
|
data = readFileSync(filePath, 'utf8');
|
||||||
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
|
const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten
|
||||||
for (const commercial of commercialJsonData) {
|
for (let index = 0; index < commercialJsonData.length; index++) {
|
||||||
const id = commercial.id;
|
const user = getRandomItem(generatedUserData);
|
||||||
|
const commercial = createDefaultCommercialPropertyListing();
|
||||||
|
const id = commercialJsonData[index].id;
|
||||||
delete commercial.id;
|
delete commercial.id;
|
||||||
|
|
||||||
|
commercial.email = user.email;
|
||||||
|
commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercialJsonData[index].type)).value;
|
||||||
|
commercial.title = commercialJsonData[index].title;
|
||||||
|
commercial.description = commercialJsonData[index].description;
|
||||||
|
try {
|
||||||
|
const cityGeo = geos.states.find(s => s.state_code === commercialJsonData[index].state).cities.find(c => c.name === commercialJsonData[index].city);
|
||||||
|
commercial.location = {};
|
||||||
|
commercial.location.latitude = cityGeo.latitude;
|
||||||
|
commercial.location.longitude = cityGeo.longitude;
|
||||||
|
commercial.location.city = commercialJsonData[index].city;
|
||||||
|
commercial.location.state = commercialJsonData[index].state;
|
||||||
|
// console.log(JSON.stringify(commercial.location));
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`----------------> ERROR ${commercialJsonData[index].state} - ${commercialJsonData[index].city}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
commercial.price = commercialJsonData[index].price;
|
||||||
|
commercial.listingsCategory = 'commercialProperty';
|
||||||
|
commercial.draft = false;
|
||||||
commercial.imageOrder = getFilenames(id);
|
commercial.imageOrder = getFilenames(id);
|
||||||
commercial.imagePath = id;
|
commercial.imagePath = emailToDirName(user.email);
|
||||||
commercial.created = getRandomDateWithinLastYear();
|
const insertionDate = getRandomDateWithinLastYear();
|
||||||
commercial.userId = getRandomItem(generatedUserData);
|
commercial.created = insertionDate;
|
||||||
await db.insert(schema.commercials).values(commercial);
|
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
|
//End
|
||||||
await client.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 {
|
function getRandomItem<T>(arr: T[]): T {
|
||||||
if (arr.length === 0) {
|
if (arr.length === 0) {
|
||||||
throw new Error('The array is empty.');
|
throw new Error('The array is empty.');
|
||||||
@@ -91,10 +283,10 @@ function getRandomItem<T>(arr: T[]): T {
|
|||||||
}
|
}
|
||||||
function getFilenames(id: string): string[] {
|
function getFilenames(id: string): string[] {
|
||||||
try {
|
try {
|
||||||
let filePath = `./pictures/property/${id}`;
|
let filePath = `./pictures_base/property/${id}`;
|
||||||
return readdirSync(filePath);
|
return readdirSync(filePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getRandomDateWithinLastYear(): Date {
|
function getRandomDateWithinLastYear(): Date {
|
||||||
@@ -117,14 +309,14 @@ async function storeProfilePicture(buffer: Buffer, userId: string) {
|
|||||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storeCompanyLogo(buffer: Buffer, userId: string) {
|
async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) {
|
||||||
let quality = 50;
|
let quality = 50;
|
||||||
const output = await sharp(buffer)
|
const output = await sharp(buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.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);
|
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import pkg from 'pg';
|
import pkg from 'pg';
|
||||||
const { Pool } = pkg;
|
|
||||||
import * as schema from './schema.js';
|
import * as schema from './schema.js';
|
||||||
import { migrate } from 'drizzle-orm/node-postgres/migrator';
|
const { Pool } = pkg;
|
||||||
const connectionString = process.env.DATABASE_URL
|
const connectionString = process.env.DATABASE_URL;
|
||||||
const pool = new Pool({connectionString})
|
const pool = new Pool({ connectionString });
|
||||||
const db = drizzle(pool, { schema });
|
const db = drizzle(pool, { schema });
|
||||||
// This will run migrations on the database, skipping the ones already applied
|
// This will run migrations on the database, skipping the ones already applied
|
||||||
await migrate(db, { migrationsFolder: './src/drizzle/migrations' });
|
//await migrate(db, { migrationsFolder: './src/drizzle/migrations' });
|
||||||
// Don't forget to close the connection, otherwise the script will hang
|
// Don't forget to close the connection, otherwise the script will hang
|
||||||
await pool.end();
|
//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 $$;
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS "businesses" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"userId" uuid,
|
|
||||||
"type" integer,
|
|
||||||
"title" varchar(255),
|
|
||||||
"description" text,
|
|
||||||
"city" varchar(255),
|
|
||||||
"state" char(2),
|
|
||||||
"price" double precision,
|
|
||||||
"favoritesForUser" varchar(30)[],
|
|
||||||
"draft" boolean,
|
|
||||||
"listingsCategory" varchar(255),
|
|
||||||
"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,
|
|
||||||
"created" timestamp,
|
|
||||||
"updated" timestamp,
|
|
||||||
"visits" integer,
|
|
||||||
"lastVisit" timestamp
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE IF NOT EXISTS "commercials" (
|
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
||||||
"userId" uuid,
|
|
||||||
"type" integer,
|
|
||||||
"title" varchar(255),
|
|
||||||
"description" text,
|
|
||||||
"city" varchar(255),
|
|
||||||
"state" char(2),
|
|
||||||
"price" double precision,
|
|
||||||
"favoritesForUser" varchar(30)[],
|
|
||||||
"hideImage" boolean,
|
|
||||||
"draft" boolean,
|
|
||||||
"zipCode" integer,
|
|
||||||
"county" varchar(255),
|
|
||||||
"email" varchar(255),
|
|
||||||
"website" varchar(255),
|
|
||||||
"phoneNumber" varchar(255),
|
|
||||||
"imageOrder" varchar(30)[],
|
|
||||||
"imagePath" varchar(50),
|
|
||||||
"created" timestamp,
|
|
||||||
"updated" timestamp,
|
|
||||||
"visits" integer,
|
|
||||||
"lastVisit" timestamp
|
|
||||||
);
|
|
||||||
--> 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),
|
|
||||||
"companyLocation" varchar(255),
|
|
||||||
"offeredServices" text,
|
|
||||||
"areasServed" varchar(100)[],
|
|
||||||
"hasProfile" boolean,
|
|
||||||
"hasCompanyLogo" boolean,
|
|
||||||
"licensedIn" varchar(50)[]
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
DO $$ BEGIN
|
|
||||||
ALTER TABLE "businesses" ADD CONSTRAINT "businesses_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "users"("id") 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_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "commercials" ALTER COLUMN "imageOrder" SET DATA TYPE varchar(200)[];
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
DO $$ BEGIN
|
|
||||||
CREATE TYPE "gender" AS ENUM('male', 'female');
|
|
||||||
EXCEPTION
|
|
||||||
WHEN duplicate_object THEN null;
|
|
||||||
END $$;
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "gender" "gender";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "commercials" ADD COLUMN "listingsCategory" varchar(255);
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"id": "f6d421f9-2394-4a1c-9268-9e46285f0a41",
|
"id": "a8283ca6-2c10-42bb-a640-ca984544ba30",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "5",
|
"version": "7",
|
||||||
"dialect": "pg",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
"businesses": {
|
"public.businesses": {
|
||||||
"name": "businesses",
|
"name": "businesses",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
@@ -15,15 +15,15 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "gen_random_uuid()"
|
"default": "gen_random_uuid()"
|
||||||
},
|
},
|
||||||
"userId": {
|
"email": {
|
||||||
"name": "userId",
|
"name": "email",
|
||||||
"type": "uuid",
|
"type": "varchar(255)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"name": "type",
|
"name": "type",
|
||||||
"type": "integer",
|
"type": "varchar(255)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -71,7 +71,8 @@
|
|||||||
},
|
},
|
||||||
"listingsCategory": {
|
"listingsCategory": {
|
||||||
"name": "listingsCategory",
|
"name": "listingsCategory",
|
||||||
"type": "varchar(255)",
|
"type": "listingsCategory",
|
||||||
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -147,6 +148,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"imageName": {
|
||||||
|
"name": "imageName",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"created": {
|
"created": {
|
||||||
"name": "created",
|
"name": "created",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
@@ -159,30 +166,30 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"visits": {
|
"latitude": {
|
||||||
"name": "visits",
|
"name": "latitude",
|
||||||
"type": "integer",
|
"type": "double precision",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"lastVisit": {
|
"longitude": {
|
||||||
"name": "lastVisit",
|
"name": "longitude",
|
||||||
"type": "timestamp",
|
"type": "double precision",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"businesses_userId_users_id_fk": {
|
"businesses_email_users_email_fk": {
|
||||||
"name": "businesses_userId_users_id_fk",
|
"name": "businesses_email_users_email_fk",
|
||||||
"tableFrom": "businesses",
|
"tableFrom": "businesses",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"userId"
|
"email"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"id"
|
"email"
|
||||||
],
|
],
|
||||||
"onDelete": "no action",
|
"onDelete": "no action",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -191,7 +198,7 @@
|
|||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {}
|
"uniqueConstraints": {}
|
||||||
},
|
},
|
||||||
"commercials": {
|
"public.commercials": {
|
||||||
"name": "commercials",
|
"name": "commercials",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
@@ -202,15 +209,21 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "gen_random_uuid()"
|
"default": "gen_random_uuid()"
|
||||||
},
|
},
|
||||||
"userId": {
|
"serialId": {
|
||||||
"name": "userId",
|
"name": "serialId",
|
||||||
"type": "uuid",
|
"type": "serial",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "varchar(255)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"name": "type",
|
"name": "type",
|
||||||
"type": "integer",
|
"type": "varchar(255)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -250,9 +263,10 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"hideImage": {
|
"listingsCategory": {
|
||||||
"name": "hideImage",
|
"name": "listingsCategory",
|
||||||
"type": "boolean",
|
"type": "listingsCategory",
|
||||||
|
"typeSchema": "public",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -262,45 +276,15 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"zipCode": {
|
|
||||||
"name": "zipCode",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"county": {
|
|
||||||
"name": "county",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"website": {
|
|
||||||
"name": "website",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"phoneNumber": {
|
|
||||||
"name": "phoneNumber",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"imageOrder": {
|
"imageOrder": {
|
||||||
"name": "imageOrder",
|
"name": "imageOrder",
|
||||||
"type": "varchar(30)[]",
|
"type": "varchar(200)[]",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"imagePath": {
|
"imagePath": {
|
||||||
"name": "imagePath",
|
"name": "imagePath",
|
||||||
"type": "varchar(50)",
|
"type": "varchar(200)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -316,30 +300,30 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"visits": {
|
"latitude": {
|
||||||
"name": "visits",
|
"name": "latitude",
|
||||||
"type": "integer",
|
"type": "double precision",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"lastVisit": {
|
"longitude": {
|
||||||
"name": "lastVisit",
|
"name": "longitude",
|
||||||
"type": "timestamp",
|
"type": "double precision",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {
|
"foreignKeys": {
|
||||||
"commercials_userId_users_id_fk": {
|
"commercials_email_users_email_fk": {
|
||||||
"name": "commercials_userId_users_id_fk",
|
"name": "commercials_email_users_email_fk",
|
||||||
"tableFrom": "commercials",
|
"tableFrom": "commercials",
|
||||||
"tableTo": "users",
|
"tableTo": "users",
|
||||||
"columnsFrom": [
|
"columnsFrom": [
|
||||||
"userId"
|
"email"
|
||||||
],
|
],
|
||||||
"columnsTo": [
|
"columnsTo": [
|
||||||
"id"
|
"email"
|
||||||
],
|
],
|
||||||
"onDelete": "no action",
|
"onDelete": "no action",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
@@ -348,7 +332,7 @@
|
|||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {}
|
"uniqueConstraints": {}
|
||||||
},
|
},
|
||||||
"users": {
|
"public.users": {
|
||||||
"name": "users",
|
"name": "users",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
"columns": {
|
"columns": {
|
||||||
@@ -407,12 +391,18 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
"companyLocation": {
|
"city": {
|
||||||
"name": "companyLocation",
|
"name": "city",
|
||||||
"type": "varchar(255)",
|
"type": "varchar(255)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "char(2)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"offeredServices": {
|
"offeredServices": {
|
||||||
"name": "offeredServices",
|
"name": "offeredServices",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -421,7 +411,7 @@
|
|||||||
},
|
},
|
||||||
"areasServed": {
|
"areasServed": {
|
||||||
"name": "areasServed",
|
"name": "areasServed",
|
||||||
"type": "varchar(100)[]",
|
"type": "jsonb",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -439,7 +429,52 @@
|
|||||||
},
|
},
|
||||||
"licensedIn": {
|
"licensedIn": {
|
||||||
"name": "licensedIn",
|
"name": "licensedIn",
|
||||||
"type": "varchar(50)[]",
|
"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,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
}
|
}
|
||||||
@@ -447,11 +482,57 @@
|
|||||||
"indexes": {},
|
"indexes": {},
|
||||||
"foreignKeys": {},
|
"foreignKeys": {},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {}
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"columns": {},
|
"columns": {},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
|||||||
@@ -1,460 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "3e4b8c5f-4474-4877-abec-38283408ee34",
|
|
||||||
"prevId": "f6d421f9-2394-4a1c-9268-9e46285f0a41",
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "pg",
|
|
||||||
"tables": {
|
|
||||||
"businesses": {
|
|
||||||
"name": "businesses",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "integer",
|
|
||||||
"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": "varchar(255)",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated": {
|
|
||||||
"name": "updated",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"visits": {
|
|
||||||
"name": "visits",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"lastVisit": {
|
|
||||||
"name": "lastVisit",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"businesses_userId_users_id_fk": {
|
|
||||||
"name": "businesses_userId_users_id_fk",
|
|
||||||
"tableFrom": "businesses",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"commercials": {
|
|
||||||
"name": "commercials",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "integer",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"hideImage": {
|
|
||||||
"name": "hideImage",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"draft": {
|
|
||||||
"name": "draft",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"zipCode": {
|
|
||||||
"name": "zipCode",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"county": {
|
|
||||||
"name": "county",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"website": {
|
|
||||||
"name": "website",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"phoneNumber": {
|
|
||||||
"name": "phoneNumber",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"imageOrder": {
|
|
||||||
"name": "imageOrder",
|
|
||||||
"type": "varchar(200)[]",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"imagePath": {
|
|
||||||
"name": "imagePath",
|
|
||||||
"type": "varchar(50)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated": {
|
|
||||||
"name": "updated",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"visits": {
|
|
||||||
"name": "visits",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"lastVisit": {
|
|
||||||
"name": "lastVisit",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"commercials_userId_users_id_fk": {
|
|
||||||
"name": "commercials_userId_users_id_fk",
|
|
||||||
"tableFrom": "commercials",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"firstname": {
|
|
||||||
"name": "firstname",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"lastname": {
|
|
||||||
"name": "lastname",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"phoneNumber": {
|
|
||||||
"name": "phoneNumber",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyName": {
|
|
||||||
"name": "companyName",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyOverview": {
|
|
||||||
"name": "companyOverview",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyWebsite": {
|
|
||||||
"name": "companyWebsite",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyLocation": {
|
|
||||||
"name": "companyLocation",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"offeredServices": {
|
|
||||||
"name": "offeredServices",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"areasServed": {
|
|
||||||
"name": "areasServed",
|
|
||||||
"type": "varchar(100)[]",
|
|
||||||
"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": "varchar(50)[]",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {},
|
|
||||||
"schemas": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,474 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "ad48c6eb-2d04-442f-9242-b6765553c7c4",
|
|
||||||
"prevId": "3e4b8c5f-4474-4877-abec-38283408ee34",
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "pg",
|
|
||||||
"tables": {
|
|
||||||
"businesses": {
|
|
||||||
"name": "businesses",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "integer",
|
|
||||||
"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": "varchar(255)",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated": {
|
|
||||||
"name": "updated",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"visits": {
|
|
||||||
"name": "visits",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"lastVisit": {
|
|
||||||
"name": "lastVisit",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"businesses_userId_users_id_fk": {
|
|
||||||
"name": "businesses_userId_users_id_fk",
|
|
||||||
"tableFrom": "businesses",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"commercials": {
|
|
||||||
"name": "commercials",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "integer",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"hideImage": {
|
|
||||||
"name": "hideImage",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"draft": {
|
|
||||||
"name": "draft",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"zipCode": {
|
|
||||||
"name": "zipCode",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"county": {
|
|
||||||
"name": "county",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"website": {
|
|
||||||
"name": "website",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"phoneNumber": {
|
|
||||||
"name": "phoneNumber",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"imageOrder": {
|
|
||||||
"name": "imageOrder",
|
|
||||||
"type": "varchar(200)[]",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"imagePath": {
|
|
||||||
"name": "imagePath",
|
|
||||||
"type": "varchar(50)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated": {
|
|
||||||
"name": "updated",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"visits": {
|
|
||||||
"name": "visits",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"lastVisit": {
|
|
||||||
"name": "lastVisit",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"commercials_userId_users_id_fk": {
|
|
||||||
"name": "commercials_userId_users_id_fk",
|
|
||||||
"tableFrom": "commercials",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"firstname": {
|
|
||||||
"name": "firstname",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"lastname": {
|
|
||||||
"name": "lastname",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"phoneNumber": {
|
|
||||||
"name": "phoneNumber",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyName": {
|
|
||||||
"name": "companyName",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyOverview": {
|
|
||||||
"name": "companyOverview",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyWebsite": {
|
|
||||||
"name": "companyWebsite",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyLocation": {
|
|
||||||
"name": "companyLocation",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"offeredServices": {
|
|
||||||
"name": "offeredServices",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"areasServed": {
|
|
||||||
"name": "areasServed",
|
|
||||||
"type": "varchar(100)[]",
|
|
||||||
"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": "varchar(50)[]",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"gender": {
|
|
||||||
"name": "gender",
|
|
||||||
"type": "gender",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {
|
|
||||||
"gender": {
|
|
||||||
"name": "gender",
|
|
||||||
"values": {
|
|
||||||
"male": "male",
|
|
||||||
"female": "female"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schemas": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,480 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "da786c6a-fd5f-4629-bd5e-3ecd42ab1f2c",
|
|
||||||
"prevId": "ad48c6eb-2d04-442f-9242-b6765553c7c4",
|
|
||||||
"version": "5",
|
|
||||||
"dialect": "pg",
|
|
||||||
"tables": {
|
|
||||||
"businesses": {
|
|
||||||
"name": "businesses",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "integer",
|
|
||||||
"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": "varchar(255)",
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated": {
|
|
||||||
"name": "updated",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"visits": {
|
|
||||||
"name": "visits",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"lastVisit": {
|
|
||||||
"name": "lastVisit",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"businesses_userId_users_id_fk": {
|
|
||||||
"name": "businesses_userId_users_id_fk",
|
|
||||||
"tableFrom": "businesses",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"commercials": {
|
|
||||||
"name": "commercials",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"name": "type",
|
|
||||||
"type": "integer",
|
|
||||||
"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": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"hideImage": {
|
|
||||||
"name": "hideImage",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"draft": {
|
|
||||||
"name": "draft",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"zipCode": {
|
|
||||||
"name": "zipCode",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"county": {
|
|
||||||
"name": "county",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"website": {
|
|
||||||
"name": "website",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"phoneNumber": {
|
|
||||||
"name": "phoneNumber",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"imageOrder": {
|
|
||||||
"name": "imageOrder",
|
|
||||||
"type": "varchar(200)[]",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"imagePath": {
|
|
||||||
"name": "imagePath",
|
|
||||||
"type": "varchar(50)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"name": "created",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"updated": {
|
|
||||||
"name": "updated",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"visits": {
|
|
||||||
"name": "visits",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"lastVisit": {
|
|
||||||
"name": "lastVisit",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"commercials_userId_users_id_fk": {
|
|
||||||
"name": "commercials_userId_users_id_fk",
|
|
||||||
"tableFrom": "commercials",
|
|
||||||
"tableTo": "users",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
},
|
|
||||||
"users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "uuid",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "gen_random_uuid()"
|
|
||||||
},
|
|
||||||
"firstname": {
|
|
||||||
"name": "firstname",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"lastname": {
|
|
||||||
"name": "lastname",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"name": "email",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"phoneNumber": {
|
|
||||||
"name": "phoneNumber",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"name": "description",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyName": {
|
|
||||||
"name": "companyName",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyOverview": {
|
|
||||||
"name": "companyOverview",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyWebsite": {
|
|
||||||
"name": "companyWebsite",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"companyLocation": {
|
|
||||||
"name": "companyLocation",
|
|
||||||
"type": "varchar(255)",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"offeredServices": {
|
|
||||||
"name": "offeredServices",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"areasServed": {
|
|
||||||
"name": "areasServed",
|
|
||||||
"type": "varchar(100)[]",
|
|
||||||
"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": "varchar(50)[]",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"gender": {
|
|
||||||
"name": "gender",
|
|
||||||
"type": "gender",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"enums": {
|
|
||||||
"gender": {
|
|
||||||
"name": "gender",
|
|
||||||
"values": {
|
|
||||||
"male": "male",
|
|
||||||
"female": "female"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"schemas": {},
|
|
||||||
"_meta": {
|
|
||||||
"columns": {},
|
|
||||||
"schemas": {},
|
|
||||||
"tables": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,12 @@
|
|||||||
{
|
{
|
||||||
"version": "5",
|
"version": "7",
|
||||||
"dialect": "pg",
|
"dialect": "postgresql",
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "5",
|
"version": "7",
|
||||||
"when": 1714913766996,
|
"when": 1723045357281,
|
||||||
"tag": "0000_third_spacker_dave",
|
"tag": "0000_lean_marvex",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1714981666488,
|
|
||||||
"tag": "0001_rapid_daimon_hellstrom",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 2,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1714982539265,
|
|
||||||
"tag": "0002_black_zaladane",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 3,
|
|
||||||
"version": "5",
|
|
||||||
"when": 1715254754561,
|
|
||||||
"tag": "0003_tough_hobgoblin",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,38 +1,52 @@
|
|||||||
import { boolean, char, doublePrecision, integer, pgEnum, pgTable, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
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 PG_CONNECTION = 'PG_CONNECTION';
|
||||||
export const genderEnum = pgEnum('gender', ['male', 'female']);
|
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', {
|
export const users = pgTable('users', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||||
firstname: varchar('firstname', { length: 255 }).notNull(),
|
firstname: varchar('firstname', { length: 255 }).notNull(),
|
||||||
lastname: varchar('lastname', { length: 255 }).notNull(),
|
lastname: varchar('lastname', { length: 255 }).notNull(),
|
||||||
email: varchar('email', { length: 255 }).notNull(),
|
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||||
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
companyName: varchar('companyName', { length: 255 }),
|
companyName: varchar('companyName', { length: 255 }),
|
||||||
companyOverview: text('companyOverview'),
|
companyOverview: text('companyOverview'),
|
||||||
companyWebsite: varchar('companyWebsite', { length: 255 }),
|
companyWebsite: varchar('companyWebsite', { length: 255 }),
|
||||||
companyLocation: varchar('companyLocation', { length: 255 }),
|
city: varchar('city', { length: 255 }),
|
||||||
|
state: char('state', { length: 2 }),
|
||||||
offeredServices: text('offeredServices'),
|
offeredServices: text('offeredServices'),
|
||||||
areasServed: varchar('areasServed', { length: 100 }).array(),
|
areasServed: jsonb('areasServed').$type<AreasServed[]>(),
|
||||||
hasProfile: boolean('hasProfile'),
|
hasProfile: boolean('hasProfile'),
|
||||||
hasCompanyLogo: boolean('hasCompanyLogo'),
|
hasCompanyLogo: boolean('hasCompanyLogo'),
|
||||||
licensedIn: varchar('licensedIn', { length: 50 }).array(),
|
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
||||||
gender: genderEnum('gender'),
|
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', {
|
export const businesses = pgTable('businesses', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||||
userId: uuid('userId').references(() => users.id),
|
email: varchar('email', { length: 255 }).references(() => users.email),
|
||||||
type: integer('type'),
|
type: varchar('type', { length: 255 }),
|
||||||
title: varchar('title', { length: 255 }),
|
title: varchar('title', { length: 255 }),
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
city: varchar('city', { length: 255 }),
|
city: varchar('city', { length: 255 }),
|
||||||
state: char('state', { length: 2 }),
|
state: char('state', { length: 2 }),
|
||||||
|
// zipCode: integer('zipCode'),
|
||||||
|
// county: varchar('county', { length: 255 }),
|
||||||
price: doublePrecision('price'),
|
price: doublePrecision('price'),
|
||||||
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
||||||
draft: boolean('draft'),
|
draft: boolean('draft'),
|
||||||
listingsCategory: varchar('listingsCategory', { length: 255 }),
|
listingsCategory: listingsCategoryEnum('listingsCategory'), //varchar('listingsCategory', { length: 255 }),
|
||||||
realEstateIncluded: boolean('realEstateIncluded'),
|
realEstateIncluded: boolean('realEstateIncluded'),
|
||||||
leasedLocation: boolean('leasedLocation'),
|
leasedLocation: boolean('leasedLocation'),
|
||||||
franchiseResale: boolean('franchiseResale'),
|
franchiseResale: boolean('franchiseResale'),
|
||||||
@@ -45,34 +59,34 @@ export const businesses = pgTable('businesses', {
|
|||||||
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
||||||
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
||||||
internals: text('internals'),
|
internals: text('internals'),
|
||||||
|
imageName: varchar('imageName', { length: 200 }),
|
||||||
created: timestamp('created'),
|
created: timestamp('created'),
|
||||||
updated: timestamp('updated'),
|
updated: timestamp('updated'),
|
||||||
visits: integer('visits'),
|
latitude: doublePrecision('latitude'),
|
||||||
lastVisit: timestamp('lastVisit'),
|
longitude: doublePrecision('longitude'),
|
||||||
|
// embedding: vector('embedding', { dimensions: 1536 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const commercials = pgTable('commercials', {
|
export const commercials = pgTable('commercials', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||||
userId: uuid('userId').references(() => users.id),
|
serialId: serial('serialId'),
|
||||||
type: integer('type'),
|
email: varchar('email', { length: 255 }).references(() => users.email),
|
||||||
|
type: varchar('type', { length: 255 }),
|
||||||
title: varchar('title', { length: 255 }),
|
title: varchar('title', { length: 255 }),
|
||||||
description: text('description'),
|
description: text('description'),
|
||||||
city: varchar('city', { length: 255 }),
|
city: varchar('city', { length: 255 }),
|
||||||
state: char('state', { length: 2 }),
|
state: char('state', { length: 2 }),
|
||||||
price: doublePrecision('price'),
|
price: doublePrecision('price'),
|
||||||
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
||||||
listingsCategory: varchar('listingsCategory', { length: 255 }),
|
listingsCategory: listingsCategoryEnum('listingsCategory'), //listingsCategory: varchar('listingsCategory', { length: 255 }),
|
||||||
hideImage: boolean('hideImage'),
|
|
||||||
draft: boolean('draft'),
|
draft: boolean('draft'),
|
||||||
zipCode: integer('zipCode'),
|
// zipCode: integer('zipCode'),
|
||||||
county: varchar('county', { length: 255 }),
|
// county: varchar('county', { length: 255 }),
|
||||||
email: varchar('email', { length: 255 }),
|
|
||||||
website: varchar('website', { length: 255 }),
|
|
||||||
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
|
||||||
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
||||||
imagePath: varchar('imagePath', { length: 50 }),
|
imagePath: varchar('imagePath', { length: 200 }),
|
||||||
created: timestamp('created'),
|
created: timestamp('created'),
|
||||||
updated: timestamp('updated'),
|
updated: timestamp('updated'),
|
||||||
visits: integer('visits'),
|
latitude: doublePrecision('latitude'),
|
||||||
lastVisit: timestamp('lastVisit'),
|
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 { Inject, Injectable } from '@nestjs/common';
|
||||||
import { fstat, readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import path from 'path';
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { ImageProperty } from '../models/main.model.js';
|
|
||||||
import sharp from 'sharp';
|
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
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 { Logger } from 'winston';
|
||||||
|
import { ImageProperty, Subscription } from '../models/main.model.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
@@ -21,73 +20,86 @@ export class FileService {
|
|||||||
fs.ensureDirSync(`./pictures/logo`);
|
fs.ensureDirSync(`./pictures/logo`);
|
||||||
fs.ensureDirSync(`./pictures/property`);
|
fs.ensureDirSync(`./pictures/property`);
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
|
// Subscriptions
|
||||||
|
// ############
|
||||||
private loadSubscriptions(): void {
|
private loadSubscriptions(): void {
|
||||||
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
||||||
const rawData = readFileSync(filePath, 'utf8');
|
const rawData = readFileSync(filePath, 'utf8');
|
||||||
this.subscriptions = JSON.parse(rawData);
|
this.subscriptions = JSON.parse(rawData);
|
||||||
}
|
}
|
||||||
getSubscriptions() {
|
getSubscriptions(): Subscription[] {
|
||||||
return this.subscriptions
|
return this.subscriptions;
|
||||||
}
|
}
|
||||||
async storeProfilePicture(file: Express.Multer.File, userId: string) {
|
// ############
|
||||||
|
// Profile
|
||||||
|
// ############
|
||||||
|
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
|
||||||
let quality = 50;
|
let quality = 50;
|
||||||
const output = await sharp(file.buffer)
|
const output = await sharp(file.buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
await sharp(output).toFile(`./pictures/profile/${adjustedEmail}.avif`);
|
||||||
}
|
}
|
||||||
hasProfile(userId: string){
|
hasProfile(adjustedEmail: string) {
|
||||||
return fs.existsSync(`./pictures/profile/${userId}.avif`)
|
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;
|
let quality = 50;
|
||||||
const output = await sharp(file.buffer)
|
const output = await sharp(file.buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.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);
|
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||||
}
|
}
|
||||||
hasCompanyLogo(userId: string){
|
hasCompanyLogo(adjustedEmail: string) {
|
||||||
return fs.existsSync(`./pictures/logo/${userId}.avif`)?true:false
|
return fs.existsSync(`./pictures/logo/${adjustedEmail}.avif`) ? true : false;
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
async getPropertyImages(listingId: string): Promise<string[]> {
|
// Property
|
||||||
const result: string[] = []
|
// ############
|
||||||
const directory = `./pictures/property/${listingId}`
|
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
|
||||||
|
const result: string[] = [];
|
||||||
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
files.forEach(f => {
|
files.forEach(f => {
|
||||||
result.push(f)
|
result.push(f);
|
||||||
})
|
});
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
return []
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async hasPropertyImages(listingId: string): Promise<boolean> {
|
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
|
||||||
const result: ImageProperty[] = []
|
const result: ImageProperty[] = [];
|
||||||
const directory = `./pictures/property/${listingId}`
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
return files.length>0
|
return files.length > 0;
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async storePropertyPicture(file: Express.Multer.File, listingId: string) : Promise<string> {
|
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
|
||||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg'
|
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
|
||||||
const directory = `./pictures/property/${listingId}`
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
fs.ensureDirSync(`${directory}`);
|
fs.ensureDirSync(`${directory}`);
|
||||||
const imageName = await this.getNextImageName(directory);
|
const imageName = await this.getNextImageName(directory);
|
||||||
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
||||||
await this.resizeImageToAVIF(file.buffer,150 * 1024,imageName,directory);
|
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
|
||||||
return `${imageName}.avif`
|
return `${imageName}.avif`;
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
|
// utils
|
||||||
|
// ############
|
||||||
async getNextImageName(directory) {
|
async getNextImageName(directory) {
|
||||||
try {
|
try {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
@@ -103,37 +115,35 @@ export class FileService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async resizeImageToAVIF(buffer: Buffer, maxSize: number,imageName:string,directory:string) {
|
async resizeImageToAVIF(buffer: Buffer, maxSize: number, imageName: string, directory: string) {
|
||||||
let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
|
let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
|
||||||
let output;
|
let output;
|
||||||
let start = Date.now();
|
let start = Date.now();
|
||||||
output = await sharp(buffer)
|
output = await sharp(buffer)
|
||||||
.resize({ width: 1500 })
|
.resize({ width: 1500 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
|
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
|
||||||
let timeTaken = Date.now() - start;
|
let timeTaken = Date.now() - start;
|
||||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`)
|
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
||||||
}
|
}
|
||||||
getProfileImagesForUsers(userids:string){
|
deleteImage(path: 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;
|
|
||||||
}
|
|
||||||
deleteImage(path:string){
|
|
||||||
fs.unlinkSync(path);
|
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,18 +1,27 @@
|
|||||||
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';
|
import { GeoService } from './geo.service.js';
|
||||||
|
|
||||||
@Controller('geo')
|
@Controller('geo')
|
||||||
export class GeoController {
|
export class GeoController {
|
||||||
constructor(private geoService:GeoService){}
|
constructor(private geoService: GeoService) {}
|
||||||
|
|
||||||
@Get(':prefix')
|
@Get(':prefix')
|
||||||
findByPrefix(@Param('prefix') prefix:string): any {
|
findByPrefix(@Param('prefix') prefix: string): any {
|
||||||
return this.geoService.findCitiesStartingWith(prefix);
|
return this.geoService.findCitiesStartingWith(prefix);
|
||||||
}
|
}
|
||||||
|
@Get('citiesandstates/:prefix')
|
||||||
|
findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any {
|
||||||
|
return this.geoService.findCitiesAndStatesStartingWith(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':prefix/:state')
|
@Get(':prefix/:state')
|
||||||
findByPrefixAndState(@Param('prefix') prefix:string,@Param('state') state:string): any {
|
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
|
||||||
return this.geoService.findCitiesStartingWith(prefix,state);
|
return this.geoService.findCitiesStartingWith(prefix, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('counties')
|
||||||
|
findByPrefixAndStates(@Body() countyRequest: CountyRequest): any {
|
||||||
|
return this.geoService.findCountiesStartingWith(countyRequest.prefix, countyRequest.states);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,106 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import path, { join } from 'path';
|
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 { fileURLToPath } from 'url';
|
||||||
|
import { City, CountyData, Geo, State } from '../models/server.model.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GeoService {
|
export class GeoService {
|
||||||
geo:Geo;
|
geo: Geo;
|
||||||
constructor() {
|
counties: CountyData[];
|
||||||
this.loadGeo();
|
constructor() {
|
||||||
}
|
this.loadGeo();
|
||||||
private loadGeo(): void {
|
}
|
||||||
const filePath = join(__dirname,'../..', 'assets', 'geo.json');
|
private loadGeo(): void {
|
||||||
const rawData = readFileSync(filePath, 'utf8');
|
const filePath = join(__dirname, '../..', 'assets', 'geo.json');
|
||||||
this.geo = JSON.parse(rawData);
|
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 }[] {
|
this.counties.forEach(stateData => {
|
||||||
const result: { city: string; state: string; state_code: string }[] = [];
|
if (!states || states.includes(stateData.state)) {
|
||||||
|
stateData.counties.forEach(county => {
|
||||||
this.geo.states.forEach((state: State) => {
|
if (county.startsWith(prefix.toUpperCase())) {
|
||||||
state.cities.forEach((city: City) => {
|
results.push({
|
||||||
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
|
id: idCounter++,
|
||||||
result.push({
|
name: county,
|
||||||
city: city.name,
|
state: stateData.state_full,
|
||||||
state: state.name,
|
state_code: stateData.state,
|
||||||
state_code: state.state_code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return state ? result.filter(e=>e.state_code.toLowerCase()===state.toLowerCase()) :result;
|
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.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 }> = [];
|
||||||
|
|
||||||
|
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 { 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 { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { ListingsService } from '../listings/listings.service.js';
|
import { CommercialPropertyService } from '../listings/commercial-property.service.js';
|
||||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||||
|
|
||||||
import { commercials } from 'src/drizzle/schema.js';
|
|
||||||
import { CommercialPropertyListing } from 'src/models/db.model.js';
|
|
||||||
|
|
||||||
@Controller('image')
|
@Controller('image')
|
||||||
export class ImageController {
|
export class ImageController {
|
||||||
constructor(
|
constructor(
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
private listingService: ListingsService,
|
private listingService: CommercialPropertyService,
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
private selectOptions: SelectOptionsService,
|
private selectOptions: SelectOptionsService,
|
||||||
) {}
|
) {}
|
||||||
|
// ############
|
||||||
@Post('uploadPropertyPicture/:id')
|
// Property
|
||||||
|
// ############
|
||||||
|
@Post('uploadPropertyPicture/:imagePath/:serial')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
|
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
|
||||||
const imagename = await this.fileService.storePropertyPicture(file, id);
|
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
|
||||||
await this.listingService.addImage(id, imagename);
|
await this.listingService.addImage(imagePath, serial, imagename);
|
||||||
}
|
}
|
||||||
|
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
||||||
@Post('uploadProfile/:id')
|
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);
|
||||||
|
}
|
||||||
|
// ############
|
||||||
|
// Profile
|
||||||
|
// ############
|
||||||
|
@Post('uploadProfile/:email')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
|
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||||
await this.fileService.storeProfilePicture(file, id);
|
await this.fileService.storeProfilePicture(file, adjustedEmail);
|
||||||
}
|
}
|
||||||
|
@Delete('profile/:email/')
|
||||||
@Post('uploadCompanyLogo/:id')
|
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||||
|
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
||||||
|
}
|
||||||
|
// ############
|
||||||
|
// Logo
|
||||||
|
// ############
|
||||||
|
@Post('uploadCompanyLogo/:email')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
|
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||||
await this.fileService.storeCompanyLogo(file, id);
|
await this.fileService.storeCompanyLogo(file, adjustedEmail);
|
||||||
}
|
}
|
||||||
|
@Delete('logo/:email/')
|
||||||
@Get(':id')
|
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||||
async getPropertyImagesById(@Param('id') id: string): Promise<any> {
|
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
||||||
const result = await this.listingService.findById(id, commercials);
|
|
||||||
const listing = result as CommercialPropertyListing;
|
|
||||||
if (listing.imageOrder) {
|
|
||||||
return listing.imageOrder;
|
|
||||||
} else {
|
|
||||||
const imageOrder = await this.fileService.getPropertyImages(id);
|
|
||||||
listing.imageOrder = imageOrder;
|
|
||||||
this.listingService.updateListing(listing.id, listing, commercials);
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
@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}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
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 { ImageController } from './image.controller.js';
|
||||||
import { ImageService } from './image.service.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({
|
@Module({
|
||||||
imports: [ListingsModule],
|
imports: [ListingsModule],
|
||||||
controllers: [ImageController],
|
controllers: [ImageController],
|
||||||
providers: [ImageService,FileService,SelectOptionsService]
|
providers: [ImageService, FileService, SelectOptionsService],
|
||||||
})
|
})
|
||||||
export class ImageModule {}
|
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 { Body, Controller, Inject, Post } from '@nestjs/common';
|
||||||
import { FileService } from '../file/file.service.js';
|
|
||||||
import { convertStringToNullUndefined } from '../utils.js';
|
|
||||||
import { ListingsService } from './listings.service.js';
|
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
|
import { UserListingCriteria } from 'src/models/main.model.js';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { UserService } from '../user/user.service.js';
|
import { UserService } from '../user/user.service.js';
|
||||||
|
|
||||||
@Controller('listings/professionals_brokers')
|
@Controller('listings/professionals_brokers')
|
||||||
export class BrokerListingsController {
|
export class BrokerListingsController {
|
||||||
|
constructor(
|
||||||
constructor(private readonly userService:UserService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
private readonly userService: UserService,
|
||||||
}
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post('search')
|
@Post('search')
|
||||||
find(@Body() criteria: any): any {
|
find(@Body() criteria: UserListingCriteria): any {
|
||||||
return this.userService.findUser(criteria);
|
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,47 +1,61 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
|
import { BusinessListing } from 'src/models/db.model.js';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { businesses } from '../drizzle/schema.js';
|
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
|
||||||
import { ListingsService } from './listings.service.js';
|
import { BusinessListingService } from './business-listing.service.js';
|
||||||
|
|
||||||
@Controller('listings/business')
|
@Controller('listings/business')
|
||||||
export class BusinessListingsController {
|
export class BusinessListingsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly listingsService: ListingsService,
|
private readonly listingsService: BusinessListingService,
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findById(@Param('id') id: string): any {
|
findById(@Request() req, @Param('id') id: string): any {
|
||||||
return this.listingsService.findById(id, businesses);
|
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||||
}
|
|
||||||
@Get('user/:userid')
|
|
||||||
findByUserId(@Param('userid') userid: string): any {
|
|
||||||
return this.listingsService.findByUserId(userid, businesses);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('search')
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
find(@Body() criteria: ListingCriteria): any {
|
@Get('user/:userid')
|
||||||
return this.listingsService.findListingsByCriteria(criteria, businesses);
|
findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||||
|
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
|
@Post('find')
|
||||||
|
find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||||
|
return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||||
|
}
|
||||||
|
@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);
|
||||||
|
// }
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() listing: any) {
|
create(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
return this.listingsService.createListing(listing, businesses);
|
return this.listingsService.createListing(listing);
|
||||||
}
|
}
|
||||||
@Put()
|
@Put()
|
||||||
update(@Body() listing: any) {
|
update(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
return this.listingsService.updateListing(listing.id, listing, businesses);
|
return this.listingsService.updateBusinessListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
deleteById(@Param('id') id: string) {
|
deleteById(@Param('id') id: string) {
|
||||||
this.listingsService.deleteListing(id, businesses);
|
this.listingsService.deleteListing(id);
|
||||||
}
|
}
|
||||||
@Get('states/all')
|
@Get('states/all')
|
||||||
getStates(): any {
|
getStates(): any {
|
||||||
return this.listingsService.getStates(businesses);
|
return this.listingsService.getStates();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,57 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { commercials } from '../drizzle/schema.js';
|
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||||
import { ListingsService } from './listings.service.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')
|
@Controller('listings/commercialProperty')
|
||||||
export class CommercialPropertyListingsController {
|
export class CommercialPropertyListingsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly listingsService: ListingsService,
|
private readonly listingsService: CommercialPropertyService,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findById(@Param('id') id: string): any {
|
findById(@Request() req, @Param('id') id: string): any {
|
||||||
return this.listingsService.findById(id, commercials);
|
return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
@Get('user/:userid')
|
|
||||||
findByUserId(@Param('userid') userid: string): any {
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
return this.listingsService.findByUserId(userid, commercials);
|
@Get('user/:email')
|
||||||
|
findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||||
|
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
@Post('search')
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
async find(@Body() criteria: ListingCriteria): Promise<any> {
|
@Post('find')
|
||||||
return await this.listingsService.findListingsByCriteria(criteria, commercials);
|
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')
|
@Get('states/all')
|
||||||
getStates(): any {
|
getStates(): any {
|
||||||
return this.listingsService.getStates(commercials);
|
return this.listingsService.getStates();
|
||||||
}
|
}
|
||||||
@Post()
|
@Post()
|
||||||
async create(@Body() listing: any) {
|
async create(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
return await this.listingsService.createListing(listing, commercials);
|
return await this.listingsService.createListing(listing);
|
||||||
}
|
}
|
||||||
@Put()
|
@Put()
|
||||||
async update(@Body() listing: any) {
|
async update(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
return await this.listingsService.updateListing(listing.id, listing, commercials);
|
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
@Delete(':id')
|
@Delete(':id/:imagePath')
|
||||||
deleteById(@Param('id') id: string) {
|
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||||
this.listingsService.deleteListing(id, commercials);
|
this.listingsService.deleteListing(id);
|
||||||
}
|
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||||
|
|
||||||
@Put('imageOrder/:id')
|
|
||||||
async changeImageOrder(@Param('id') id: string, @Body() imageOrder: string[]) {
|
|
||||||
this.listingsService.updateImageOrder(id, imageOrder);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,17 +1,22 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthModule } from '../auth/auth.module.js';
|
||||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { UserService } from '../user/user.service.js';
|
import { UserService } from '../user/user.service.js';
|
||||||
import { BrokerListingsController } from './broker-listings.controller.js';
|
import { BrokerListingsController } from './broker-listings.controller.js';
|
||||||
import { BusinessListingsController } from './business-listings.controller.js';
|
import { BusinessListingsController } from './business-listings.controller.js';
|
||||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
|
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
|
||||||
import { ListingsService } from './listings.service.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';
|
import { UnknownListingsController } from './unknown-listings.controller.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DrizzleModule],
|
imports: [DrizzleModule, AuthModule, GeoModule],
|
||||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||||
providers: [ListingsService, FileService, UserService],
|
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
||||||
exports: [ListingsService],
|
exports: [BusinessListingService, CommercialPropertyService],
|
||||||
})
|
})
|
||||||
export class ListingsModule {}
|
export class ListingsModule {}
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { and, eq, gte, ilike, lte, sql } from 'drizzle-orm';
|
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
|
||||||
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
|
|
||||||
import { Logger } from 'winston';
|
|
||||||
import * as schema from '../drizzle/schema.js';
|
|
||||||
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
|
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ListingsService {
|
|
||||||
constructor(
|
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
|
||||||
) {}
|
|
||||||
private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials): any[] {
|
|
||||||
const conditions = [];
|
|
||||||
if (criteria.type) {
|
|
||||||
conditions.push(eq(table.type, criteria.type));
|
|
||||||
}
|
|
||||||
if (criteria.state) {
|
|
||||||
conditions.push(eq(table.state, criteria.state));
|
|
||||||
}
|
|
||||||
if (criteria.minPrice) {
|
|
||||||
conditions.push(gte(table.price, criteria.minPrice));
|
|
||||||
}
|
|
||||||
if (criteria.maxPrice) {
|
|
||||||
conditions.push(lte(table.price, criteria.maxPrice));
|
|
||||||
}
|
|
||||||
if (criteria.realEstateChecked) {
|
|
||||||
conditions.push(eq(businesses.realEstateIncluded, true));
|
|
||||||
}
|
|
||||||
if (criteria.title) {
|
|
||||||
conditions.push(ilike(table.title, `%${criteria.title}%`));
|
|
||||||
}
|
|
||||||
return conditions;
|
|
||||||
}
|
|
||||||
// ##############################################################
|
|
||||||
// Listings general
|
|
||||||
// ##############################################################
|
|
||||||
async findListingsByCriteria(criteria: ListingCriteria, table: typeof businesses | typeof commercials): Promise<{ data: Record<string, any>[]; total: number }> {
|
|
||||||
const start = criteria.start ? criteria.start : 0;
|
|
||||||
const length = criteria.length ? criteria.length : 12;
|
|
||||||
return await this.findListings(table, criteria, start, length);
|
|
||||||
}
|
|
||||||
private async findListings(table: typeof businesses | typeof commercials, criteria: ListingCriteria, start = 0, length = 12): Promise<any> {
|
|
||||||
const conditions = this.getConditions(criteria, table);
|
|
||||||
const [data, total] = await Promise.all([
|
|
||||||
this.conn
|
|
||||||
.select()
|
|
||||||
.from(table)
|
|
||||||
.where(and(...conditions))
|
|
||||||
.offset(start)
|
|
||||||
.limit(length),
|
|
||||||
this.conn
|
|
||||||
.select({ count: sql`count(*)` })
|
|
||||||
.from(table)
|
|
||||||
.where(and(...conditions))
|
|
||||||
.then(result => Number(result[0].count)),
|
|
||||||
]);
|
|
||||||
return { total, data };
|
|
||||||
}
|
|
||||||
async findById(id: string, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
|
||||||
const result = await this.conn
|
|
||||||
.select()
|
|
||||||
.from(table)
|
|
||||||
.where(sql`${table.id} = ${id}`);
|
|
||||||
return result[0] as BusinessListing | CommercialPropertyListing;
|
|
||||||
}
|
|
||||||
|
|
||||||
async findByUserId(userId: string, table: typeof businesses | typeof commercials): Promise<BusinessListing[] | CommercialPropertyListing[]> {
|
|
||||||
return (await this.conn.select().from(table).where(eq(table.userId, userId))) as BusinessListing[] | CommercialPropertyListing[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
|
||||||
data.created = new Date();
|
|
||||||
data.updated = new Date();
|
|
||||||
data.visits = 0;
|
|
||||||
data.lastVisit = null;
|
|
||||||
const [createdListing] = await this.conn.insert(table).values(data).returning();
|
|
||||||
return createdListing as BusinessListing | CommercialPropertyListing;
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateListing(id: string, data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
|
||||||
data.updated = new Date();
|
|
||||||
data.created = new Date(data.created);
|
|
||||||
const [updateListing] = await this.conn.update(table).set(data).where(eq(table.id, id)).returning();
|
|
||||||
return updateListing as BusinessListing | CommercialPropertyListing;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
|
|
||||||
await this.conn.delete(table).where(eq(table.id, id));
|
|
||||||
}
|
|
||||||
async getStates(table: typeof businesses | typeof commercials): Promise<any[]> {
|
|
||||||
return await this.conn
|
|
||||||
.select({ state: table.state, count: sql<number>`count(${table.id})`.mapWith(Number) })
|
|
||||||
.from(table)
|
|
||||||
.groupBy(sql`${table.state}`)
|
|
||||||
.orderBy(sql`count desc`);
|
|
||||||
}
|
|
||||||
// ##############################################################
|
|
||||||
// Images for commercial Properties
|
|
||||||
// ##############################################################
|
|
||||||
|
|
||||||
async updateImageOrder(id: string, imageOrder: string[]) {
|
|
||||||
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
|
|
||||||
listing.imageOrder = imageOrder;
|
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
|
||||||
}
|
|
||||||
async deleteImage(id: string, name: string) {
|
|
||||||
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
|
|
||||||
const index = listing.imageOrder.findIndex(im => im === name);
|
|
||||||
if (index > -1) {
|
|
||||||
listing.imageOrder.splice(index, 1);
|
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async addImage(id: string, imagename: string) {
|
|
||||||
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
|
|
||||||
listing.imageOrder.push(imagename);
|
|
||||||
listing.imagePath = listing.id;
|
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,18 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
import { Controller, Inject } from '@nestjs/common';
|
||||||
import { FileService } from '../file/file.service.js';
|
|
||||||
import { convertStringToNullUndefined } from '../utils.js';
|
|
||||||
import { ListingsService } from './listings.service.js';
|
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { businesses, commercials } from 'src/drizzle/schema.js';
|
|
||||||
|
|
||||||
|
|
||||||
@Controller('listings/undefined')
|
@Controller('listings/undefined')
|
||||||
export class UnknownListingsController {
|
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);
|
||||||
@Get(':id')
|
// if (result) {
|
||||||
async findById(@Param('id') id:string): Promise<any> {
|
// return result;
|
||||||
const result = await this.listingsService.findById(id,businesses);
|
// } else {
|
||||||
if (result){
|
// return await this.listingsService.findById(id, commercials);
|
||||||
return result
|
// }
|
||||||
} else {
|
// }
|
||||||
return await this.listingsService.findById(id,commercials);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { Body, Controller, Post } from '@nestjs/common';
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
import { User } from 'src/models/db.model.js';
|
import { ErrorResponse, MailInfo } from '../models/main.model';
|
||||||
import { MailInfo } from 'src/models/main.model.js';
|
|
||||||
import { MailService } from './mail.service.js';
|
import { MailService } from './mail.service.js';
|
||||||
|
|
||||||
@Controller('mail')
|
@Controller('mail')
|
||||||
export class MailController {
|
export class MailController {
|
||||||
constructor(private mailService: MailService) {}
|
constructor(private mailService: MailService) {}
|
||||||
@Post()
|
@Post()
|
||||||
sendEMail(@Body() mailInfo: MailInfo): Promise<User> {
|
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||||
return this.mailService.sendInquiry(mailInfo);
|
if (mailInfo.listing) {
|
||||||
|
return this.mailService.sendInquiry(mailInfo);
|
||||||
|
} else {
|
||||||
|
return this.mailService.sendRequest(mailInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import path, { join } from 'path';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||||
import { FileService } from '../file/file.service.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 { UserModule } from '../user/user.module.js';
|
||||||
import { UserService } from '../user/user.service.js';
|
import { UserService } from '../user/user.service.js';
|
||||||
import { MailController } from './mail.controller.js';
|
import { MailController } from './mail.controller.js';
|
||||||
@@ -17,6 +19,7 @@ const password = process.env.amazon_password;
|
|||||||
imports: [
|
imports: [
|
||||||
DrizzleModule,
|
DrizzleModule,
|
||||||
UserModule,
|
UserModule,
|
||||||
|
GeoModule,
|
||||||
MailerModule.forRoot({
|
MailerModule.forRoot({
|
||||||
transport: {
|
transport: {
|
||||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||||
@@ -39,7 +42,7 @@ const password = process.env.amazon_password;
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [MailService, UserService, FileService],
|
providers: [MailService, UserService, FileService, GeoService],
|
||||||
controllers: [MailController],
|
controllers: [MailController],
|
||||||
})
|
})
|
||||||
export class MailModule {}
|
export class MailModule {}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { MailerService } from '@nestjs-modules/mailer';
|
import { MailerService } from '@nestjs-modules/mailer';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import path, { join } from 'path';
|
import path, { join } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { User } from '../models/db.model.js';
|
import { ZodError } from 'zod';
|
||||||
import { MailInfo } from '../models/main.model.js';
|
import { SenderSchema } from '../models/db.model.js';
|
||||||
|
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js';
|
||||||
import { UserService } from '../user/user.service.js';
|
import { UserService } from '../user/user.service.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -15,10 +16,23 @@ export class MailService {
|
|||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async sendInquiry(mailInfo: MailInfo): Promise<User> {
|
async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||||
//const user = await this.authService.getUser(mailInfo.userId) as KeycloakUser;
|
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);
|
const user = await this.userService.getUserByMail(mailInfo.email);
|
||||||
console.log(JSON.stringify(user));
|
if (isEmpty(mailInfo.sender.name)) {
|
||||||
|
return { fields: [{ fieldname: 'name', message: 'Required' }] };
|
||||||
|
}
|
||||||
await this.mailerService.sendMail({
|
await this.mailerService.sendMail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
from: '"Bizmatch Team" <info@bizmatch.net>', // override default from
|
from: '"Bizmatch Team" <info@bizmatch.net>', // override default from
|
||||||
@@ -34,8 +48,37 @@ export class MailService {
|
|||||||
iname: mailInfo.sender.name,
|
iname: mailInfo.sender.name,
|
||||||
phone: mailInfo.sender.phoneNumber,
|
phone: mailInfo.sender.phoneNumber,
|
||||||
email: mailInfo.sender.email,
|
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>
|
<!DOCTYPE html>
|
||||||
<p>You got an inquiry a</p>
|
<html lang="en">
|
||||||
<p>
|
<head>
|
||||||
{{inquiry}}
|
<meta charset="UTF-8">
|
||||||
</p>
|
<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,17 +1,16 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import express from 'express';
|
||||||
import { AppModule } from './app.module.js';
|
import { AppModule } from './app.module.js';
|
||||||
import * as express from 'express';
|
|
||||||
import path, { join } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
const server = express();
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.setGlobalPrefix('bizmatch');
|
app.setGlobalPrefix('bizmatch');
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: '*',
|
origin: '*',
|
||||||
|
//origin: 'http://localhost:4200', // Die URL Ihrer Angular-App
|
||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
allowedHeaders: 'Content-Type, Accept',
|
allowedHeaders: 'Content-Type, Accept, Authorization',
|
||||||
});
|
});
|
||||||
//origin: 'http://localhost:4200',
|
//origin: 'http://localhost:4200',
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export interface User {
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export interface UserData {
|
||||||
id?: string;
|
id?: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
@@ -14,60 +16,275 @@ export interface User {
|
|||||||
hasProfile?: boolean;
|
hasProfile?: boolean;
|
||||||
hasCompanyLogo?: boolean;
|
hasCompanyLogo?: boolean;
|
||||||
licensedIn?: string[];
|
licensedIn?: string[];
|
||||||
}
|
gender?: 'male' | 'female';
|
||||||
|
customerType?: 'buyer' | 'broker' | 'professional';
|
||||||
export interface BusinessListing {
|
customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||||
id: string;
|
|
||||||
userId?: string;
|
|
||||||
type?: number;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
city?: string;
|
|
||||||
state?: string;
|
|
||||||
price?: number;
|
|
||||||
favoritesForUser?: string[];
|
|
||||||
draft?: boolean;
|
|
||||||
realEstateIncluded?: boolean;
|
|
||||||
leasedLocation?: boolean;
|
|
||||||
franchiseResale?: boolean;
|
|
||||||
salesRevenue?: number;
|
|
||||||
cashFlow?: number;
|
|
||||||
supportAndTraining?: string;
|
|
||||||
employees?: number;
|
|
||||||
established?: number;
|
|
||||||
internalListingNumber?: number;
|
|
||||||
reasonForSale?: string;
|
|
||||||
brokerLicencing?: string;
|
|
||||||
internals?: string;
|
|
||||||
created?: Date;
|
created?: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
visits?: number;
|
|
||||||
lastVisit?: Date;
|
|
||||||
listingsCategory?: string;
|
|
||||||
}
|
}
|
||||||
|
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 interface CommercialPropertyListing {
|
export const GenderEnum = z.enum(['male', 'female']);
|
||||||
id: string;
|
export const CustomerTypeEnum = z.enum(['buyer', 'professional']);
|
||||||
userId?: string;
|
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||||
type?: number;
|
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);
|
||||||
title?: string;
|
const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']);
|
||||||
description?: string;
|
const TypeEnum = z.enum([
|
||||||
city?: string;
|
'automotive',
|
||||||
state?: string;
|
'industrialServices',
|
||||||
price?: number;
|
'foodAndRestaurant',
|
||||||
favoritesForUser?: string[];
|
'realEstate',
|
||||||
hideImage?: boolean;
|
'retail',
|
||||||
draft?: boolean;
|
'oilfield',
|
||||||
zipCode?: number;
|
'service',
|
||||||
county?: string;
|
'advertising',
|
||||||
email?: string;
|
'agriculture',
|
||||||
website?: string;
|
'franchise',
|
||||||
phoneNumber?: string;
|
'professional',
|
||||||
imageOrder?: string[];
|
'manufacturing',
|
||||||
imagePath?: string;
|
'uncategorized',
|
||||||
created?: Date;
|
]);
|
||||||
updated?: Date;
|
|
||||||
visits?: number;
|
const USStates = z.enum([
|
||||||
lastVisit?: Date;
|
'AL',
|
||||||
listingsCategory?: string;
|
'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,4 +1,4 @@
|
|||||||
import { BusinessListing, CommercialPropertyListing, User } from './db.model';
|
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
|
||||||
|
|
||||||
export interface StatesResult {
|
export interface StatesResult {
|
||||||
state: string;
|
state: string;
|
||||||
@@ -16,8 +16,8 @@ export interface KeyValueRatio {
|
|||||||
export interface KeyValueStyle {
|
export interface KeyValueStyle {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
oldValue?: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
bgColorClass: string;
|
|
||||||
textColorClass: string;
|
textColorClass: string;
|
||||||
}
|
}
|
||||||
export type SelectOption<T = number> = {
|
export type SelectOption<T = number> = {
|
||||||
@@ -36,52 +36,89 @@ export type ListingCategory = {
|
|||||||
export type ListingType = BusinessListing | CommercialPropertyListing;
|
export type ListingType = BusinessListing | CommercialPropertyListing;
|
||||||
|
|
||||||
export type ResponseBusinessListingArray = {
|
export type ResponseBusinessListingArray = {
|
||||||
data: BusinessListing[];
|
results: BusinessListing[];
|
||||||
total: number;
|
totalCount: number;
|
||||||
};
|
};
|
||||||
export type ResponseBusinessListing = {
|
export type ResponseBusinessListing = {
|
||||||
data: BusinessListing;
|
data: BusinessListing;
|
||||||
};
|
};
|
||||||
export type ResponseCommercialPropertyListingArray = {
|
export type ResponseCommercialPropertyListingArray = {
|
||||||
data: CommercialPropertyListing[];
|
results: CommercialPropertyListing[];
|
||||||
total: number;
|
totalCount: number;
|
||||||
};
|
};
|
||||||
export type ResponseCommercialPropertyListing = {
|
export type ResponseCommercialPropertyListing = {
|
||||||
data: CommercialPropertyListing;
|
data: CommercialPropertyListing;
|
||||||
};
|
};
|
||||||
export type ResponseUsersArray = {
|
export type ResponseUsersArray = {
|
||||||
data: User[];
|
results: User[];
|
||||||
total: number;
|
totalCount: number;
|
||||||
};
|
};
|
||||||
export interface ListingCriteria {
|
export interface ListCriteria {
|
||||||
start: number;
|
start: number;
|
||||||
length: number;
|
length: number;
|
||||||
page: number;
|
page: number;
|
||||||
pageCount: number;
|
types: string[];
|
||||||
type: number;
|
|
||||||
state: 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;
|
minPrice: number;
|
||||||
maxPrice: number;
|
maxPrice: number;
|
||||||
|
minRevenue: number;
|
||||||
|
maxRevenue: number;
|
||||||
|
minCashFlow: number;
|
||||||
|
maxCashFlow: number;
|
||||||
|
minNumberEmployees: number;
|
||||||
|
maxNumberEmployees: number;
|
||||||
|
establishedSince: number;
|
||||||
|
establishedUntil: number;
|
||||||
realEstateChecked: boolean;
|
realEstateChecked: boolean;
|
||||||
|
leasedLocation: boolean;
|
||||||
|
franchiseResale: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
category: 'professional|broker';
|
brokerName: string;
|
||||||
name: 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 {
|
export interface KeycloakUser {
|
||||||
id: string;
|
id: string;
|
||||||
createdTimestamp: number;
|
createdTimestamp?: number;
|
||||||
username: string;
|
username?: string;
|
||||||
enabled: boolean;
|
enabled?: boolean;
|
||||||
totp: boolean;
|
totp?: boolean;
|
||||||
emailVerified: boolean;
|
emailVerified?: boolean;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
disableableCredentialTypes: any[];
|
disableableCredentialTypes?: any[];
|
||||||
requiredActions: any[];
|
requiredActions?: any[];
|
||||||
notBefore: number;
|
notBefore?: number;
|
||||||
access: Access;
|
access?: Access;
|
||||||
|
}
|
||||||
|
export interface JwtUser {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
roles: string[];
|
||||||
}
|
}
|
||||||
export interface Access {
|
export interface Access {
|
||||||
manageGroupMembership: boolean;
|
manageGroupMembership: boolean;
|
||||||
@@ -130,6 +167,14 @@ export interface JwtToken {
|
|||||||
email: string;
|
email: string;
|
||||||
user_id: 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 {
|
interface Resourceaccess {
|
||||||
account: Realmaccess;
|
account: Realmaccess;
|
||||||
}
|
}
|
||||||
@@ -148,19 +193,162 @@ export interface AutoCompleteCompleteEvent {
|
|||||||
}
|
}
|
||||||
export interface MailInfo {
|
export interface MailInfo {
|
||||||
sender: Sender;
|
sender: Sender;
|
||||||
userId: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
|
url: string;
|
||||||
listing?: BusinessListing;
|
listing?: BusinessListing;
|
||||||
}
|
}
|
||||||
export interface Sender {
|
// export interface Sender {
|
||||||
name?: string;
|
// name?: string;
|
||||||
email?: string;
|
// email?: string;
|
||||||
phoneNumber?: string;
|
// phoneNumber?: string;
|
||||||
state?: string;
|
// state?: string;
|
||||||
comments?: string;
|
// comments?: string;
|
||||||
}
|
// }
|
||||||
export interface ImageProperty {
|
export interface ImageProperty {
|
||||||
id: string;
|
id: string;
|
||||||
code: string;
|
code: string;
|
||||||
name: 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,4 +1,3 @@
|
|||||||
import { Entity } from "redis-om";
|
|
||||||
export interface Geo {
|
export interface Geo {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -19,8 +18,8 @@ export interface Geo {
|
|||||||
nationality: string;
|
nationality: string;
|
||||||
timezones: Timezone[];
|
timezones: Timezone[];
|
||||||
translations: Translations;
|
translations: Translations;
|
||||||
latitude: string;
|
latitude: number;
|
||||||
longitude: string;
|
longitude: number;
|
||||||
emoji: string;
|
emoji: string;
|
||||||
emojiU: string;
|
emojiU: string;
|
||||||
states: State[];
|
states: State[];
|
||||||
@@ -29,16 +28,16 @@ export interface State {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
state_code: string;
|
state_code: string;
|
||||||
latitude: string;
|
latitude: number;
|
||||||
longitude: string;
|
longitude: number;
|
||||||
type: string;
|
type: string;
|
||||||
cities: City[];
|
cities: City[];
|
||||||
}
|
}
|
||||||
export interface City {
|
export interface City {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
latitude: string;
|
latitude: number;
|
||||||
longitude: string;
|
longitude: number;
|
||||||
}
|
}
|
||||||
export interface Translations {
|
export interface Translations {
|
||||||
kr: string;
|
kr: string;
|
||||||
@@ -62,3 +61,12 @@ export interface Timezone {
|
|||||||
abbreviation: string;
|
abbreviation: string;
|
||||||
tzName: string;
|
tzName: string;
|
||||||
}
|
}
|
||||||
|
export interface CountyData {
|
||||||
|
state: string;
|
||||||
|
state_full: string;
|
||||||
|
counties: string[];
|
||||||
|
}
|
||||||
|
export interface CountyRequest {
|
||||||
|
prefix: string;
|
||||||
|
states: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
|
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestDurationMiddleware implements NestMiddleware {
|
export class RequestDurationMiddleware implements NestMiddleware {
|
||||||
@@ -8,8 +8,17 @@ export class RequestDurationMiddleware implements NestMiddleware {
|
|||||||
use(req: Request, res: Response, next: NextFunction) {
|
use(req: Request, res: Response, next: NextFunction) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
res.on('finish', () => {
|
res.on('finish', () => {
|
||||||
|
// const duration = Date.now() - start;
|
||||||
|
// this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
|
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();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,18 @@ import { SelectOptionsService } from './select-options.service.js';
|
|||||||
|
|
||||||
@Controller('select-options')
|
@Controller('select-options')
|
||||||
export class SelectOptionsController {
|
export class SelectOptionsController {
|
||||||
constructor(private selectOptionsService:SelectOptionsService){}
|
constructor(private selectOptionsService: SelectOptionsService) {}
|
||||||
@Get()
|
@Get()
|
||||||
getSelectOption():any{
|
getSelectOption(): any {
|
||||||
return {
|
return {
|
||||||
typesOfBusiness:this.selectOptionsService.typesOfBusiness,
|
typesOfBusiness: this.selectOptionsService.typesOfBusiness,
|
||||||
prices:this.selectOptionsService.prices,
|
prices: this.selectOptionsService.prices,
|
||||||
listingCategories:this.selectOptionsService.listingCategories,
|
listingCategories: this.selectOptionsService.listingCategories,
|
||||||
categories:this.selectOptionsService.categories,
|
customerTypes: this.selectOptionsService.customerTypes,
|
||||||
locations:this.selectOptionsService.locations,
|
locations: this.selectOptionsService.locations,
|
||||||
typesOfCommercialProperty:this.selectOptionsService.typesOfCommercialProperty,
|
typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty,
|
||||||
}
|
customerSubTypes: this.selectOptionsService.customerSubTypes,
|
||||||
}
|
distances: this.selectOptionsService.distances,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,28 @@ import { ImageType, KeyValue, KeyValueStyle } from '../models/main.model.js';
|
|||||||
export class SelectOptionsService {
|
export class SelectOptionsService {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
public typesOfBusiness: Array<KeyValueStyle> = [
|
public typesOfBusiness: Array<KeyValueStyle> = [
|
||||||
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' },
|
{ name: 'Automotive', value: 'automotive', oldValue: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||||
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
|
{ name: 'Industrial Services', value: 'industrialServices', oldValue: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||||
{ name: 'Real Estate', value: '3', icon: 'pi pi-building', bgColorClass: 'bg-blue-100', textColorClass: 'text-blue-600' },
|
{ name: 'Food and Restaurant', value: 'foodAndRestaurant', oldValue: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' },
|
||||||
{ name: 'Uncategorized', value: '4', icon: 'pi pi-question', bgColorClass: 'bg-cyan-100', textColorClass: 'text-cyan-600' },
|
{ name: 'Real Estate', value: 'realEstate', oldValue: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
|
||||||
{ name: 'Retail', value: '5', icon: 'fa-solid fa-money-bill-wave', bgColorClass: 'bg-pink-100', textColorClass: 'text-pink-600' },
|
{ name: 'Retail', value: 'retail', oldValue: '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', bgColorClass: 'bg-indigo-100', textColorClass: 'text-indigo-600' },
|
{ name: 'Oilfield SVE and MFG.', value: 'oilfield', oldValue: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' },
|
||||||
{ name: 'Service', value: '7', icon: 'fa-solid fa-umbrella', bgColorClass: 'bg-teal-100', textColorClass: 'text-teal-600' },
|
{ name: 'Service', value: 'service', oldValue: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' },
|
||||||
{ name: 'Advertising', value: '8', icon: 'fa-solid fa-rectangle-ad', bgColorClass: 'bg-orange-100', textColorClass: 'text-orange-600' },
|
{ name: 'Advertising', value: 'advertising', oldValue: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' },
|
||||||
{ name: 'Agriculture', value: '9', icon: 'fa-solid fa-wheat-awn', bgColorClass: 'bg-bluegray-100', textColorClass: 'text-bluegray-600' },
|
{ name: 'Agriculture', value: 'agriculture', oldValue: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' },
|
||||||
{ name: 'Franchise', value: '10', icon: 'pi pi-star', bgColorClass: 'bg-purple-100', textColorClass: 'text-purple-600' },
|
{ name: 'Franchise', value: 'franchise', oldValue: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' },
|
||||||
{ name: 'Professional', value: '11', icon: 'fa-solid fa-user-gear', bgColorClass: 'bg-gray-100', textColorClass: 'text-gray-600' },
|
{ name: 'Professional', value: 'professional', oldValue: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' },
|
||||||
{ name: 'Manufacturing', value: '12', icon: 'fa-solid fa-industry', bgColorClass: 'bg-red-100', textColorClass: 'text-red-600' },
|
{ name: 'Manufacturing', value: 'manufacturing', oldValue: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' },
|
||||||
{ name: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', bgColorClass: 'bg-primary-100', textColorClass: 'text-primary-600' },
|
{ name: 'Uncategorized', value: 'uncategorized', oldValue: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' },
|
||||||
];
|
];
|
||||||
public typesOfCommercialProperty: Array<KeyValueStyle> = [
|
public typesOfCommercialProperty: Array<KeyValueStyle> = [
|
||||||
{ name: 'Retail', value: '100', icon: 'fa-solid fa-money-bill-wave', bgColorClass: 'bg-pink-100', textColorClass: 'text-pink-600' },
|
{ name: 'Retail', value: 'retail', oldValue: '100', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' },
|
||||||
{ name: 'Land', value: '101', icon: 'pi pi-building', bgColorClass: 'bg-blue-100', textColorClass: 'text-blue-600' },
|
{ name: 'Land', value: 'land', oldValue: '101', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
|
||||||
{ name: 'Industrial', value: '102', icon: 'fa-solid fa-industry', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
|
{ name: 'Industrial', value: 'industrial', oldValue: '102', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||||
{ name: 'Office', value: '103', icon: 'fa-solid fa-umbrella', bgColorClass: 'bg-teal-100', textColorClass: 'text-teal-600' },
|
{ name: 'Office', value: 'office', oldValue: '103', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' },
|
||||||
{ name: 'Mixed Use', value: '104', icon: 'fa-solid fa-rectangle-ad', bgColorClass: 'bg-orange-100', textColorClass: 'text-orange-600' },
|
{ name: 'Mixed Use', value: 'mixedUse', oldValue: '104', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' },
|
||||||
{ name: 'Multifamily', value: '105', icon: 'pi pi-star', bgColorClass: 'bg-purple-100', textColorClass: 'text-purple-600' },
|
{ name: 'Multifamily', value: 'multifamily', oldValue: '105', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' },
|
||||||
{ name: 'Uncategorized', value: '106', icon: 'pi pi-question', bgColorClass: 'bg-cyan-100', textColorClass: 'text-cyan-600' },
|
{ name: 'Uncategorized', value: 'uncategorized', oldValue: '106', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' },
|
||||||
];
|
];
|
||||||
public prices: Array<KeyValue> = [
|
public prices: Array<KeyValue> = [
|
||||||
{ name: '$100K', value: '100000' },
|
{ name: '$100K', value: '100000' },
|
||||||
@@ -35,13 +35,36 @@ export class SelectOptionsService {
|
|||||||
{ name: '$1M', value: '1000000' },
|
{ name: '$1M', value: '1000000' },
|
||||||
{ name: '$5M', value: '5000000' },
|
{ 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> = [
|
public listingCategories: Array<KeyValue> = [
|
||||||
{ name: 'Business', value: 'business' },
|
{ name: 'Business', value: 'business' },
|
||||||
{ name: 'Commercial Property', value: 'commercialProperty' },
|
{ name: 'Commercial Property', value: 'commercialProperty' },
|
||||||
];
|
];
|
||||||
public categories: Array<KeyValueStyle> = [
|
public customerTypes: Array<KeyValue> = [
|
||||||
{ name: 'Broker', value: 'broker', icon: 'pi-image', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' },
|
{ name: 'Buyer', value: 'buyer' },
|
||||||
{ name: 'Professional', value: 'professional', icon: 'pi-globe', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
|
{ 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[] = [
|
public imageTypes: ImageType[] = [
|
||||||
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
|
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
|
||||||
|
|||||||
@@ -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,21 +1,28 @@
|
|||||||
import { Body, Controller, Get, Inject, Param, Post, Put, Query, Req } from '@nestjs/common';
|
import { Body, Controller, Get, Inject, Param, Post, Query, Request, UseGuards } from '@nestjs/common';
|
||||||
import { UserService } from './user.service.js';
|
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { User } from 'src/models/db.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')
|
@Controller('user')
|
||||||
export class UserController {
|
export class UserController {
|
||||||
|
constructor(
|
||||||
constructor(private userService: UserService, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
private userService: UserService,
|
||||||
|
private fileService: FileService,
|
||||||
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
|
) {}
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
findByMail(@Query('mail') mail: string): any {
|
findByMail(@Request() req, @Query('mail') mail: string): any {
|
||||||
this.logger.info(`Searching for user with EMail: ${mail}`);
|
this.logger.info(`Searching for user with EMail: ${mail}`);
|
||||||
const user = this.userService.getUserByMail(mail);
|
const user = this.userService.getUserByMail(mail, req.user as JwtUser);
|
||||||
this.logger.info(`Found user: ${JSON.stringify(user)}`);
|
this.logger.info(`Found user: ${JSON.stringify(user)}`);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findById(@Param('id') id: string): any {
|
findById(@Param('id') id: string): any {
|
||||||
this.logger.info(`Searching for user with ID: ${id}`);
|
this.logger.info(`Searching for user with ID: ${id}`);
|
||||||
@@ -32,11 +39,33 @@ export class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('search')
|
@Post('search')
|
||||||
find(@Body() criteria: any): any {
|
find(@Body() criteria: UserListingCriteria): any {
|
||||||
this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
|
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)}`);
|
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
|
||||||
return 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,12 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||||
import { FileService } from '../file/file.service.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 { UserController } from './user.controller.js';
|
||||||
import { UserService } from './user.service.js';
|
import { UserService } from './user.service.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DrizzleModule],
|
imports: [DrizzleModule, GeoModule],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
providers: [UserService, FileService],
|
providers: [UserService, FileService, GeoService],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
@@ -1,77 +1,156 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { and, eq, ilike, or, sql } from 'drizzle-orm';
|
import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { PG_CONNECTION } from 'src/drizzle/schema.js';
|
|
||||||
import { User } from 'src/models/db.model.js';
|
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
|
import { ZodError } from 'zod';
|
||||||
import * as schema from '../drizzle/schema.js';
|
import * as schema from '../drizzle/schema.js';
|
||||||
|
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { ListingCriteria } from '../models/main.model.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()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
|
private geoService: GeoService,
|
||||||
) {}
|
) {}
|
||||||
private getConditions(criteria: ListingCriteria): any[] {
|
|
||||||
const conditions = [];
|
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}%`));
|
||||||
|
}
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
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) {
|
if (criteria.state) {
|
||||||
conditions.push(sql`EXISTS (SELECT 1 FROM unnest(users."areasServed") AS area WHERE area LIKE '%' || ${criteria.state} || '%')`);
|
whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${criteria.state})`);
|
||||||
}
|
}
|
||||||
if (criteria.name) {
|
return whereConditions;
|
||||||
conditions.push(or(ilike(schema.users.firstname, `%${criteria.name}%`), ilike(schema.users.lastname, `%${criteria.name}%`)));
|
|
||||||
}
|
|
||||||
return conditions;
|
|
||||||
}
|
}
|
||||||
async getUserByMail(email: string) {
|
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
|
const users = (await this.conn
|
||||||
.select()
|
.select()
|
||||||
.from(schema.users)
|
.from(schema.users)
|
||||||
.where(sql`email = ${email}`)) as User[];
|
.where(sql`email = ${email}`)) as User[];
|
||||||
const user = users[0];
|
if (users.length === 0) {
|
||||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(user.id);
|
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname) };
|
||||||
user.hasProfile = this.fileService.hasProfile(user.id);
|
const u = await this.saveUser(user);
|
||||||
return 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) {
|
async getUserById(id: string) {
|
||||||
const users = (await this.conn
|
const users = (await this.conn
|
||||||
.select()
|
.select()
|
||||||
.from(schema.users)
|
.from(schema.users)
|
||||||
.where(sql`id = ${id}`)) as User[];
|
.where(sql`id = ${id}`)) as User[];
|
||||||
|
|
||||||
const user = users[0];
|
const user = users[0];
|
||||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(id);
|
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||||
user.hasProfile = this.fileService.hasProfile(id);
|
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||||
return user;
|
return convertDrizzleUserToUser(user);
|
||||||
}
|
}
|
||||||
async saveUser(user: any): Promise<User> {
|
async saveUser(user: User): Promise<User> {
|
||||||
if (user.id) {
|
try {
|
||||||
const [updateUser] = await this.conn.update(schema.users).set(user).where(eq(schema.users.id, user.id)).returning();
|
user.updated = new Date();
|
||||||
return updateUser as User;
|
if (user.id) {
|
||||||
} else {
|
user.created = new Date(user.created);
|
||||||
const [newUser] = await this.conn.insert(schema.users).values(user).returning();
|
} else {
|
||||||
return newUser as User;
|
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 findUser(criteria: ListingCriteria) {
|
async getStates(): Promise<any[]> {
|
||||||
const start = criteria.start ? criteria.start : 0;
|
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 length = criteria.length ? criteria.length : 12;
|
const result = await this.conn.execute(query);
|
||||||
const conditions = this.getConditions(criteria);
|
return result.rows;
|
||||||
const [data, total] = await Promise.all([
|
|
||||||
this.conn
|
|
||||||
.select()
|
|
||||||
.from(schema.users)
|
|
||||||
.where(and(...conditions))
|
|
||||||
.offset(start)
|
|
||||||
.limit(length),
|
|
||||||
this.conn
|
|
||||||
.select({ count: sql`count(*)` })
|
|
||||||
.from(schema.users)
|
|
||||||
.where(and(...conditions))
|
|
||||||
.then(result => Number(result[0].count)),
|
|
||||||
]);
|
|
||||||
return { total, data };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
export function convertStringToNullUndefined(value) {
|
||||||
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
|
// Konvertiert den Wert zu Kleinbuchstaben für eine case-insensitive Überprüfung
|
||||||
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase();
|
const lowerCaseValue = typeof value === 'boolean' ? value : value?.toLowerCase();
|
||||||
@@ -11,3 +16,110 @@ export function convertStringToNullUndefined(value) {
|
|||||||
// Gibt den Originalwert zurück, wenn es sich nicht um 'null' oder 'undefined' handelt
|
// Gibt den Originalwert zurück, wenn es sich nicht um 'null' oder 'undefined' handelt
|
||||||
return value;
|
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));
|
||||||
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,58 +3,63 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve & http-server ../bizmatch-server",
|
"start": "ng serve --host 0.0.0.0 & http-server ../bizmatch-server",
|
||||||
"build": "ng build",
|
"prebuild": "node version.js",
|
||||||
"build.dev": "ng build --configuration dev",
|
"build": "node version.js && ng build",
|
||||||
|
"build.dev": "node version.js && ng build --configuration dev --output-hashing=all",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test",
|
"test": "ng test",
|
||||||
"serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs"
|
"serve:ssr:bizmatch": "node dist/bizmatch/server/server.mjs"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.3.3",
|
"@angular/animations": "^18.1.3",
|
||||||
"@angular/cdk": "^17.3.2",
|
"@angular/cdk": "^18.0.6",
|
||||||
"@angular/common": "^17.3.3",
|
"@angular/common": "^18.1.3",
|
||||||
"@angular/compiler": "^17.3.3",
|
"@angular/compiler": "^18.1.3",
|
||||||
"@angular/core": "^17.3.3",
|
"@angular/core": "^18.1.3",
|
||||||
"@angular/forms": "^17.3.3",
|
"@angular/forms": "^18.1.3",
|
||||||
"@angular/platform-browser": "^17.3.3",
|
"@angular/platform-browser": "^18.1.3",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.3",
|
"@angular/platform-browser-dynamic": "^18.1.3",
|
||||||
"@angular/platform-server": "^17.3.3",
|
"@angular/platform-server": "^18.1.3",
|
||||||
"@angular/router": "^17.3.3",
|
"@angular/router": "^18.1.3",
|
||||||
"@fortawesome/angular-fontawesome": "^0.14.1",
|
"@fortawesome/angular-fontawesome": "^0.15.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.5.1",
|
"@fortawesome/fontawesome-free": "^6.5.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.5.1",
|
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@types/uuid": "^9.0.8",
|
"@ng-select/ng-select": "^13.4.1",
|
||||||
"angular-cropperjs": "^14.0.1",
|
"@ngneat/until-destroy": "^10.0.0",
|
||||||
"angular-mixed-cdk-drag-drop": "^2.2.3",
|
"@types/cropperjs": "^1.3.0",
|
||||||
|
"@types/uuid": "^10.0.0",
|
||||||
"browser-bunyan": "^1.8.0",
|
"browser-bunyan": "^1.8.0",
|
||||||
"cropperjs": "^1.6.1",
|
"dayjs": "^1.11.11",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"flowbite": "^2.4.1",
|
||||||
"jwt-decode": "^4.0.0",
|
"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",
|
"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",
|
"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",
|
"rxjs": "~7.8.1",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.6.3",
|
||||||
"urlcat": "^3.1.0",
|
"urlcat": "^3.1.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^10.0.0",
|
||||||
"zone.js": "~0.14.4"
|
"zone.js": "~0.14.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^17.3.3",
|
"@angular-devkit/build-angular": "^18.1.3",
|
||||||
"@angular/cli": "^17.3.3",
|
"@angular/cli": "^18.1.3",
|
||||||
"@angular/compiler-cli": "^17.3.3",
|
"@angular/compiler-cli": "^18.1.3",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jasmine": "~5.1.4",
|
"@types/jasmine": "~5.1.4",
|
||||||
"@types/node": "^20.11.20",
|
"@types/node": "^20.14.9",
|
||||||
|
"autoprefixer": "^10.4.19",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"jasmine-core": "~5.1.2",
|
"jasmine-core": "~5.1.2",
|
||||||
"karma": "~6.4.2",
|
"karma": "~6.4.2",
|
||||||
@@ -62,6 +67,8 @@
|
|||||||
"karma-coverage": "~2.2.1",
|
"karma-coverage": "~2.2.1",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,55 @@
|
|||||||
<div class="container">
|
<!-- <div class="container"> -->
|
||||||
<div class="content">
|
<div [ngClass]="{ 'bg-slate-100': actualRoute !== 'home' }">
|
||||||
@if (actualRoute !=='home' && actualRoute !=='pricing'){
|
@if (actualRoute !=='home' && actualRoute !=='pricing'){
|
||||||
<header></header>
|
<header></header>
|
||||||
}
|
}
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</div>
|
|
||||||
<footer></footer>
|
<app-footer></app-footer>
|
||||||
</div>
|
</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) {
|
@if (loadingService.isLoading$ | async) {
|
||||||
<div class="spinner-overlay">
|
<div class="spinner-overlay">
|
||||||
<div class="spinner-container">
|
<div class="spinner-container">
|
||||||
<p-progressSpinner></p-progressSpinner>
|
@let loadingText = (loadingService.loadingText$ | async); @if(loadingText){
|
||||||
<div class="spinner-text" *ngIf="loadingService.loadingText$ | async as loadingText">{{loadingText}}</div>
|
<div class="spinner-text">{{ loadingText }}</div>
|
||||||
|
}
|
||||||
|
<div role="status">
|
||||||
|
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<!-- <span class="sr-only">Loading ...</span> -->
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
<!-- <div *ngIf="loadingService.isLoading$ | async" class="spinner-overlay">
|
||||||
|
<div class="spinner-container">
|
||||||
|
<ng-container *ngIf="loadingService.loadingText$ | async as loadingText">
|
||||||
|
<div *ngIf="loadingText" class="spinner-text">{{ loadingText }}</div>
|
||||||
|
</ng-container>
|
||||||
|
<div role="status">
|
||||||
|
<svg aria-hidden="true" class="inline w-10 h-10 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<app-message-container></app-message-container>
|
||||||
|
<app-search-modal></app-search-modal>
|
||||||
|
<app-confirmation></app-confirmation>
|
||||||
|
|||||||
@@ -1,64 +1,48 @@
|
|||||||
.container {
|
// .progress-spinner {
|
||||||
display: flex;
|
// position: fixed;
|
||||||
flex-direction: column;
|
// z-index: 999;
|
||||||
min-height: 100vh;
|
// top: 0;
|
||||||
}
|
// left: 0;
|
||||||
.content {
|
// bottom: 0;
|
||||||
flex: 1;
|
// right: 0;
|
||||||
/* Optional: Padding für den Inhalt, um sicherzustellen, dass er nicht direkt am Footer klebt */
|
// display: flex;
|
||||||
// padding-bottom: 20px;
|
// 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 {
|
// .progress-spinner:before {
|
||||||
content: '';
|
// content: '';
|
||||||
display: block;
|
// display: block;
|
||||||
position: fixed;
|
// position: fixed;
|
||||||
top: 0;
|
// top: 0;
|
||||||
left: 0;
|
// left: 0;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
height: 100%;
|
// height: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
// background-color: rgba(0, 0, 0, 0.3);
|
||||||
}
|
// }
|
||||||
.spinner-text {
|
.spinner-text {
|
||||||
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
|
margin-top: 20px; /* Abstand zwischen Spinner und Text anpassen */
|
||||||
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */
|
font-size: 20px; /* Schriftgröße nach Bedarf anpassen */
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
text-shadow: 0 0 8px rgba(255, 255, 255, 0.6); /* Hinzufügen eines leichten Glows */
|
text-shadow: 0 0 8px rgba(255, 255, 255, 0.6); /* Hinzufügen eines leichten Glows */
|
||||||
font-weight: bold; /* Macht den Text fett */
|
font-weight: bold; /* Macht den Text fett */
|
||||||
}
|
}
|
||||||
.spinner-overlay {
|
.spinner-overlay {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: fixed; /* oder 'absolute', abhängig vom Kontext */
|
position: fixed; /* oder 'absolute', abhängig vom Kontext */
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(0, 0, 0, 0.4);
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
z-index: 1000; /* Stellt sicher, dass der Overlay über anderen Elementen liegt */
|
z-index: 1000; /* Stellt sicher, dass der Overlay über anderen Elementen liegt */
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-container {
|
.spinner-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* Keine Hintergrundfarbe hier, um Transparenz nur im Overlay zu haben */
|
/* 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,46 +1,63 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||||
import { HeaderComponent } from './components/header/header.component';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { ProgressSpinnerModule } from 'primeng/progressspinner';
|
|
||||||
import { ToastModule } from 'primeng/toast';
|
|
||||||
import { LoadingService } from './services/loading.service';
|
|
||||||
import { HomeComponent } from './pages/home/home.component';
|
|
||||||
import { filter } from 'rxjs/operators';
|
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 { FooterComponent } from './components/footer/footer.component';
|
||||||
import { KeycloakService } from './services/keycloak.service';
|
import { HeaderComponent } from './components/header/header.component';
|
||||||
import { KeycloakEventType } from './models/keycloak-event';
|
import { MessageContainerComponent } from './components/message/message-container.component';
|
||||||
import { createGenericObject } from './utils/utils';
|
import { SearchModalComponent } from './components/search-modal/search-modal.component';
|
||||||
import onChange from 'on-change';
|
import { LoadingService } from './services/loading.service';
|
||||||
import { UserService } from './services/user.service';
|
import { UserService } from './services/user.service';
|
||||||
import {ListingCriteria} from '../../../bizmatch-server/src/models/main.model'
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterOutlet, HeaderComponent, ProgressSpinnerModule, FooterComponent],
|
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent],
|
||||||
|
providers: [],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss'
|
styleUrl: './app.component.scss',
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
|
build = build;
|
||||||
title = 'bizmatch';
|
title = 'bizmatch';
|
||||||
actualRoute ='';
|
actualRoute = '';
|
||||||
listingCriteria:ListingCriteria = onChange(createGenericObject<ListingCriteria>(),(path, value, previousValue, applyData)=>{
|
|
||||||
sessionStorage.setItem('criteria',JSON.stringify(value));
|
public constructor(
|
||||||
});
|
public loadingService: LoadingService,
|
||||||
public constructor(public loadingService: LoadingService, private router: Router,private activatedRoute: ActivatedRoute, private keycloakService:KeycloakService,private userService:UserService) {
|
private router: Router,
|
||||||
this.router.events.pipe(
|
private activatedRoute: ActivatedRoute,
|
||||||
filter(event => event instanceof NavigationEnd)
|
private keycloakService: KeycloakService,
|
||||||
).subscribe(() => {
|
private userService: UserService,
|
||||||
|
private confirmationService: ConfirmationService,
|
||||||
|
) {
|
||||||
|
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
|
||||||
let currentRoute = this.activatedRoute.root;
|
let currentRoute = this.activatedRoute.root;
|
||||||
while (currentRoute.children[0] !== undefined) {
|
while (currentRoute.children[0] !== undefined) {
|
||||||
currentRoute = currentRoute.children[0];
|
currentRoute = currentRoute.children[0];
|
||||||
}
|
}
|
||||||
// Hier haben Sie Zugriff auf den aktuellen Route-Pfad
|
// Hier haben Sie Zugriff auf den aktuellen Route-Pfad
|
||||||
this.actualRoute=currentRoute.snapshot.url[0].path
|
this.actualRoute = currentRoute.snapshot.url[0].path;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ngOnInit(){
|
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 { APP_INITIALIZER, ApplicationConfig } from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScrolling } from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
import { provideClientHydration } from '@angular/platform-browser';
|
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
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 { environment } from '../environments/environment';
|
||||||
import { SelectOptionsService } from './services/select-options.service';
|
import { customKeycloakAdapter } from '../keycloak';
|
||||||
import { KeycloakService } from './services/keycloak.service';
|
import { routes } from './app.routes';
|
||||||
import { UserService } from './services/user.service';
|
|
||||||
import { LoadingInterceptor } from './interceptors/loading.interceptor';
|
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()
|
// provideClientHydration()
|
||||||
|
const logger = createLogger('ApplicationConfig');
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
{provide:KeycloakService},
|
{ provide: KeycloakService },
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
useFactory: initializeKeycloak,
|
// useFactory: initializeKeycloak,
|
||||||
|
//useFactory: initializeKeycloak,
|
||||||
|
useFactory: initializeKeycloak3,
|
||||||
multi: true,
|
multi: true,
|
||||||
deps: [KeycloakService],
|
//deps: [KeycloakService],
|
||||||
|
deps: [KeycloakInitializerService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER,
|
||||||
@@ -28,28 +34,70 @@ export const appConfig: ApplicationConfig = {
|
|||||||
deps: [SelectOptionsService],
|
deps: [SelectOptionsService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide:HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
useClass:LoadingInterceptor,
|
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) {
|
function initServices(selectOptions: SelectOptionsService) {
|
||||||
return () => {
|
return async () => {
|
||||||
//selectOptions.init();
|
await selectOptions.init();
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
function initServices(selectOptions:SelectOptionsService) {
|
export function initializeKeycloak3(keycloak: KeycloakInitializerService) {
|
||||||
return () => {
|
return () => keycloak.initialize();
|
||||||
selectOptions.init();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function initializeKeycloak(keycloak: KeycloakService) {
|
||||||
return () =>
|
return async () => {
|
||||||
keycloak.init({
|
logger.info(`###>calling keycloakService init ...`);
|
||||||
|
const authenticated = await keycloak.init({
|
||||||
config: {
|
config: {
|
||||||
url: environment.keycloak.url,
|
url: environment.keycloak.url,
|
||||||
realm: environment.keycloak.realm,
|
realm: environment.keycloak.realm,
|
||||||
@@ -57,7 +105,13 @@ function initializeKeycloak(keycloak: KeycloakService) {
|
|||||||
},
|
},
|
||||||
initOptions: {
|
initOptions: {
|
||||||
onLoad: 'check-sso',
|
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,6 +1,9 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { LogoutComponent } from './components/logout/logout.component';
|
import { LogoutComponent } from './components/logout/logout.component';
|
||||||
import { authGuard } from './guards/auth.guard';
|
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 { 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 { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
|
||||||
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
|
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
|
||||||
@@ -46,6 +49,11 @@ export const routes: Routes = [
|
|||||||
path: 'details-commercial-property-listing/:id',
|
path: 'details-commercial-property-listing/:id',
|
||||||
component: DetailsCommercialPropertyListingComponent,
|
component: DetailsCommercialPropertyListingComponent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'listing/:id',
|
||||||
|
canActivate: [ListingCategoryGuard],
|
||||||
|
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
|
||||||
|
},
|
||||||
// #########
|
// #########
|
||||||
// User Details
|
// User Details
|
||||||
{
|
{
|
||||||
@@ -57,57 +65,62 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: 'account',
|
path: 'account',
|
||||||
component: AccountComponent,
|
component: AccountComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'account/:id',
|
||||||
|
component: AccountComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// Create, Update Listings
|
// Create, Update Listings
|
||||||
{
|
{
|
||||||
path: 'editBusinessListing/:id',
|
path: 'editBusinessListing/:id',
|
||||||
component: EditBusinessListingComponent,
|
component: EditBusinessListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'createBusinessListing',
|
path: 'createBusinessListing',
|
||||||
component: EditBusinessListingComponent,
|
component: EditBusinessListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'editCommercialPropertyListing/:id',
|
path: 'editCommercialPropertyListing/:id',
|
||||||
component: EditCommercialPropertyListingComponent,
|
component: EditCommercialPropertyListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'createCommercialPropertyListing',
|
path: 'createCommercialPropertyListing',
|
||||||
component: EditCommercialPropertyListingComponent,
|
component: EditCommercialPropertyListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// My Listings
|
// My Listings
|
||||||
{
|
{
|
||||||
path: 'myListings',
|
path: 'myListings',
|
||||||
component: MyListingComponent,
|
component: MyListingComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// My Favorites
|
// My Favorites
|
||||||
{
|
{
|
||||||
path: 'myFavorites',
|
path: 'myFavorites',
|
||||||
component: FavoritesComponent,
|
component: FavoritesComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// EMAil Us
|
// EMAil Us
|
||||||
{
|
{
|
||||||
path: 'emailUs',
|
path: 'emailUs',
|
||||||
component: EmailUsComponent,
|
component: EmailUsComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// Logout
|
// Logout
|
||||||
{
|
{
|
||||||
path: 'logout',
|
path: 'logout',
|
||||||
component: LogoutComponent,
|
component: LogoutComponent,
|
||||||
canActivate: [authGuard],
|
canActivate: [AuthGuard],
|
||||||
},
|
},
|
||||||
// #########
|
// #########
|
||||||
// Pricing
|
// Pricing
|
||||||
|
|||||||
@@ -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
@@ -1,6 +1,24 @@
|
|||||||
:host{
|
:host {
|
||||||
height: 192px;
|
// position: absolute;
|
||||||
|
// bottom: 0px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
}
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
.order-2 {
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
.order-3 {
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
section p {
|
||||||
|
display: block;
|
||||||
|
margin-block-start: 1em;
|
||||||
|
margin-block-end: 1em;
|
||||||
|
margin-inline-start: 0px;
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
unicode-bidi: isolate;
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,26 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ButtonModule } from 'primeng/button';
|
|
||||||
import { CheckboxModule } from 'primeng/checkbox';
|
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
|
||||||
import {StyleClassModule} from 'primeng/styleclass';
|
|
||||||
import { DropdownModule } from 'primeng/dropdown';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { UserService } from '../../services/user.service';
|
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||||
import { SharedModule } from '../../shared/shared/shared.module';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
|
import { initFlowbite } from 'flowbite';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'footer',
|
selector: 'app-footer',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule],
|
imports: [CommonModule, FormsModule, RouterModule, FontAwesomeModule],
|
||||||
templateUrl: './footer.component.html',
|
templateUrl: './footer.component.html',
|
||||||
styleUrl: './footer.component.scss'
|
styleUrl: './footer.component.scss',
|
||||||
})
|
})
|
||||||
export class FooterComponent {
|
export class FooterComponent {
|
||||||
constructor(public userService:UserService){}
|
privacyVisible = false;
|
||||||
login(){
|
termsVisible = false;
|
||||||
this.userService.login(window.location.href);
|
currentYear: number = new Date().getFullYear();
|
||||||
}
|
constructor(private router: Router) {}
|
||||||
|
ngOnInit() {
|
||||||
|
this.router.events.subscribe(event => {
|
||||||
|
if (event instanceof NavigationEnd) {
|
||||||
|
initFlowbite();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,249 @@
|
|||||||
<div class="wrapper">
|
<!-- <nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||||
<div class="pl-3 flex align-items-center gap-2">
|
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||||
<a routerLink="/home"><img src="assets/images/header-logo.png" height="40" alt="bizmatch" /></a>
|
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||||
<p-tabMenu [model]="tabItems" ariaLabelledBy="label" styleClass="flex" [activeItem]="activeItem">
|
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
|
||||||
</p-tabMenu>
|
</a>
|
||||||
<p-menubar [model]="menuItems"></p-menubar>
|
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
|
||||||
<div *ngIf="user$ | async as user else empty">Welcome, {{user.firstname}}</div>
|
<button
|
||||||
<ng-template #empty>
|
type="button"
|
||||||
</ng-template>
|
class="flex text-sm bg-gray-200 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||||
</div>
|
id="user-menu-button"
|
||||||
</div>
|
aria-expanded="false"
|
||||||
|
[attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'"
|
||||||
|
data-dropdown-placement="bottom"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open user menu</span>
|
||||||
|
@if(user){
|
||||||
|
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" alt="user photo" />
|
||||||
|
} @else {
|
||||||
|
<i class="flex justify-center items-center text-stone-50 w-8 h-8 rounded-full fa-solid fa-bars"></i>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
@if(user){
|
||||||
|
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-login">
|
||||||
|
<div class="px-4 py-3">
|
||||||
|
<span class="block text-sm text-gray-900 dark:text-white">Welcome, {{ user.firstname }} </span>
|
||||||
|
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">{{ user.email }}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||||
|
<li>
|
||||||
|
<a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||||
|
>Create Listing</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="/myListings" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Listings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="/emailUs" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">EMail Us</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-unknown">
|
||||||
|
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||||
|
<li>
|
||||||
|
<a (click)="login()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Log In</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a (click)="register()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Register</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
data-collapse-toggle="navbar-user"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||||
|
aria-controls="navbar-user"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||||
|
<ul
|
||||||
|
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLinkActive="active-link"
|
||||||
|
routerLink="/businessListings"
|
||||||
|
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/businessListings') }"
|
||||||
|
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
aria-current="page"
|
||||||
|
(click)="closeMenus()"
|
||||||
|
>Businesses</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLinkActive="active-link"
|
||||||
|
routerLink="/commercialPropertyListings"
|
||||||
|
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/commercialPropertyListings') }"
|
||||||
|
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
(click)="closeMenus()"
|
||||||
|
>Properties</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLinkActive="active-link"
|
||||||
|
routerLink="/brokerListings"
|
||||||
|
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/brokerListings') }"
|
||||||
|
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
(click)="closeMenus()"
|
||||||
|
>Professionals</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav> -->
|
||||||
|
|
||||||
|
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||||
|
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||||
|
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||||
|
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
|
||||||
|
<!-- Filter button -->
|
||||||
|
@if(isListingUrl()){
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
#triggerButton
|
||||||
|
(click)="openModal()"
|
||||||
|
id="filterDropdownButton"
|
||||||
|
class="max-sm:hidden px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 md:me-2"
|
||||||
|
>
|
||||||
|
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex text-sm bg-gray-200 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
||||||
|
id="user-menu-button"
|
||||||
|
aria-expanded="false"
|
||||||
|
[attr.data-dropdown-toggle]="user ? 'user-login' : 'user-unknown'"
|
||||||
|
data-dropdown-placement="bottom"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open user menu</span>
|
||||||
|
@if(user){
|
||||||
|
<img class="w-8 h-8 rounded-full object-cover" src="{{ profileUrl }}" alt="user photo" />
|
||||||
|
} @else {
|
||||||
|
<i class="flex justify-center items-center text-stone-50 w-8 h-8 rounded-full fa-solid fa-bars"></i>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
<!-- Dropdown menu -->
|
||||||
|
@if(user){
|
||||||
|
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-login">
|
||||||
|
<div class="px-4 py-3">
|
||||||
|
<span class="block text-sm text-gray-900 dark:text-white">Welcome, {{ user.firstname }} </span>
|
||||||
|
<span class="block text-sm text-gray-500 truncate dark:text-gray-400">{{ user.email }}</span>
|
||||||
|
</div>
|
||||||
|
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||||
|
<li>
|
||||||
|
<a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
||||||
|
>Create Listing</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="/myListings" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Listings</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="/emailUs" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">EMail Us</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-unknown">
|
||||||
|
<ul class="py-2" aria-labelledby="user-menu-button">
|
||||||
|
<li>
|
||||||
|
<a (click)="login()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Log In</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a (click)="register()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Register</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<button
|
||||||
|
data-collapse-toggle="navbar-user"
|
||||||
|
type="button"
|
||||||
|
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
|
||||||
|
aria-controls="navbar-user"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="sr-only">Open main menu</span>
|
||||||
|
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
|
||||||
|
<ul
|
||||||
|
class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:space-x-8 rtl:space-x-reverse md:flex-row md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLinkActive="active-link"
|
||||||
|
routerLink="/businessListings"
|
||||||
|
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/businessListings') }"
|
||||||
|
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
aria-current="page"
|
||||||
|
(click)="closeMenusAndSetCriteria('businessListings')"
|
||||||
|
>Businesses</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLinkActive="active-link"
|
||||||
|
routerLink="/commercialPropertyListings"
|
||||||
|
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/commercialPropertyListings') }"
|
||||||
|
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
(click)="closeMenusAndSetCriteria('commercialPropertyListings')"
|
||||||
|
>Properties</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
routerLinkActive="active-link"
|
||||||
|
routerLink="/brokerListings"
|
||||||
|
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/brokerListings') }"
|
||||||
|
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
|
||||||
|
(click)="closeMenusAndSetCriteria('brokerListings')"
|
||||||
|
>Professionals</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Mobile filter button -->
|
||||||
|
@if(isListingUrl()){
|
||||||
|
<div class="md:hidden flex justify-center pb-4">
|
||||||
|
<button
|
||||||
|
(click)="openModal()"
|
||||||
|
type="button"
|
||||||
|
id="filterDropdownMobileButton"
|
||||||
|
class="w-full mx-4 px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-blue-700 focus:ring-2 focus:ring-blue-700 focus:text-blue-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
||||||
|
>
|
||||||
|
<i class="fas fa-filter mr-2"></i>Filter ({{ getNumberOfFiltersSet() }})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
|||||||
@@ -1,115 +1,186 @@
|
|||||||
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||||
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { MenuItem } from 'primeng/api';
|
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { MenubarModule } from 'primeng/menubar';
|
import { filter, Observable, Subject, Subscription } from 'rxjs';
|
||||||
import { OverlayPanelModule } from 'primeng/overlaypanel';
|
|
||||||
import { TabMenuModule } from 'primeng/tabmenu';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||||
|
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, KeycloakUser, UserListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { CriteriaChangeService } from '../../services/criteria-change.service';
|
||||||
|
import { SearchService } from '../../services/search.service';
|
||||||
|
import { SharedService } from '../../services/shared.service';
|
||||||
import { UserService } from '../../services/user.service';
|
import { UserService } from '../../services/user.service';
|
||||||
|
import { compareObjects, createEmptyBusinessListingCriteria, createEmptyCommercialPropertyListingCriteria, createEmptyUserListingCriteria, getCriteriaProxy, map2User } from '../../utils/utils';
|
||||||
|
import { DropdownComponent } from '../dropdown/dropdown.component';
|
||||||
|
import { ModalService } from '../search-modal/modal.service';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'header',
|
selector: 'header',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, MenubarModule, ButtonModule, OverlayPanelModule, TabMenuModule],
|
imports: [CommonModule, RouterModule, DropdownComponent, FormsModule],
|
||||||
templateUrl: './header.component.html',
|
templateUrl: './header.component.html',
|
||||||
styleUrl: './header.component.scss',
|
styleUrl: './header.component.scss',
|
||||||
})
|
})
|
||||||
export class HeaderComponent {
|
export class HeaderComponent {
|
||||||
public buildVersion = environment.buildVersion;
|
public buildVersion = environment.buildVersion;
|
||||||
user$: Observable<User>;
|
user$: Observable<KeycloakUser>;
|
||||||
|
keycloakUser: KeycloakUser;
|
||||||
user: User;
|
user: User;
|
||||||
public tabItems: MenuItem[];
|
|
||||||
public menuItems: MenuItem[];
|
|
||||||
activeItem;
|
activeItem;
|
||||||
faUserGear = faUserGear;
|
faUserGear = faUserGear;
|
||||||
constructor(public userService: UserService, private router: Router) {}
|
profileUrl: string;
|
||||||
|
env = environment;
|
||||||
|
private filterDropdown: Dropdown | null = null;
|
||||||
|
isMobile: boolean = false;
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
prompt: string;
|
||||||
|
private subscription: Subscription;
|
||||||
|
criteria: BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria;
|
||||||
|
private routerSubscription: Subscription | undefined;
|
||||||
|
baseRoute: string;
|
||||||
|
constructor(
|
||||||
|
public keycloakService: KeycloakService,
|
||||||
|
private router: Router,
|
||||||
|
private userService: UserService,
|
||||||
|
private sharedService: SharedService,
|
||||||
|
private breakpointObserver: BreakpointObserver,
|
||||||
|
private modalService: ModalService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
private criteriaChangeService: CriteriaChangeService,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
async ngOnInit() {
|
||||||
this.user$ = this.userService.getUserObservable();
|
const token = await this.keycloakService.getToken();
|
||||||
this.user$.subscribe(u => {
|
this.keycloakUser = map2User(token);
|
||||||
this.user = u;
|
if (this.keycloakUser) {
|
||||||
this.menuItems = [
|
this.user = await this.userService.getByMail(this.keycloakUser?.email);
|
||||||
{
|
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||||
label: 'User Actions',
|
}
|
||||||
icon: 'fas fa-cog',
|
|
||||||
items: [
|
setTimeout(() => {
|
||||||
{
|
initFlowbite();
|
||||||
label: 'Account',
|
}, 10);
|
||||||
icon: 'pi pi-user',
|
|
||||||
routerLink: `/account`,
|
this.sharedService.currentProfilePhoto.subscribe(photoUrl => {
|
||||||
visible: this.isUserLogedIn(),
|
if (photoUrl) {
|
||||||
},
|
this.profileUrl = photoUrl;
|
||||||
{
|
}
|
||||||
label: 'Create Listing',
|
|
||||||
icon: 'pi pi-plus-circle',
|
|
||||||
routerLink: '/createBusinessListing',
|
|
||||||
visible: this.isUserLogedIn(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'My Listings',
|
|
||||||
icon: 'pi pi-list',
|
|
||||||
routerLink: '/myListings',
|
|
||||||
visible: this.isUserLogedIn(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'My Favorites',
|
|
||||||
icon: 'pi pi-star',
|
|
||||||
routerLink: '/myFavorites',
|
|
||||||
visible: this.isUserLogedIn(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'EMail Us',
|
|
||||||
icon: 'fa-regular fa-envelope',
|
|
||||||
routerLink: '/emailUs',
|
|
||||||
visible: this.isUserLogedIn(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Logout',
|
|
||||||
icon: 'fa-solid fa-right-from-bracket',
|
|
||||||
routerLink: '/logout',
|
|
||||||
visible: this.isUserLogedIn(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Login',
|
|
||||||
icon: 'fa-solid fa-right-from-bracket',
|
|
||||||
command: () => this.login(),
|
|
||||||
visible: !this.isUserLogedIn(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
});
|
});
|
||||||
this.tabItems = [
|
|
||||||
{
|
|
||||||
label: 'Businesses for Sale',
|
|
||||||
routerLink: '/businessListings',
|
|
||||||
state: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Commercial Property',
|
|
||||||
routerLink: '/commercialPropertyListings',
|
|
||||||
state: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Professionals/Brokers Directory',
|
|
||||||
routerLink: '/brokerListings',
|
|
||||||
state: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
this.activeItem = this.tabItems[0];
|
this.checkCurrentRoute(this.router.url);
|
||||||
|
|
||||||
|
this.routerSubscription = this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe((event: any) => {
|
||||||
|
this.checkCurrentRoute(event.urlAfterRedirects);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
private checkCurrentRoute(url: string): void {
|
||||||
|
this.baseRoute = url.split('/')[1]; // Nimmt den ersten Teil der Route nach dem ersten '/'
|
||||||
|
const specialRoutes = [, '', ''];
|
||||||
|
this.criteria = getCriteriaProxy(this.baseRoute, this);
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
|
// getCriteriaProxy(path:string):BusinessListingCriteria | CommercialPropertyListingCriteria | UserListingCriteria{
|
||||||
|
// if ('businessListings' === path) {
|
||||||
|
// return this.createEnhancedProxy(getCriteriaStateObject('business'));
|
||||||
|
// } else if ('commercialPropertyListings' === path) {
|
||||||
|
// return this.createEnhancedProxy(getCriteriaStateObject('commercialProperty'));
|
||||||
|
// } else if ('brokerListings' === path) {
|
||||||
|
// return this.createEnhancedProxy(getCriteriaStateObject('broker'));
|
||||||
|
// } else {
|
||||||
|
// return undefined;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// private createEnhancedProxy(obj: any) {
|
||||||
|
// const component = this;
|
||||||
|
|
||||||
|
// const sessionStorageHandler = function (path, value, previous, applyData) {
|
||||||
|
// let criteriaType = '';
|
||||||
|
// if ('/businessListings' === window.location.pathname) {
|
||||||
|
// criteriaType = 'business';
|
||||||
|
// } else if ('/commercialPropertyListings' === window.location.pathname) {
|
||||||
|
// criteriaType = 'commercialProperty';
|
||||||
|
// } else if ('/brokerListings' === window.location.pathname) {
|
||||||
|
// criteriaType = 'broker';
|
||||||
|
// }
|
||||||
|
// sessionStorage.setItem(`${criteriaType}_criteria`, JSON.stringify(this));
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return onChange(obj, function (path, value, previous, applyData) {
|
||||||
|
// // Call the original sessionStorageHandler
|
||||||
|
// sessionStorageHandler.call(this, path, value, previous, applyData);
|
||||||
|
|
||||||
|
// // Notify about the criteria change using the component's context
|
||||||
|
// component.criteriaChangeService.notifyCriteriaChange();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
ngAfterViewInit() {}
|
||||||
|
|
||||||
|
async openModal() {
|
||||||
|
const accepted = await this.modalService.showModal(this.criteria);
|
||||||
|
if (accepted) {
|
||||||
|
this.searchService.search(this.criteria);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
navigateWithState(dest: string, state: any) {
|
navigateWithState(dest: string, state: any) {
|
||||||
this.router.navigate([dest], { state: state });
|
this.router.navigate([dest], { state: state });
|
||||||
}
|
}
|
||||||
isUserLogedIn() {
|
|
||||||
return this.userService?.isLoggedIn();
|
|
||||||
}
|
|
||||||
login() {
|
login() {
|
||||||
this.userService.login(window.location.href);
|
this.keycloakService.login({
|
||||||
|
redirectUri: window.location.href,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
register() {
|
||||||
|
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||||
|
}
|
||||||
|
isActive(route: string): boolean {
|
||||||
|
return this.router.url === route;
|
||||||
|
}
|
||||||
|
isListingUrl(): boolean {
|
||||||
|
return ['/businessListings', '/commercialPropertyListings', '/brokerListings'].includes(this.router.url);
|
||||||
|
}
|
||||||
|
closeDropdown() {
|
||||||
|
const dropdownButton = document.getElementById('user-menu-button');
|
||||||
|
const dropdownMenu = this.user ? document.getElementById('user-login') : document.getElementById('user-unknown');
|
||||||
|
|
||||||
|
if (dropdownButton && dropdownMenu) {
|
||||||
|
const dropdown = new Dropdown(dropdownMenu, dropdownButton);
|
||||||
|
dropdown.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeMobileMenu() {
|
||||||
|
const targetElement = document.getElementById('navbar-user');
|
||||||
|
const triggerElement = document.querySelector('[data-collapse-toggle="navbar-user"]');
|
||||||
|
|
||||||
|
if (targetElement instanceof HTMLElement && triggerElement instanceof HTMLElement) {
|
||||||
|
const collapse = new Collapse(targetElement, triggerElement);
|
||||||
|
collapse.collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeMenusAndSetCriteria(path: string) {
|
||||||
|
this.closeDropdown();
|
||||||
|
this.closeMobileMenu();
|
||||||
|
const criteria = getCriteriaProxy(path, this);
|
||||||
|
criteria.page = 1;
|
||||||
|
criteria.start = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
getNumberOfFiltersSet() {
|
||||||
|
if (this.criteria?.criteriaType === 'brokerListings') {
|
||||||
|
return compareObjects(createEmptyUserListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||||
|
} else if (this.criteria?.criteriaType === 'businessListings') {
|
||||||
|
return compareObjects(createEmptyBusinessListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||||
|
} else if (this.criteria?.criteriaType === 'commercialPropertyListings') {
|
||||||
|
return compareObjects(createEmptyCommercialPropertyListingCriteria(), this.criteria, ['start', 'length', 'page', 'searchType', 'radius']);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!-- Modal -->
|
||||||
|
<div *ngIf="showModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
|
||||||
|
<div class="bg-white p-5 rounded-lg shadow-xl" style="width: 90%; max-width: 600px">
|
||||||
|
<h3 class="text-lg font-semibold mb-4">Crop Image</h3>
|
||||||
|
<image-cropper (loadImageFailed)="loadImageFailed()" [imageChangedEvent]="imageChangedEvent" [maintainAspectRatio]="false" format="png" (imageCropped)="imageCropped($event)"></image-cropper>
|
||||||
|
<div class="mt-4 flex justify-end">
|
||||||
|
<button (click)="closeModal()" class="mr-2 px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Cancel</button>
|
||||||
|
<button (click)="uploadImage()" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="file" #fileInput style="display: none" (change)="fileChangeEvent($event)" accept="image/*" />
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
::ng-deep image-cropper {
|
||||||
|
justify-content: center;
|
||||||
|
& > div {
|
||||||
|
width: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Component, ElementRef, Input, output, ViewChild } from '@angular/core';
|
||||||
|
import { ImageCroppedEvent, ImageCropperComponent } from 'ngx-image-cropper';
|
||||||
|
import { UploadParams } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
|
import { ImageService } from '../../services/image.service';
|
||||||
|
import { ListingsService } from '../../services/listings.service';
|
||||||
|
import { SharedModule } from '../../shared/shared/shared.module';
|
||||||
|
export interface UploadReponse {
|
||||||
|
success: boolean;
|
||||||
|
type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile';
|
||||||
|
}
|
||||||
|
@Component({
|
||||||
|
selector: 'app-image-crop-and-upload',
|
||||||
|
standalone: true,
|
||||||
|
imports: [SharedModule, ImageCropperComponent],
|
||||||
|
templateUrl: './image-crop-and-upload.component.html',
|
||||||
|
styleUrl: './image-crop-and-upload.component.scss',
|
||||||
|
})
|
||||||
|
export class ImageCropAndUploadComponent {
|
||||||
|
showModal = false;
|
||||||
|
imageChangedEvent: any = '';
|
||||||
|
croppedImage: Blob | null = null;
|
||||||
|
@Input() uploadParams: UploadParams;
|
||||||
|
uploadFinished = output<UploadReponse>();
|
||||||
|
@ViewChild('fileInput', { static: true }) fileInput!: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
|
constructor(private imageService: ImageService, private listingsService: ListingsService) {}
|
||||||
|
|
||||||
|
ngOnInit() {}
|
||||||
|
ngOnChanges() {
|
||||||
|
this.openFileDialog();
|
||||||
|
}
|
||||||
|
openFileDialog() {
|
||||||
|
if (this.uploadParams) {
|
||||||
|
this.fileInput.nativeElement.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileChangeEvent(event: any): void {
|
||||||
|
this.imageChangedEvent = event;
|
||||||
|
this.showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageCropped(event: ImageCroppedEvent) {
|
||||||
|
this.croppedImage = event.blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
this.imageChangedEvent = null;
|
||||||
|
this.croppedImage = null;
|
||||||
|
this.showModal = false;
|
||||||
|
this.fileInput.nativeElement.value = '';
|
||||||
|
this.uploadFinished.emit({ success: false, type: this.uploadParams.type });
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadImage() {
|
||||||
|
if (this.croppedImage) {
|
||||||
|
await this.imageService.uploadImage(this.croppedImage, this.uploadParams.type, this.uploadParams.imagePath, this.uploadParams.serialId);
|
||||||
|
this.closeModal();
|
||||||
|
this.uploadFinished.emit({ success: true, type: this.uploadParams.type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadImageFailed() {
|
||||||
|
console.error('Load image failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="cropperConfig"></angular-cropper>
|
|
||||||
<div class="flex justify-content-between mt-3">
|
|
||||||
@if(ratioVariable){
|
|
||||||
<div>
|
|
||||||
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)"
|
|
||||||
optionLabel="label" optionValue="value"></p-selectButton>
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div></div>
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true"></p-button>
|
|
||||||
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { Component, ViewChild } from '@angular/core';
|
|
||||||
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
|
|
||||||
import { LoadingService } from '../../services/loading.service';
|
|
||||||
import { ImageService } from '../../services/image.service';
|
|
||||||
import { HttpEventType } from '@angular/common/http';
|
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
|
||||||
import { SharedModule } from '../../shared/shared/shared.module';
|
|
||||||
import { SelectButtonModule } from 'primeng/selectbutton';
|
|
||||||
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
|
|
||||||
export const stateOptions:KeyValueRatio[]=[
|
|
||||||
{label:'16/9',value:16/9},
|
|
||||||
{label:'1/1',value:1},
|
|
||||||
{label:'2/3',value:2/3},
|
|
||||||
]
|
|
||||||
@Component({
|
|
||||||
selector: 'app-image-cropper',
|
|
||||||
standalone: true,
|
|
||||||
imports: [SharedModule,FileUploadModule,AngularCropperjsModule,SelectButtonModule],
|
|
||||||
templateUrl: './image-cropper.component.html',
|
|
||||||
styleUrl: './image-cropper.component.scss'
|
|
||||||
})
|
|
||||||
export class ImageCropperComponent {
|
|
||||||
@ViewChild(CropperComponent) public angularCropper: CropperComponent;
|
|
||||||
imageUrl:string; //wird im Template verwendet
|
|
||||||
fileUpload:FileUpload
|
|
||||||
value:number = stateOptions[0].value;
|
|
||||||
cropperConfig={aspectRatio: this.value}
|
|
||||||
ratioVariable:boolean
|
|
||||||
stateOptions=stateOptions
|
|
||||||
constructor(
|
|
||||||
private loadingService:LoadingService,
|
|
||||||
private imageUploadService: ImageService,
|
|
||||||
public config: DynamicDialogConfig,
|
|
||||||
public ref: DynamicDialogRef
|
|
||||||
){}
|
|
||||||
ngOnInit(): void {
|
|
||||||
if (this.config.data) {
|
|
||||||
this.imageUrl = this.config.data.imageUrl;
|
|
||||||
this.fileUpload = this.config.data.fileUpload;
|
|
||||||
this.cropperConfig = this.config.data.config ? this.config.data.config: this.cropperConfig;
|
|
||||||
this.ratioVariable = this.config.data.ratioVariable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sendImage(){
|
|
||||||
// setTimeout(()=>{
|
|
||||||
// this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => {
|
|
||||||
// this.ref.close(blob);
|
|
||||||
// this.fileUpload.clear()
|
|
||||||
// }, 'image/jpg');
|
|
||||||
// },0)
|
|
||||||
|
|
||||||
this.fileUpload.clear()
|
|
||||||
this.ref.close(this.angularCropper.cropper);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancelUpload(){
|
|
||||||
this.fileUpload.clear();
|
|
||||||
this.ref.close();
|
|
||||||
}
|
|
||||||
changeAspectRation(ratio:number){
|
|
||||||
this.cropperConfig={aspectRatio: ratio}
|
|
||||||
this.angularCropper.cropper.setAspectRatio(ratio);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
// @layer primeng {
|
|
||||||
app-inputnumber,
|
|
||||||
.p-inputnumber {
|
|
||||||
display: inline-flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-stacked .p-button.p-inputnumber-button .p-button-label,
|
|
||||||
.p-inputnumber-buttons-horizontal .p-button.p-inputnumber-button .p-button-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-stacked .p-button.p-inputnumber-button-up {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-stacked .p-inputnumber-input {
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-stacked .p-button.p-inputnumber-button-down {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-stacked .p-inputnumber-button-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-stacked .p-inputnumber-button-group .p-button.p-inputnumber-button {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-horizontal .p-button.p-inputnumber-button-up {
|
|
||||||
order: 3;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-horizontal .p-inputnumber-input {
|
|
||||||
order: 2;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-horizontal .p-button.p-inputnumber-button-down {
|
|
||||||
order: 1;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-vertical {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-vertical .p-button.p-inputnumber-button-up {
|
|
||||||
order: 1;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-vertical .p-inputnumber-input {
|
|
||||||
order: 2;
|
|
||||||
border-radius: 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-buttons-vertical .p-button.p-inputnumber-button-down {
|
|
||||||
order: 3;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-input {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-fluid app-inputnumber,
|
|
||||||
.p-fluid .p-inputnumber {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-fluid .p-inputnumber .p-inputnumber-input {
|
|
||||||
width: 1%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-fluid .p-inputnumber-buttons-vertical .p-inputnumber-input {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-clear-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
margin-top: -0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.p-inputnumber-clearable {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,17 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { UserService } from '../../services/user.service';
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'logout',
|
selector: 'logout',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule,RouterModule],
|
imports: [CommonModule, RouterModule],
|
||||||
template:``
|
template: ``,
|
||||||
})
|
})
|
||||||
export class LogoutComponent {
|
export class LogoutComponent {
|
||||||
constructor(private userService:UserService){
|
constructor(public keycloakService: KeycloakService) {
|
||||||
userService.logout();
|
sessionStorage.removeItem('USERID');
|
||||||
|
keycloakService.logout(window.location.origin + '/home');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { MessageComponent } from './message.component';
|
||||||
|
import { Message, MessageService } from './message.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-message-container',
|
||||||
|
standalone: true,
|
||||||
|
imports: [CommonModule, MessageComponent],
|
||||||
|
template: `
|
||||||
|
<div class="fixed top-5 right-5 z-50 flex flex-col items-end">
|
||||||
|
<app-message *ngFor="let message of messages" [message]="message" (close)="removeMessage(message)"> </app-message>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
export class MessageContainerComponent implements OnInit {
|
||||||
|
messages: Message[] = [];
|
||||||
|
|
||||||
|
constructor(private messageService: MessageService) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.messageService.messages$.subscribe(messages => {
|
||||||
|
this.messages = messages;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeMessage(message: Message): void {
|
||||||
|
this.messageService.removeMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user