71 Commits

Author SHA1 Message Date
69b0a83b1e location eingebaut, back buttons angeglichen, paging & criteria korrigiert 2024-08-08 17:25:00 +02:00
7df5d32cc4 Umbau zu location / companyLocation 2024-08-07 19:00:34 +02:00
1a77656d8a fix missing id 2024-08-07 13:33:22 +02:00
b1f26fbf48 set user.companyLocation = {}; 2024-08-07 13:31:24 +02:00
3795a5a30c validatedCity, mask for input, confirmatonService, Version Info, 2024-08-07 13:11:26 +02:00
8698aa3e66 Paket Aktualisierung 2024-08-05 19:04:00 +02:00
398f8d29ca bugFixes 2024-08-05 18:54:03 +02:00
8b71b073be update message component, bugfixes 2024-08-05 13:31:32 +02:00
4c1b1fbc87 Einbau Validation finished 2024-08-03 12:16:04 +02:00
f58448679d conversion to components part 2 2024-08-02 20:18:19 +02:00
c9305749d2 conversion inputs to components 2024-08-02 20:14:07 +02:00
29f88d610f Validierung Part II, neue Komponenten 2024-08-01 22:43:32 +02:00
2955c034a0 Validation first Part 2024-07-30 23:23:25 +02:00
55e800009e landing page finished 2024-07-29 21:23:26 +02:00
6348af8862 home1 removed 2024-07-26 19:19:37 +02:00
a6ae643458 homepage overhault, aiService 1. try 2024-07-26 19:18:28 +02:00
38e943c18e location radius search 2024-07-24 16:16:25 +02:00
acec14d372 reset criteria on home, show filter on home, new BOM generation, Schema overhaul 2024-07-23 20:46:38 +02:00
9db23c2177 counties, pagination, filter count, show total results 2024-07-19 18:06:56 +02:00
abcde3991d Paginator & SQL Querries where clauses & city search 2024-07-18 19:02:32 +02:00
f88eebe8d3 Criteria Objekt überarbeitet 2024-07-17 20:19:13 +02:00
bdafb03165 Einbau klassische Filter als Overlay ... 2024-07-16 17:09:59 +02:00
af982d19d8 First version AI Search 2024-07-15 19:37:35 +02:00
b7b34dacab add embeddings, remove userId from business & commercialProperty replaced by email 2024-07-13 19:44:07 +02:00
bf4bd69337 changes to imports & import Embeddings 2024-07-12 22:00:29 +02:00
b4644ea295 Fehlerbehebung & Start Vector Search 2024-07-11 17:09:35 +02:00
7bd5e1aaf8 account, myListings and emailUs pages 2024-07-10 18:40:46 +02:00
08c179fa09 account Komponente 2024-07-09 21:19:51 +02:00
7f67b81242 editCommercialProps, confirmationService, MessageService, Drag & Drop 2024-07-09 14:32:20 +02:00
e0dbebb61c Umbau commercial-edit & user-details 2024-07-05 17:39:01 +02:00
677b95c21c Umbau commercial-details, Anfang Umbau user-details 2024-07-05 14:00:05 +02:00
1534c14a68 Umbau broker listing 2024-07-04 20:43:07 +02:00
5fa2dd60fa Update Angular 18, ng-select, quill editor, ngx-currency, Umbau business-detail, edit-business, commercial-lsitings, remove ng-prime 2024-07-04 17:51:35 +02:00
9228cbebbe Umbau business-details & edit-business 2024-07-04 14:59:02 +02:00
1ccd1d174c Umbau: header & businessListing 2024-07-03 18:51:01 +02:00
958f0afd9b Umbau zu tailwind + mobile friendly: LandingPage & Footer 2024-07-02 19:17:14 +02:00
3d5b7e3f39 account changes 2024-06-03 09:35:42 -05:00
d488f90f48 BugFix: #38 EMail Us 2024-05-29 17:03:23 -05:00
044f8efa0f Bug Fixed: #34,#28,#48,#46,#2,#15 2024-05-29 12:41:55 -05:00
24a3d210f0 renew ts value, better logging JWT 2024-05-28 17:26:45 -05:00
2465b8966b add more logging 2024-05-28 14:41:26 -05:00
e87222d3c1 findByImagePath: do not check for draft because it's internally ... 2024-05-28 14:19:32 -05:00
902ab9caed acc. draft mode, take care of ADMIN role or on own listings 2024-05-28 13:15:31 -05:00
44acbcd4d0 safe if user===undefined 2024-05-28 11:55:26 -05:00
b4cf17b8ea Draft Mode inkl. Token implementiert 2024-05-28 11:30:00 -05:00
226d2ebc1e Auth Token Übersendung eingebaut 2024-05-27 18:02:47 -05:00
0473f74241 Issues #47,#42, #35 resolved, request logging erweitert 2024-05-24 18:15:33 -05:00
c9d94e973a Fix for Issue #45 2024-05-24 10:52:11 -05:00
f9d9c6ad9e image path adjusted 2024-05-23 18:39:25 -05:00
5dc893da38 imagePath changed 2024-05-23 18:09:54 -05:00
c471629c6d add serialId 2024-05-22 17:36:49 -05:00
13fb3cd4b8 Schema changed for commercials 2024-05-22 17:34:50 -05:00
d6768b3da9 waiting for initialization 2024-05-22 13:32:17 -05:00
7fdc87fb0b flow: 'implicit' 2024-05-22 11:40:49 -05:00
0b7e33612a new authorization approach 2024-05-22 11:05:40 -05:00
8fba3aa832 authGuard acc. lejdiprifti.com 2024-05-22 09:31:31 -05:00
214327031c Bug Fixes 2024-05-20 17:52:05 -05:00
dc9adb151d new initialization process, keycloak update 24.0.4 2024-05-20 15:54:01 -05:00
747435bfba first validation 2024-05-17 16:46:17 -05:00
782c254a33 --output-hashing=all added to build 2024-05-17 15:33:19 -05:00
df4e2b00e2 Issue fixing + deletion of profile & logo 2024-05-17 14:50:50 -05:00
0684b9534f Kriterium name reseted 2024-05-16 16:45:33 -05:00
e0ecea5af2 fix further bugs 2024-05-16 15:57:39 -05:00
327aef0f21 add version.js 2024-05-16 13:32:42 -05:00
cb73daf863 Version Info 2024-05-16 13:20:42 -05:00
08c53e2eb2 raw template 2024-05-16 12:14:56 -05:00
492c03c2be BugFix deletion of prop images 2024-05-16 10:08:23 -05:00
f51a298227 Bugfixes 2024-05-15 17:35:04 -05:00
474d7c63d5 timestamp based images 2024-05-14 15:28:08 -05:00
d2e5562602 history service, mail template improved, general listing entry 2024-05-14 11:53:20 -05:00
aff55c5433 BugFixes image upload, image display, new DB structure for areasServed, licenedIn 2024-05-13 17:31:01 -05:00
209 changed files with 438312 additions and 127743 deletions

4
.gitignore vendored
View File

@@ -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

View 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',
},
};

View File

@@ -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

View File

@@ -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,
},
] ]
} }

View File

@@ -698,7 +698,7 @@
"realEstateIncluded": true, "realEstateIncluded": true,
"franchiseResale": false, "franchiseResale": false,
"draft": false, "draft": false,
"internals": "", "internals": null,
"created": "2023-11-18T13:00:00.000Z" "created": "2023-11-18T13:00:00.000Z"
}, },
{ {
@@ -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,

View File

@@ -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
View 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,
}

View File

@@ -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,
});

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}, },

View 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);
}
}

View 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 {}

View 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');
}
}
}

View File

@@ -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';
} }
} }

View File

@@ -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 {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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 {}

View File

@@ -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;
} }
} }

View File

@@ -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);
} }

View File

@@ -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();

View 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 $$;

View File

@@ -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 $$;

View File

@@ -1 +0,0 @@
ALTER TABLE "commercials" ALTER COLUMN "imageOrder" SET DATA TYPE varchar(200)[];

View File

@@ -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";

View File

@@ -1 +0,0 @@
ALTER TABLE "commercials" ADD COLUMN "listingsCategory" varchar(255);

View File

@@ -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": {},

View File

@@ -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": {}
}
}

View File

@@ -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": {}
}
}

View File

@@ -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": {}
}
}

View File

@@ -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
} }
] ]

View File

@@ -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 }),
}); });

View 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);

View File

@@ -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);
}
}
} }

View File

@@ -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);
}
} }

View File

@@ -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);
}
}

View File

@@ -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}`);
} }
} }

View File

@@ -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 {}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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);
} }
} }

View 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`);
}
}

View File

@@ -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();
} }
} }

View File

@@ -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);
} }
} }

View 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`);
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
} }

View File

@@ -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);
}
} }
} }

View File

@@ -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 {}

View File

@@ -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;
} }
} }

View File

@@ -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>

View 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>

View File

@@ -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);

View File

@@ -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>;

View File

@@ -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',
};
}

View File

@@ -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[];
}

View File

@@ -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();
} }

View File

@@ -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,
};
}
} }

View File

@@ -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' },

View File

@@ -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();
}
}

View File

@@ -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;
}
} }

View File

@@ -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 {}

View File

@@ -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 };
} }
} }

View File

@@ -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;
}

View 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
View 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}`);
}
});

View File

@@ -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"
} }
} }

View File

@@ -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>

View File

@@ -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 */
// }

View File

@@ -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' });
}
} }

View File

@@ -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}`);
};
} }

View File

@@ -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

View File

@@ -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 {}
}

View File

@@ -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) {}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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;
};
}

View File

@@ -0,0 +1 @@
<p>dropdown works!</p>

View 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

View File

@@ -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;
} }

View File

@@ -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();
}
});
}
} }

View File

@@ -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>

View File

@@ -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;
}
} }
} }

View File

@@ -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/*" />

View File

@@ -0,0 +1,6 @@
::ng-deep image-cropper {
justify-content: center;
& > div {
width: unset !important;
}
}

View File

@@ -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');
}
}

View File

@@ -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>

View File

@@ -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);
}
}

View File

@@ -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;
}
// }

View File

@@ -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');
} }
} }

View File

@@ -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