16 Commits

70 changed files with 1165 additions and 804 deletions

3
.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

View File

@@ -56,3 +56,4 @@ 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

View File

@@ -6,17 +6,15 @@
"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",
@@ -24,9 +22,7 @@
"name": "Debug Current TS File", "name": "Debug Current TS File",
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js", "program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
"preLaunchTask": "tsc: build - tsconfig.json", "preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [ "outFiles": ["${workspaceFolder}/out/**/*.js"],
"${workspaceFolder}/out/**/*.js"
],
"sourceMaps": true, "sourceMaps": true,
"smartStep": true, "smartStep": true,
"internalConsoleOptions": "openOnSessionStart" "internalConsoleOptions": "openOnSessionStart"
@@ -35,31 +31,21 @@
"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": [ "skipFiles": ["<node_internals>/**"],
"<node_internals>/**"
],
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js", "program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
"outFiles": [ "outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
"${workspaceFolder}/dist/src/drizzle/**/*.js"
],
"sourceMaps": true, "sourceMaps": true,
"smartStep": true, "smartStep": true
}
},
] ]
} }

View File

@@ -9,10 +9,10 @@
"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",
@@ -37,7 +37,9 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"drizzle-orm": "^0.30.8", "drizzle-orm": "^0.30.8",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"jwks-rsa": "^3.1.0",
"ky": "^1.2.0", "ky": "^1.2.0",
"nest-winston": "^1.9.4", "nest-winston": "^1.9.4",
"nodemailer": "^6.9.10", "nodemailer": "^6.9.10",

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,5 +1,8 @@
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 path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
@@ -14,12 +17,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 { UserModule } from './user/user.module.js'; import { UserModule } from './user/user.module.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
function loadEnvFiles() {
// Load the .env file
dotenv.config();
console.log('Loaded .env file');
// Determine which additional env file to load
let envFilePath = '';
const host = process.env.HOST_NAME || '';
if (host.includes('localhost')) {
envFilePath = '.env.local';
} else if (host.includes('dev.bizmatch.net')) {
envFilePath = '.env.dev';
} else if (host.includes('www.bizmatch.net') || host.includes('bizmatch.net')) {
envFilePath = '.env.prod';
}
// Load the additional env file if it exists
if (fs.existsSync(envFilePath)) {
dotenv.config({ path: envFilePath });
console.log(`Loaded ${envFilePath} file`);
} else {
console.log(`No additional .env file found for HOST_NAME: ${host}`);
}
}
loadEnvFiles();
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ isGlobal: true }), ConfigModule.forRoot({ isGlobal: true }),
@@ -46,6 +74,7 @@ const __dirname = path.dirname(__filename);
ListingsModule, ListingsModule,
SelectOptionsModule, SelectOptionsModule,
ImageModule, ImageModule,
PassportModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService, FileService], providers: [AppService, FileService],

View File

@@ -1,17 +1,16 @@
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 path from 'path';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { AuthService } from './auth.service.js'; import { JwtStrategy } from '../jwt.strategy.js';
import { AuthController } from './auth.controller.js'; import { AuthController } from './auth.controller.js';
import { AuthService } from './auth.service.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); 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,13 @@
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, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
import fs from 'fs-extra';
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, UserData } from 'src/models/db.model.js'; import { BusinessListing, CommercialPropertyListing, User, UserData } from 'src/models/db.model.js';
import { emailToDirName } from 'src/models/main.model.js';
import * as schema from './schema.js'; import * as schema from './schema.js';
const { Pool } = pkg; const { Pool } = pkg;
@@ -32,6 +34,11 @@ const targetPathProfile = `./pictures/profile`;
deleteFilesOfDir(targetPathProfile); deleteFilesOfDir(targetPathProfile);
const targetPathLogo = `./pictures/logo`; const targetPathLogo = `./pictures/logo`;
deleteFilesOfDir(targetPathLogo); deleteFilesOfDir(targetPathLogo);
const targetPathProperty = `./pictures/property`;
deleteFilesOfDir(targetPathProperty);
fs.ensureDirSync(`./pictures/logo`);
fs.ensureDirSync(`./pictures/profile`);
fs.ensureDirSync(`./pictures/property`);
for (const userData of usersData) { for (const userData of usersData) {
const user: User = { firstname: '', lastname: '', email: '' }; const user: User = { firstname: '', lastname: '', email: '' };
user.licensedIn = []; user.licensedIn = [];
@@ -58,20 +65,21 @@ for (const userData of usersData) {
user.gender = userData.gender; user.gender = userData.gender;
user.created = new Date(); user.created = new Date();
user.updated = new Date(); user.updated = new Date();
const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender }); const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email });
generatedUserData.push(u[0].insertedId); generatedUserData.push(u[0]);
i++; i++;
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_${female}.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 //Business Listings
filePath = `./data/businesses.json`; filePath = `./data/businesses.json`;
@@ -82,7 +90,9 @@ for (const business of businessJsonData) {
delete business.id; delete business.id;
business.created = new Date(business.created); business.created = new Date(business.created);
business.updated = new Date(business.created); business.updated = new Date(business.created);
business.userId = getRandomItem(generatedUserData); const user = getRandomItem(generatedUserData);
business.userId = user.insertedId;
business.imageName = emailToDirName(user.email);
await db.insert(schema.businesses).values(business); await db.insert(schema.businesses).values(business);
} }
//Corporate Listings //Corporate Listings
@@ -92,14 +102,21 @@ const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // E
for (const commercial of commercialJsonData) { for (const commercial of commercialJsonData) {
const id = commercial.id; const id = commercial.id;
delete commercial.id; delete commercial.id;
const user = getRandomItem(generatedUserData);
commercial.imageOrder = getFilenames(id); commercial.imageOrder = getFilenames(id);
commercial.imagePath = id; commercial.imagePath = emailToDirName(user.email);
const insertionDate = getRandomDateWithinLastYear(); const insertionDate = getRandomDateWithinLastYear();
commercial.created = insertionDate; commercial.created = insertionDate;
commercial.updated = insertionDate; commercial.updated = insertionDate;
commercial.userId = getRandomItem(generatedUserData); commercial.userId = user.insertedId;
await db.insert(schema.commercials).values(commercial); commercial.draft = false;
const result = await db.insert(schema.commercials).values(commercial).returning();
//fs.ensureDirSync(`./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
try {
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
} catch (err) {
console.log(`----- No pictures available for ${id} ------`);
}
} }
//End //End
@@ -115,7 +132,7 @@ 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 null;
@@ -141,14 +158,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

@@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS "businesses" (
"reasonForSale" varchar(255), "reasonForSale" varchar(255),
"brokerLicencing" varchar(255), "brokerLicencing" varchar(255),
"internals" text, "internals" text,
"imagePath" varchar(200),
"created" timestamp, "created" timestamp,
"updated" timestamp, "updated" timestamp,
"visits" integer, "visits" integer,
@@ -36,6 +37,7 @@ CREATE TABLE IF NOT EXISTS "businesses" (
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE IF NOT EXISTS "commercials" ( CREATE TABLE IF NOT EXISTS "commercials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"serial_id" serial NOT NULL,
"userId" uuid, "userId" uuid,
"type" integer, "type" integer,
"title" varchar(255), "title" varchar(255),
@@ -53,7 +55,7 @@ CREATE TABLE IF NOT EXISTS "commercials" (
"website" varchar(255), "website" varchar(255),
"phoneNumber" varchar(255), "phoneNumber" varchar(255),
"imageOrder" varchar(200)[], "imageOrder" varchar(200)[],
"imagePath" varchar(50), "imagePath" varchar(200),
"created" timestamp, "created" timestamp,
"updated" timestamp, "updated" timestamp,
"visits" integer, "visits" integer,
@@ -76,7 +78,9 @@ CREATE TABLE IF NOT EXISTS "users" (
"hasProfile" boolean, "hasProfile" boolean,
"hasCompanyLogo" boolean, "hasCompanyLogo" boolean,
"licensedIn" jsonb, "licensedIn" jsonb,
"gender" "gender" "gender" "gender",
"created" timestamp,
"updated" timestamp
); );
--> statement-breakpoint --> statement-breakpoint
DO $$ BEGIN DO $$ BEGIN

View File

@@ -1,2 +0,0 @@
ALTER TABLE "users" ADD COLUMN "created" timestamp;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "updated" timestamp;

View File

@@ -0,0 +1,7 @@
DO $$ BEGIN
CREATE TYPE "customerType" AS ENUM('buyer', 'broker', 'professional');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "customerType" "customerType";

View File

@@ -1,5 +1,5 @@
{ {
"id": "98e2be90-3301-49a8-b323-78d9d8f79cb5", "id": "fc58c59b-ac5c-406e-8fdb-b05de40aed17",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "5", "version": "5",
"dialect": "pg", "dialect": "pg",
@@ -147,6 +147,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": { "created": {
"name": "created", "name": "created",
"type": "timestamp", "type": "timestamp",
@@ -202,6 +208,12 @@
"notNull": true, "notNull": true,
"default": "gen_random_uuid()" "default": "gen_random_uuid()"
}, },
"serial_id": {
"name": "serial_id",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"userId": { "userId": {
"name": "userId", "name": "userId",
"type": "uuid", "type": "uuid",
@@ -306,7 +318,7 @@
}, },
"imagePath": { "imagePath": {
"name": "imagePath", "name": "imagePath",
"type": "varchar(50)", "type": "varchar(200)",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
@@ -454,6 +466,18 @@
"type": "gender", "type": "gender",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
} }
}, },
"indexes": {}, "indexes": {},

View File

@@ -1,6 +1,6 @@
{ {
"id": "41802273-1335-433f-97cb-77774ddb3362", "id": "0bc02618-4414-4e90-8c44-808737611da7",
"prevId": "98e2be90-3301-49a8-b323-78d9d8f79cb5", "prevId": "fc58c59b-ac5c-406e-8fdb-b05de40aed17",
"version": "5", "version": "5",
"dialect": "pg", "dialect": "pg",
"tables": { "tables": {
@@ -147,6 +147,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": { "created": {
"name": "created", "name": "created",
"type": "timestamp", "type": "timestamp",
@@ -202,6 +208,12 @@
"notNull": true, "notNull": true,
"default": "gen_random_uuid()" "default": "gen_random_uuid()"
}, },
"serial_id": {
"name": "serial_id",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"userId": { "userId": {
"name": "userId", "name": "userId",
"type": "uuid", "type": "uuid",
@@ -306,7 +318,7 @@
}, },
"imagePath": { "imagePath": {
"name": "imagePath", "name": "imagePath",
"type": "varchar(50)", "type": "varchar(200)",
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
@@ -455,6 +467,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": false "notNull": false
}, },
"customerType": {
"name": "customerType",
"type": "customerType",
"primaryKey": false,
"notNull": false
},
"created": { "created": {
"name": "created", "name": "created",
"type": "timestamp", "type": "timestamp",
@@ -475,6 +493,14 @@
} }
}, },
"enums": { "enums": {
"customerType": {
"name": "customerType",
"values": {
"buyer": "buyer",
"broker": "broker",
"professional": "professional"
}
},
"gender": { "gender": {
"name": "gender", "name": "gender",
"values": { "values": {

View File

@@ -5,15 +5,15 @@
{ {
"idx": 0, "idx": 0,
"version": "5", "version": "5",
"when": 1715627517508, "when": 1716495198537,
"tag": "0000_open_hannibal_king", "tag": "0000_burly_bruce_banner",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 1, "idx": 1,
"version": "5", "version": "5",
"when": 1715631674334, "when": 1717085220861,
"tag": "0001_charming_thundra", "tag": "0001_wet_mephistopheles",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -1,8 +1,10 @@
import { boolean, char, doublePrecision, integer, jsonb, 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 'src/models/db.model'; import { AreasServed, LicensedIn } from 'src/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', 'broker', 'professional']);
export const users = pgTable('users', { export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
firstname: varchar('firstname', { length: 255 }).notNull(), firstname: varchar('firstname', { length: 255 }).notNull(),
@@ -20,6 +22,7 @@ export const users = pgTable('users', {
hasCompanyLogo: boolean('hasCompanyLogo'), hasCompanyLogo: boolean('hasCompanyLogo'),
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(), licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
gender: genderEnum('gender'), gender: genderEnum('gender'),
customerType: customerTypeEnum('customerType'),
created: timestamp('created'), created: timestamp('created'),
updated: timestamp('updated'), updated: timestamp('updated'),
}); });
@@ -48,6 +51,7 @@ 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('imagePath', { length: 200 }),
created: timestamp('created'), created: timestamp('created'),
updated: timestamp('updated'), updated: timestamp('updated'),
visits: integer('visits'), visits: integer('visits'),
@@ -56,6 +60,7 @@ export const businesses = pgTable('businesses', {
export const commercials = pgTable('commercials', { export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(), id: uuid('id').primaryKey().defaultRandom(),
serialId: serial('serial_id'),
userId: uuid('userId').references(() => users.id), userId: uuid('userId').references(() => users.id),
type: integer('type'), type: integer('type'),
title: varchar('title', { length: 255 }), title: varchar('title', { length: 255 }),
@@ -73,7 +78,7 @@ export const commercials = pgTable('commercials', {
website: varchar('website', { length: 255 }), website: varchar('website', { length: 255 }),
phoneNumber: varchar('phoneNumber', { 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'), visits: integer('visits'),

View File

@@ -20,6 +20,9 @@ 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');
@@ -28,36 +31,43 @@ export class FileService {
getSubscriptions(): Subscription[] { 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
// ############
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
const result: string[] = []; const result: string[] = [];
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);
files.forEach(f => { files.forEach(f => {
@@ -68,9 +78,9 @@ export class FileService {
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;
@@ -78,15 +88,18 @@ export class FileService {
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);
@@ -115,22 +128,6 @@ export class FileService {
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) {
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) { deleteImage(path: string) {
fs.unlinkSync(path); fs.unlinkSync(path);
} }

View File

@@ -1,4 +1,4 @@
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';
@@ -6,9 +6,6 @@ import { FileService } from '../file/file.service.js';
import { ListingsService } from '../listings/listings.service.js'; import { ListingsService } from '../listings/listings.service.js';
import { SelectOptionsService } from '../select-options/select-options.service.js'; import { SelectOptionsService } from '../select-options/select-options.service.js';
import { commercials } from '../drizzle/schema.js';
import { CommercialPropertyListing } from '../models/db.model.js';
@Controller('image') @Controller('image')
export class ImageController { export class ImageController {
constructor( constructor(
@@ -17,58 +14,42 @@ export class ImageController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
private selectOptions: SelectOptionsService, private selectOptions: SelectOptionsService,
) {} ) {}
// ############
@Post('uploadPropertyPicture/:imagePath') // Property
// ############
@Post('uploadPropertyPicture/:imagePath/:serial')
@UseInterceptors(FileInterceptor('file')) @UseInterceptors(FileInterceptor('file'))
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string) { async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
const imagename = await this.fileService.storePropertyPicture(file, imagePath); const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
await this.listingService.addImage(imagePath, 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/:imagePath/:imagename')
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('imagename') imagename: string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${imagePath}/${imagename}`);
}
@Delete('logo/:userid/')
async deleteLogoImagesById(@Param('userid') userid: string): Promise<any> {
this.fileService.deleteImage(`pictures/logo/${userid}.avif`);
}
@Delete('profile/:userid/')
async deleteProfileImagesById(@Param('userid') userid: string): Promise<any> {
this.fileService.deleteImage(`pictures/profile/${userid}.avif`);
} }
} }

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, 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,8 +1,9 @@
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 { businesses } from '../drizzle/schema.js'; import { businesses } from '../drizzle/schema.js';
import { ListingCriteria } from '../models/main.model.js'; import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
import { JwtUser, ListingCriteria } from '../models/main.model.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
@Controller('listings/business') @Controller('listings/business')
@@ -12,18 +13,22 @@ export class BusinessListingsController {
@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);
} }
@UseGuards(OptionalJwtAuthGuard)
@Get('user/:userid')
findByUserId(@Request() req, @Param('userid') userid: string): any {
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
}
@UseGuards(OptionalJwtAuthGuard)
@Post('search') @Post('search')
find(@Body() criteria: ListingCriteria): any { find(@Request() req, @Body() criteria: ListingCriteria): any {
return this.listingsService.findListingsByCriteria(criteria, businesses); return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
} }
@Post() @Post()
@@ -34,7 +39,7 @@ export class BusinessListingsController {
@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) {

View File

@@ -1,9 +1,11 @@
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 { CommercialPropertyListing } from 'src/models/db.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { commercials } from '../drizzle/schema.js'; 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 { JwtUser, ListingCriteria } from '../models/main.model.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
@Controller('listings/commercialProperty') @Controller('listings/commercialProperty')
@@ -14,17 +16,21 @@ export class CommercialPropertyListingsController {
@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);
} }
@UseGuards(OptionalJwtAuthGuard)
@Post('search') @Post('search')
async find(@Body() criteria: ListingCriteria): Promise<any> { async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> {
return await this.listingsService.findListingsByCriteria(criteria, commercials); return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
} }
@Get('states/all') @Get('states/all')
getStates(): any { getStates(): any {
@@ -38,16 +44,11 @@ export class CommercialPropertyListingsController {
@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/:imagePath') @Delete(':id/:imagePath')
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) { deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
this.listingsService.deleteListing(id, commercials); this.listingsService.deleteListing(id, commercials);
this.fileService.deleteDirectoryIfExists(imagePath); this.fileService.deleteDirectoryIfExists(imagePath);
} }
@Put('imageOrder/:id')
async changeImageOrder(@Param('id') id: string, @Body() imageOrder: string[]) {
this.listingsService.updateImageOrder(id, imageOrder);
}
} }

View File

@@ -1,4 +1,5 @@
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';
@@ -9,7 +10,7 @@ import { ListingsService } from './listings.service.js';
import { UnknownListingsController } from './unknown-listings.controller.js'; import { UnknownListingsController } from './unknown-listings.controller.js';
@Module({ @Module({
imports: [DrizzleModule], imports: [DrizzleModule, AuthModule],
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController], controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
providers: [ListingsService, FileService, UserService], providers: [ListingsService, FileService, UserService],
exports: [ListingsService], exports: [ListingsService],

View File

@@ -1,20 +1,22 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { and, eq, gte, ilike, lte, sql } from 'drizzle-orm'; import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js'; import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as schema from '../drizzle/schema.js'; import * as schema from '../drizzle/schema.js';
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js'; import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
import { ListingCriteria } from '../models/main.model.js'; import { FileService } from '../file/file.service.js';
import { JwtUser, ListingCriteria, emailToDirName } from '../models/main.model.js';
@Injectable() @Injectable()
export class ListingsService { export class ListingsService {
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 getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials): any[] { private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
const conditions = []; const conditions = [];
if (criteria.type) { if (criteria.type) {
conditions.push(eq(table.type, criteria.type)); conditions.push(eq(table.type, criteria.type));
@@ -39,46 +41,96 @@ export class ListingsService {
// ############################################################## // ##############################################################
// Listings general // Listings general
// ############################################################## // ##############################################################
async findListingsByCriteria(criteria: ListingCriteria, table: typeof businesses | typeof commercials): Promise<{ data: Record<string, any>[]; total: number }> {
async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
const start = criteria.start ? criteria.start : 0; const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12; const length = criteria.length ? criteria.length : 12;
return await this.findListings(table, criteria, start, length); const conditions = this.getConditions(criteria, commercials, user);
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
conditions.push(or(eq(commercials.draft, false), eq(commercials.imagePath, emailToDirName(user?.username))));
} }
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([ const [data, total] = await Promise.all([
this.conn this.conn
.select() .select()
.from(table) .from(commercials)
.where(and(...conditions)) .where(and(...conditions))
.offset(start) .offset(start)
.limit(length), .limit(length),
this.conn this.conn
.select({ count: sql`count(*)` }) .select({ count: sql`count(*)` })
.from(table) .from(commercials)
.where(and(...conditions)) .where(and(...conditions))
.then(result => Number(result[0].count)), .then(result => Number(result[0].count)),
]); ]);
return { total, data }; return { total, data };
} }
async findById(id: string, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> { async findBusinessListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
const result = await this.conn const start = criteria.start ? criteria.start : 0;
.select() const length = criteria.length ? criteria.length : 12;
.from(table) const conditions = this.getConditions(criteria, businesses, user);
.where(sql`${table.id} = ${id}`); if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
return result[0] as BusinessListing | CommercialPropertyListing; conditions.push(or(eq(businesses.draft, false), eq(businesses.imageName, emailToDirName(user?.username))));
} }
async findByImagePath(imagePath: string): Promise<CommercialPropertyListing> { const [data, total] = await Promise.all([
this.conn
.select()
.from(businesses)
.where(and(...conditions))
.offset(start)
.limit(length),
this.conn
.select({ count: sql`count(*)` })
.from(businesses)
.where(and(...conditions))
.then(result => Number(result[0].count)),
]);
return { total, data };
}
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 result[0] as CommercialPropertyListing;
}
async findBusinessesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
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 result[0] as BusinessListing;
}
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));
}
return (await this.conn
.select()
.from(commercials)
.where(and(...conditions))) as CommercialPropertyListing[];
}
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));
}
return (await this.conn
.select()
.from(businesses)
.where(and(...conditions))) as CommercialPropertyListing[];
}
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
const result = await this.conn const result = await this.conn
.select() .select()
.from(commercials) .from(commercials)
.where(sql`${commercials.imagePath} = ${imagePath}`); .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
return result[0] as CommercialPropertyListing; return result[0] as 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> { async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
data.created = new Date(); data.created = new Date();
data.updated = new Date(); data.updated = new Date();
@@ -86,13 +138,24 @@ export class ListingsService {
return createdListing as BusinessListing | CommercialPropertyListing; return createdListing as BusinessListing | CommercialPropertyListing;
} }
async updateListing(id: string, data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> { async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
data.updated = new Date(); data.updated = new Date();
data.created = new Date(data.created); data.created = new Date(data.created);
const [updateListing] = await this.conn.update(table).set(data).where(eq(table.id, id)).returning(); 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 [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
return updateListing as BusinessListing | CommercialPropertyListing;
}
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
data.updated = new Date();
data.created = new Date(data.created);
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
return updateListing as BusinessListing | CommercialPropertyListing; return updateListing as BusinessListing | CommercialPropertyListing;
} }
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> { async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
await this.conn.delete(table).where(eq(table.id, id)); await this.conn.delete(table).where(eq(table.id, id));
} }
@@ -106,23 +169,17 @@ export class ListingsService {
// ############################################################## // ##############################################################
// Images for commercial Properties // Images for commercial Properties
// ############################################################## // ##############################################################
async deleteImage(imagePath: string, serial: string, name: string) {
async updateImageOrder(id: string, imageOrder: string[]) { const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
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); const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) { if (index > -1) {
listing.imageOrder.splice(index, 1); listing.imageOrder.splice(index, 1);
await this.updateListing(listing.id, listing, commercials); await this.updateCommercialPropertyListing(listing.id, listing);
} }
} }
async addImage(imagePath: string, imagename: string) { async addImage(imagePath: string, serial: string, imagename: string) {
const listing = (await this.findByImagePath(imagePath)) as unknown as CommercialPropertyListing; const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
listing.imageOrder.push(imagename); listing.imageOrder.push(imagename);
await this.updateListing(listing.id, listing, commercials); await this.updateCommercialPropertyListing(listing.id, listing);
} }
} }

View File

@@ -1,7 +1,6 @@
import { Controller, Get, Inject, Param } from '@nestjs/common'; import { Controller, Inject } 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 { businesses, commercials } from '../drizzle/schema.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
@Controller('listings/undefined') @Controller('listings/undefined')
@@ -11,13 +10,13 @@ export class UnknownListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {} ) {}
@Get(':id') // @Get(':id')
async findById(@Param('id') id: string): Promise<any> { // async findById(@Param('id') id: string): Promise<any> {
const result = await this.listingsService.findById(id, businesses); // const result = await this.listingsService.findById(id, businesses);
if (result) { // if (result) {
return result; // return result;
} else { // } else {
return await this.listingsService.findById(id, commercials); // return await this.listingsService.findById(id, commercials);
} // }
} // }
} }

View File

@@ -7,6 +7,10 @@ export class MailController {
constructor(private mailService: MailService) {} constructor(private mailService: MailService) {}
@Post() @Post()
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> { sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
if (mailInfo.listing) {
return this.mailService.sendInquiry(mailInfo); return this.mailService.sendInquiry(mailInfo);
} else {
return this.mailService.sendRequest(mailInfo);
}
} }
} }

View File

@@ -41,4 +41,23 @@ export class MailService {
}, },
}); });
} }
async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> {
if (isEmpty(mailInfo.sender.name)) {
return { fields: [{ fieldname: 'name', message: 'Required' }] };
}
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,
},
});
}
} }

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,19 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js'; import express from 'express';
import * as express from 'express'; import path from 'path';
import path, { join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { AppModule } from './app.module.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); 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

@@ -15,6 +15,7 @@ export interface User {
hasCompanyLogo?: boolean; hasCompanyLogo?: boolean;
licensedIn?: LicensedIn[]; licensedIn?: LicensedIn[];
gender?: 'male' | 'female'; gender?: 'male' | 'female';
customerType?: 'buyer' | 'broker' | 'professional';
created?: Date; created?: Date;
updated?: Date; updated?: Date;
} }
@@ -35,6 +36,7 @@ export interface UserData {
hasCompanyLogo?: boolean; hasCompanyLogo?: boolean;
licensedIn?: string[]; licensedIn?: string[];
gender?: 'male' | 'female'; gender?: 'male' | 'female';
customerType?: 'buyer' | 'broker' | 'professional';
created?: Date; created?: Date;
updated?: Date; updated?: Date;
} }
@@ -61,6 +63,7 @@ export interface BusinessListing {
reasonForSale?: string; reasonForSale?: string;
brokerLicencing?: string; brokerLicencing?: string;
internals?: string; internals?: string;
imageName?: string;
created?: Date; created?: Date;
updated?: Date; updated?: Date;
visits?: number; visits?: number;
@@ -70,6 +73,7 @@ export interface BusinessListing {
export interface CommercialPropertyListing { export interface CommercialPropertyListing {
id: string; id: string;
serialId?: number;
userId?: string; userId?: string;
type?: number; type?: number;
title?: string; title?: string;

View File

@@ -83,6 +83,11 @@ export interface KeycloakUser {
notBefore?: number; notBefore?: number;
access?: Access; access?: Access;
} }
export interface JwtUser {
userId: string;
username: string;
roles: string[];
}
export interface Access { export interface Access {
manageGroupMembership: boolean; manageGroupMembership: boolean;
view: boolean; view: boolean;
@@ -130,6 +135,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;
} }
@@ -193,6 +206,9 @@ export function isEmpty(value: any): boolean {
return false; return false;
} }
export function emailToDirName(email: string): string { export function emailToDirName(email: string): string {
if (email === undefined || email === null) {
return null;
}
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche // Entferne ungültige Zeichen und ersetze sie durch Unterstriche
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_'); const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');

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

@@ -10,9 +10,9 @@ export class SelectOptionsController {
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,
} };
} }
} }

View File

@@ -39,9 +39,14 @@ export class SelectOptionsService {
{ 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: 'Broker', value: 'broker' },
{ name: 'Professional', value: 'professional' },
];
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

@@ -7,7 +7,7 @@ import * as schema from '../drizzle/schema.js';
import { PG_CONNECTION } from '../drizzle/schema.js'; import { PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { User } from '../models/db.model.js'; import { User } from '../models/db.model.js';
import { ListingCriteria } from '../models/main.model.js'; import { ListingCriteria, emailToDirName } from '../models/main.model.js';
@Injectable() @Injectable()
export class UserService { export class UserService {
@@ -33,8 +33,8 @@ export class UserService {
.from(schema.users) .from(schema.users)
.where(sql`email = ${email}`)) as User[]; .where(sql`email = ${email}`)) as User[];
const user = users[0]; const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(user.id); user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(user.id); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user; return user;
} }
async getUserById(id: string) { async getUserById(id: string) {
@@ -43,8 +43,8 @@ export class UserService {
.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 user;
} }
async saveUser(user: any): Promise<User> { async saveUser(user: any): Promise<User> {

View File

@@ -3,7 +3,7 @@ import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScroll
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations'; import { provideAnimations } from '@angular/platform-browser/animations';
import { KeycloakService } from 'keycloak-angular'; import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
import { environment } from '../environments/environment'; import { environment } from '../environments/environment';
import { customKeycloakAdapter } from '../keycloak'; import { customKeycloakAdapter } from '../keycloak';
import { routes } from './app.routes'; import { routes } from './app.routes';
@@ -37,6 +37,11 @@ export const appConfig: ApplicationConfig = {
useClass: LoadingInterceptor, useClass: LoadingInterceptor,
multi: true, multi: true,
}, },
{
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
},
provideRouter( provideRouter(
routes, routes,
withEnabledBlockingInitialNavigation(), withEnabledBlockingInitialNavigation(),
@@ -89,6 +94,10 @@ function initializeKeycloak(keycloak: KeycloakService) {
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}`); logger.info(`+++>${authenticated}`);
}; };

View File

@@ -18,6 +18,7 @@
<div class="col-12 md:col-3 text-500"> <div class="col-12 md:col-3 text-500">
<div class="text-black font-bold line-height-3 mb-3">Actions</div> <div class="text-black font-bold line-height-3 mb-3">Actions</div>
<a *ngIf="!keycloakService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a> <a *ngIf="!keycloakService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
<a *ngIf="!keycloakService.isLoggedIn()" (click)="register()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Register</a>
<a *ngIf="keycloakService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a> <a *ngIf="keycloakService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
<a *ngIf="keycloakService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="keycloakService.logout()">Log Out</a> <a *ngIf="keycloakService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="keycloakService.logout()">Log Out</a>
</div> </div>

View File

@@ -18,4 +18,7 @@ export class FooterComponent {
redirectUri: window.location.href, redirectUri: window.location.href,
}); });
} }
register() {
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
}
} }

View File

@@ -91,14 +91,17 @@ export class HeaderComponent {
{ {
label: 'Businesses for Sale', label: 'Businesses for Sale',
routerLink: '/businessListings', routerLink: '/businessListings',
state: {},
}, },
{ {
label: 'Commercial Property', label: 'Commercial Property',
routerLink: '/commercialPropertyListings', routerLink: '/commercialPropertyListings',
state: {},
}, },
{ {
label: 'Professionals/Brokers Directory', label: 'Professionals/Brokers Directory',
routerLink: '/brokerListings', routerLink: '/brokerListings',
state: {},
}, },
]; ];
this.loginItems = [ this.loginItems = [

View File

@@ -2,14 +2,13 @@
<div class="flex justify-content-between mt-3"> <div class="flex justify-content-between mt-3">
@if(ratioVariable){ @if(ratioVariable){
<div> <div>
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)" <p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)" optionLabel="label" optionValue="value" class="small"></p-selectButton>
optionLabel="label" optionValue="value"></p-selectButton>
</div> </div>
} @else { } @else {
<div></div> <div></div>
} }
<div> <div class="flex justify-content-between">
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true"></p-button> <p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true" size="small" class="mr-2"></p-button>
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button> <p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true" size="small"></p-button>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,4 @@
::ng-deep p-selectbutton.small .p-button {
font-size: 0.875rem;
padding: 0.65625rem 1.09375rem;
}

View File

@@ -1,41 +1,33 @@
import { Component, ViewChild } from '@angular/core'; import { Component, ViewChild } from '@angular/core';
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs'; 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 { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FileUpload, FileUploadModule } from 'primeng/fileupload'; 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 { SelectButtonModule } from 'primeng/selectbutton';
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model'; import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
import { ImageService } from '../../services/image.service';
import { LoadingService } from '../../services/loading.service';
import { SharedModule } from '../../shared/shared/shared.module';
export const stateOptions: KeyValueRatio[] = [ export const stateOptions: KeyValueRatio[] = [
{ label: '16/9', value: 16 / 9 }, { label: '16/9', value: 16 / 9 },
{ label: '1/1', value: 1 }, { label: '1/1', value: 1 },
{label:'2/3',value:2/3}, { label: 'Free', value: NaN },
] ];
@Component({ @Component({
selector: 'app-image-cropper', selector: 'app-image-cropper',
standalone: true, standalone: true,
imports: [SharedModule, FileUploadModule, AngularCropperjsModule, SelectButtonModule], imports: [SharedModule, FileUploadModule, AngularCropperjsModule, SelectButtonModule],
templateUrl: './image-cropper.component.html', templateUrl: './image-cropper.component.html',
styleUrl: './image-cropper.component.scss' styleUrl: './image-cropper.component.scss',
}) })
export class ImageCropperComponent { export class ImageCropperComponent {
@ViewChild(CropperComponent) public angularCropper: CropperComponent; @ViewChild(CropperComponent) public angularCropper: CropperComponent;
imageUrl: string; //wird im Template verwendet imageUrl: string; //wird im Template verwendet
fileUpload:FileUpload fileUpload: FileUpload;
value: number = stateOptions[0].value; value: number = stateOptions[0].value;
cropperConfig={aspectRatio: this.value} cropperConfig = { aspectRatio: this.value };
ratioVariable:boolean ratioVariable: boolean;
stateOptions=stateOptions stateOptions = stateOptions;
constructor( constructor(private loadingService: LoadingService, private imageUploadService: ImageService, public config: DynamicDialogConfig, public ref: DynamicDialogRef) {}
private loadingService:LoadingService,
private imageUploadService: ImageService,
public config: DynamicDialogConfig,
public ref: DynamicDialogRef
){}
ngOnInit(): void { ngOnInit(): void {
if (this.config.data) { if (this.config.data) {
this.imageUrl = this.config.data.imageUrl; this.imageUrl = this.config.data.imageUrl;
@@ -45,14 +37,7 @@ export class ImageCropperComponent {
} }
} }
sendImage() { sendImage() {
// setTimeout(()=>{ this.fileUpload.clear();
// 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); this.ref.close(this.angularCropper.cropper);
} }
@@ -61,7 +46,7 @@ export class ImageCropperComponent {
this.ref.close(); this.ref.close();
} }
changeAspectRation(ratio: number) { changeAspectRation(ratio: number) {
this.cropperConfig={aspectRatio: ratio} this.cropperConfig = { aspectRatio: ratio };
this.angularCropper.cropper.setAspectRatio(ratio); this.angularCropper.cropper.setAspectRatio(ratio);
} }
} }

View File

@@ -20,6 +20,7 @@ export class AuthGuard extends KeycloakAuthGuard {
} }
// Force the user to log in if currently unauthenticated. // Force the user to log in if currently unauthenticated.
const authenticated = this.keycloak.isLoggedIn(); const authenticated = this.keycloak.isLoggedIn();
//this.keycloak.isTokenExpired()
if (!this.authenticated && !authenticated) { if (!this.authenticated && !authenticated) {
await this.keycloak.login({ await this.keycloak.login({
redirectUri: window.location.origin + state.url, redirectUri: window.location.origin + state.url,

View File

@@ -1,95 +0,0 @@
/**
* @license
* Copyright Mauricio Gemelli Vigolo and contributors.
*
* Use of this source code is governed by a MIT-style license that can be
* found in the LICENSE file at https://github.com/mauriciovigolo/keycloak-angular/blob/main/LICENSE.md
*/
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, combineLatest, from, of } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
import { ExcludedUrlRegex } from '../models/keycloak-options';
import { KeycloakService } from '../services/keycloak.service';
/**
* This interceptor includes the bearer by default in all HttpClient requests.
*
* If you need to exclude some URLs from adding the bearer, please, take a look
* at the {@link KeycloakOptions} bearerExcludedUrls property.
*/
@Injectable()
export class KeycloakBearerInterceptor implements HttpInterceptor {
constructor(private keycloak: KeycloakService) {}
/**
* Calls to update the keycloak token if the request should update the token.
*
* @param req http request from @angular http module.
* @returns
* A promise boolean for the token update or noop result.
*/
private async conditionallyUpdateToken(req: HttpRequest<unknown>): Promise<boolean> {
if (this.keycloak.shouldUpdateToken(req)) {
return await this.keycloak.updateToken();
}
return true;
}
/**
* @deprecated
* Checks if the url is excluded from having the Bearer Authorization
* header added.
*
* @param req http request from @angular http module.
* @param excludedUrlRegex contains the url pattern and the http methods,
* excluded from adding the bearer at the Http Request.
*/
private isUrlExcluded({ method, url }: HttpRequest<unknown>, { urlPattern, httpMethods }: ExcludedUrlRegex): boolean {
const httpTest = httpMethods.length === 0 || httpMethods.join().indexOf(method.toUpperCase()) > -1;
const urlTest = urlPattern.test(url);
return httpTest && urlTest;
}
/**
* Intercept implementation that checks if the request url matches the excludedUrls.
* If not, adds the Authorization header to the request if the user is logged in.
*
* @param req
* @param next
*/
public intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
const { enableBearerInterceptor, excludedUrls } = this.keycloak;
if (!enableBearerInterceptor) {
return next.handle(req);
}
const shallPass: boolean = !this.keycloak.shouldAddToken(req) || excludedUrls.findIndex(item => this.isUrlExcluded(req, item)) > -1;
if (shallPass) {
return next.handle(req);
}
return combineLatest([from(this.conditionallyUpdateToken(req)), of(this.keycloak.isLoggedIn())]).pipe(mergeMap(([_, isLoggedIn]) => (isLoggedIn ? this.handleRequestWithTokenHeader(req, next) : next.handle(req))));
}
/**
* Adds the token of the current user to the Authorization header
*
* @param req
* @param next
*/
private handleRequestWithTokenHeader(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
return this.keycloak.addTokenToHeader(req.headers).pipe(
mergeMap(headersWithBearer => {
const kcReq = req.clone({ headers: headersWithBearer });
return next.handle(kcReq);
}),
);
}
}

View File

@@ -14,50 +14,62 @@
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<ul class="list-none p-0 m-0 border-top-1 border-300"> <ul class="list-none p-0 m-0 border-top-1 border-300">
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium flex">Description</div> <div class="text-500 w-full md:w-3 font-medium flex">Description</div>
<div class="text-900 w-full md:w-10 line-height-3 flex flex-column" [innerHTML]="description"></div> <div class="text-900 w-full md:w-9 line-height-3 flex flex-column" [innerHTML]="description"></div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Category</div> <div class="text-500 w-full md:w-3 font-medium">Category</div>
<div class="text-900 w-full md:w-10"> <div class="text-900 w-full md:w-9">
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip> <p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
</div> </div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Located in</div> <div class="text-500 w-full md:w-3 font-medium">Located in</div>
<div class="text-900 w-full md:w-10">{{ selectOptions.getState(listing.state) }}</div> <div class="text-900 w-full md:w-9">{{ listing.city }}, {{ selectOptions.getState(listing.state) }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Asking Price</div> <div class="text-500 w-full md:w-3 font-medium">Asking Price</div>
<div class="text-900 w-full md:w-10">{{ listing.price | currency }}</div> <div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Real Estate Included</div> <div class="text-500 w-full md:w-3 font-medium">Sales revenue</div>
<div class="text-900 w-full md:w-10">{{ listing.realEstateIncluded ? 'Yes' : 'No' }}</div> <div class="text-900 w-full md:w-9">{{ listing.salesRevenue | currency }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Sales revenue</div> <div class="text-500 w-full md:w-3 font-medium">Cash flow</div>
<div class="text-900 w-full md:w-10">{{ listing.salesRevenue | currency }}</div> <div class="text-900 w-full md:w-9">{{ listing.cashFlow | currency }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Cash flow</div> <div class="text-500 w-full md:w-3 font-medium">Type of Real Estate</div>
<div class="text-900 w-full md:w-10">{{ listing.cashFlow | currency }}</div> <div class="text-900 w-full md:w-9">
@if (listing.realEstateIncluded){
<p-chip label="Real Estate Included"></p-chip>
} @if (listing.leasedLocation){
<p-chip label="Leased Location"></p-chip>
} @if (listing.franchiseResale){
<p-chip label="Franchise Re-Sale"></p-chip>
}
</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Employees</div> <div class="text-500 w-full md:w-3 font-medium">Employees</div>
<div class="text-900 w-full md:w-10">{{ listing.employees }}</div> <div class="text-900 w-full md:w-9">{{ listing.employees }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Support & Training</div> <div class="text-500 w-full md:w-3 font-medium">Established since</div>
<div class="text-900 w-full md:w-10">{{ listing.supportAndTraining }}</div> <div class="text-900 w-full md:w-9">{{ listing.established }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Reason for Sale</div> <div class="text-500 w-full md:w-3 font-medium">Support & Training</div>
<div class="text-900 w-full md:w-10">{{ listing.reasonForSale }}</div> <div class="text-900 w-full md:w-9">{{ listing.supportAndTraining }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Broker licensing</div> <div class="text-500 w-full md:w-3 font-medium">Reason for Sale</div>
<div class="text-900 w-full md:w-10">{{ listing.brokerLicencing }}</div> <div class="text-900 w-full md:w-9">{{ listing.reasonForSale }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-3 font-medium">Broker licensing</div>
<div class="text-900 w-full md:w-9">{{ listing.brokerLicencing }}</div>
</li> </li>
</ul> </ul>
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){ @if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
@@ -79,7 +91,8 @@
</div> </div>
<div class="field mb-4 col-12 md:col-6"> <div class="field mb-4 col-12 md:col-6">
<label for="phoneNumber" class="font-medium text-900">Phone Number</label> <label for="phoneNumber" class="font-medium text-900">Phone Number</label>
<input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" /> <!-- <input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" /> -->
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
</div> </div>
<div class="field mb-4 col-12 md:col-6"> <div class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label> <label for="state" class="font-medium text-900">Country/State</label>
@@ -92,9 +105,9 @@
</div> </div>
@if(listingUser){ @if(listingUser){
<div class="surface-border mb-4 col-12 flex align-items-center"> <div class="surface-border mb-4 col-12 flex align-items-center">
Listing by &nbsp;<a routerLink="/details-user/{{ listingUser.id }}" class="mr-2">{{ listingUser.firstname }} {{ listingUser.lastname }}</a> Listing by &nbsp;<a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
@if(listingUser.hasCompanyLogo){ @if(listingUser.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listingUser.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 30px; max-width: 100px" /> <img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
} }
</div> </div>
} }

View File

@@ -5,6 +5,7 @@ import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change'; import onChange from 'on-change';
import { MessageService } from 'primeng/api'; import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria'; import { GalleriaModule } from 'primeng/galleria';
import { InputMaskModule } from 'primeng/inputmask';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
@@ -15,12 +16,11 @@ import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils'; import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-details-business-listing', selector: 'app-details-business-listing',
standalone: true, standalone: true,
imports: [SharedModule, GalleriaModule], imports: [SharedModule, GalleriaModule, InputMaskModule],
providers: [MessageService], providers: [MessageService],
templateUrl: './details-business-listing.component.html', templateUrl: './details-business-listing.component.html',
styleUrl: './details-business-listing.component.scss', styleUrl: './details-business-listing.component.scss',
@@ -49,7 +49,8 @@ export class DetailsBusinessListingComponent {
criteria: ListingCriteria; criteria: ListingCriteria;
mailinfo: MailInfo; mailinfo: MailInfo;
environment = environment; environment = environment;
user: KeycloakUser; keycloakUser: KeycloakUser;
user: User;
listingUser: User; listingUser: User;
description: SafeHtml; description: SafeHtml;
private history: string[] = []; private history: string[] = [];
@@ -73,11 +74,16 @@ export class DetailsBusinessListingComponent {
} }
}); });
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl }; this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
this.user;
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler); this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
} }
async ngOnInit() { async ngOnInit() {
const token = await this.keycloakService.getToken();
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
}
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business')); this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
this.listingUser = await this.userService.getById(this.listing.userId); this.listingUser = await this.userService.getById(this.listing.userId);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description); this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);

View File

@@ -14,32 +14,32 @@
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<ul class="list-none p-0 m-0 border-top-1 border-300"> <ul class="list-none p-0 m-0 border-top-1 border-300">
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium flex">Description</div> <div class="text-500 w-full md:w-3 font-medium flex">Description</div>
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div> <div class="text-900 w-full md:w-9 line-height-3" [innerHTML]="description"></div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Property Category</div> <div class="text-500 w-full md:w-3 font-medium">Property Category</div>
<div class="text-900 w-full md:w-10">{{ selectOptions.getCommercialProperty(listing.type) }}</div> <div class="text-900 w-full md:w-9">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Located in</div> <div class="text-500 w-full md:w-3 font-medium">Located in</div>
<div class="text-900 w-full md:w-10">{{ selectOptions.getState(listing.state) }}</div> <div class="text-900 w-full md:w-9">{{ selectOptions.getState(listing.state) }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">City</div> <div class="text-500 w-full md:w-3 font-medium">City</div>
<div class="text-900 w-full md:w-10">{{ listing.city }}</div> <div class="text-900 w-full md:w-9">{{ listing.city }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Zip Code</div> <div class="text-500 w-full md:w-3 font-medium">Zip Code</div>
<div class="text-900 w-full md:w-10">{{ listing.zipCode }}</div> <div class="text-900 w-full md:w-9">{{ listing.zipCode }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">County</div> <div class="text-500 w-full md:w-3 font-medium">County</div>
<div class="text-900 w-full md:w-10">{{ listing.county }}</div> <div class="text-900 w-full md:w-9">{{ listing.county }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Asking Price:</div> <div class="text-500 w-full md:w-3 font-medium">Asking Price:</div>
<div class="text-900 w-full md:w-10">{{ listing.price | currency }}</div> <div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
</li> </li>
</ul> </ul>
@@ -49,9 +49,9 @@
</div> </div>
<div class="col-12 md:col-6"> <div class="col-12 md:col-6">
<p-galleria [value]="propertyImages" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5"> <p-galleria [value]="listing.imageOrder" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
<ng-template pTemplate="item" let-item> <ng-template pTemplate="item" let-item>
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ item }}" style="width: 100%" /> <img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ item }}" style="width: 100%" />
</ng-template> </ng-template>
</p-galleria> </p-galleria>
@if (mailinfo){ @if (mailinfo){
@@ -69,7 +69,8 @@
</div> </div>
<div class="field mb-4 col-12 md:col-6"> <div class="field mb-4 col-12 md:col-6">
<label for="phoneNumber" class="font-medium text-900">Phone Number</label> <label for="phoneNumber" class="font-medium text-900">Phone Number</label>
<input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" /> <!-- <input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" /> -->
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
</div> </div>
<div class="field mb-4 col-12 md:col-6"> <div class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label> <label for="state" class="font-medium text-900">Country/State</label>
@@ -82,9 +83,9 @@
</div> </div>
@if(listingUser){ @if(listingUser){
<div class="surface-border mb-4 col-12 flex align-items-center"> <div class="surface-border mb-4 col-12 flex align-items-center">
Listing by &nbsp;<a routerLink="/details-user/{{ listingUser.id }}" class="mr-2">{{ listingUser.firstname }} {{ listingUser.lastname }}</a> Listing by &nbsp;<a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
@if(listingUser.hasCompanyLogo){ @if(listingUser.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listingUser.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 30px; max-width: 100px" /> <img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imagePath }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
} }
</div> </div>
} }

View File

@@ -5,11 +5,13 @@ import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change'; import onChange from 'on-change';
import { MessageService } from 'primeng/api'; import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria'; import { GalleriaModule } from 'primeng/galleria';
import { InputMaskModule } from 'primeng/inputmask';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service'; import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
import { MailService } from '../../../services/mail.service'; import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
@@ -20,13 +22,12 @@ import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../.
@Component({ @Component({
selector: 'app-details-commercial-property-listing', selector: 'app-details-commercial-property-listing',
standalone: true, standalone: true,
imports: [SharedModule, GalleriaModule], imports: [SharedModule, GalleriaModule, InputMaskModule],
providers: [MessageService], providers: [MessageService],
templateUrl: './details-commercial-property-listing.component.html', templateUrl: './details-commercial-property-listing.component.html',
styleUrl: './details-commercial-property-listing.component.scss', styleUrl: './details-commercial-property-listing.component.scss',
}) })
export class DetailsCommercialPropertyListingComponent { export class DetailsCommercialPropertyListingComponent {
// listings: Array<BusinessListing>;
responsiveOptions = [ responsiveOptions = [
{ {
breakpoint: '1199px', breakpoint: '1199px',
@@ -48,9 +49,9 @@ export class DetailsCommercialPropertyListingComponent {
listing: CommercialPropertyListing; listing: CommercialPropertyListing;
criteria: ListingCriteria; criteria: ListingCriteria;
mailinfo: MailInfo; mailinfo: MailInfo;
propertyImages: string[] = [];
environment = environment; environment = environment;
user: KeycloakUser; keycloakUser: KeycloakUser;
user: User;
listingUser: User; listingUser: User;
description: SafeHtml; description: SafeHtml;
ts = new Date().getTime(); ts = new Date().getTime();
@@ -67,6 +68,7 @@ export class DetailsCommercialPropertyListingComponent {
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
public historyService: HistoryService, public historyService: HistoryService,
public keycloakService: KeycloakService, public keycloakService: KeycloakService,
private imageService: ImageService,
) { ) {
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl }; this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
@@ -75,9 +77,12 @@ export class DetailsCommercialPropertyListingComponent {
async ngOnInit() { async ngOnInit() {
const token = await this.keycloakService.getToken(); const token = await this.keycloakService.getToken();
this.user = map2User(token); this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
}
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty')); this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
this.listingUser = await this.userService.getById(this.listing.userId); this.listingUser = await this.userService.getById(this.listing.userId);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description); this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
} }

View File

@@ -6,7 +6,7 @@
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between"> <div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between">
<div class="flex align-items-start flex-column md:flex-row"> <div class="flex align-items-start flex-column md:flex-row">
@if(user.hasProfile){ @if(user.hasProfile){
<img src="{{ env.imageBaseUrl }}/pictures//profile/{{ user.id }}.avif?_ts={{ ts }}" class="mr-5 mb-3 lg:mb-0" style="width: 90px" /> <img src="{{ env.imageBaseUrl }}/pictures//profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
} @else { } @else {
<img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" /> <img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
} }
@@ -20,7 +20,7 @@
</div> </div>
<div class="mr-5 mt-3"> <div class="mr-5 mt-3">
<span class="font-medium text-500">For Sale</span> <span class="font-medium text-500">For Sale</span>
<div class="text-700 mt-2">{{ businessListings.length + commercialPropListings.length }}</div> <div class="text-700 mt-2">{{ businessListings?.length + commercialPropListings?.length }}</div>
</div> </div>
<!-- <div class="mr-5 mt-3"> <!-- <div class="mr-5 mt-3">
<span class="font-medium text-500">Sold</span> <span class="font-medium text-500">Sold</span>
@@ -30,7 +30,7 @@
<!-- <span class="font-medium text-500">Logo</span> --> <!-- <span class="font-medium text-500">Logo</span> -->
<div> <div>
@if(user.hasCompanyLogo){ @if(user.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ user.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 60px; max-width: 100px" /> <img src="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 60px; max-width: 100px" />
} }
<!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png" <!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png"
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> --> class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> -->
@@ -57,7 +57,7 @@
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap"> <li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Phone Number</div> <div class="text-500 w-full md:w-2 font-medium">Phone Number</div>
<div class="text-900 w-full md:w-10 line-height-3">{{ user.phoneNumber }}</div> <div class="text-900 w-full md:w-10 line-height-3">{{ formatPhoneNumber(user.phoneNumber) }}</div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">EMail Address</div> <div class="text-500 w-full md:w-2 font-medium">EMail Address</div>
@@ -121,7 +121,7 @@
<div class="p-3 border-1 surface-border border-round surface-card"> <div class="p-3 border-1 surface-border border-round surface-card">
<div class="text-900 mb-2 flex align-items-center"> <div class="text-900 mb-2 flex align-items-center">
@if (listing.imageOrder?.length>0){ @if (listing.imageOrder?.length>0){
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" /> <img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" />
} @else { } @else {
<img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" /> <img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" />
} }

View File

@@ -6,7 +6,7 @@ import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria'; import { GalleriaModule } from 'primeng/galleria';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser, ListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model'; import { KeycloakUser, ListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service'; import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.service'; import { ImageService } from '../../../services/image.service';
@@ -14,7 +14,7 @@ import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { map2User } from '../../../utils/utils'; import { formatPhoneNumber, map2User } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-details-user', selector: 'app-details-user',
@@ -37,6 +37,8 @@ export class DetailsUserComponent {
offeredServices: SafeHtml; offeredServices: SafeHtml;
ts = new Date().getTime(); ts = new Date().getTime();
env = environment; env = environment;
emailToDirName = emailToDirName;
formatPhoneNumber = formatPhoneNumber;
constructor( constructor(
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private router: Router, private router: Router,
@@ -52,8 +54,7 @@ export class DetailsUserComponent {
async ngOnInit() { async ngOnInit() {
this.user = await this.userService.getById(this.id); this.user = await this.userService.getById(this.id);
this.user.email; const results = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
const results = await Promise.all([await this.listingsService.getListingByUserId(this.id, 'business'), await this.listingsService.getListingByUserId(this.id, 'commercialProperty')]);
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse // Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
this.businessListings = results[0]; this.businessListings = results[0];
this.commercialPropListings = results[1]; this.commercialPropListings = results[1];

View File

@@ -32,7 +32,7 @@
<div class="surface-card p-4 flex flex-column align-items-center md:flex-row md:align-items-stretch h-full"> <div class="surface-card p-4 flex flex-column align-items-center md:flex-row md:align-items-stretch h-full">
<span> <span>
@if(user.hasProfile){ @if(user.hasProfile){
<img src="{{ env.imageBaseUrl }}/pictures/profile/{{ user.id }}.avif?_ts={{ ts }}" class="w-5rem" /> <img src="{{ env.imageBaseUrl }}/pictures/profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-5rem" />
} @else { } @else {
<img src="assets/images/person_placeholder.jpg" class="w-5rem" /> <img src="assets/images/person_placeholder.jpg" class="w-5rem" />
} }
@@ -45,7 +45,7 @@
</div> </div>
<div class="px-4 py-3 text-right flex justify-content-between align-items-center"> <div class="px-4 py-3 text-right flex justify-content-between align-items-center">
@if(user.hasCompanyLogo){ @if(user.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ user.id }}.avif?_ts={{ ts }}" class="rounded-image" /> <img src="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="rounded-image" />
} @else { } @else {
<img src="assets/images/placeholder.png" class="rounded-image" /> <img src="assets/images/placeholder.png" class="rounded-image" />
} }

View File

@@ -6,7 +6,7 @@
margin-bottom: -1px; margin-bottom: -1px;
} }
.search { .search {
background-color: #343F69; background-color: #343f69;
} }
::ng-deep p-paginator div { ::ng-deep p-paginator div {
background-color: var(--surface-200) !important; background-color: var(--surface-200) !important;
@@ -16,7 +16,7 @@
border-radius: 6px; border-radius: 6px;
// width: 100px; // width: 100px;
max-width: 100px; max-width: 100px;
height: 45px; max-height: 45px;
border: 1px solid rgba(0, 0, 0, 0.2); border: 1px solid rgba(0, 0, 0, 0.2);
padding: 1px 1px; padding: 1px 1px;
object-fit: contain; object-fit: contain;

View File

@@ -12,7 +12,7 @@ import { PaginatorModule } from 'primeng/paginator';
import { StyleClassModule } from 'primeng/styleclass'; import { StyleClassModule } from 'primeng/styleclass';
import { ToggleButtonModule } from 'primeng/togglebutton'; import { ToggleButtonModule } from 'primeng/togglebutton';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model'; import { ListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service'; import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
@@ -60,7 +60,7 @@ export class BrokerListingsComponent {
ts = new Date().getTime(); ts = new Date().getTime();
env = environment; env = environment;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined; public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
emailToDirName = emailToDirName;
constructor( constructor(
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private listingsService: ListingsService, private listingsService: ListingsService,

View File

@@ -51,7 +51,7 @@
@for (listing of listings; track listing.id) { @for (listing of listings; track listing.id) {
<div class="col-12 lg:col-3 p-3"> <div class="col-12 lg:col-3 p-3">
<div class="shadow-2 border-round surface-card h-full flex-column justify-content-between flex"> <div class="shadow-2 border-round surface-card h-full flex-column justify-content-between flex">
<div class="p-4 flex flex-column relative"> <div class="p-4 flex flex-column relative h-full">
<div class="flex align-items-center"> <div class="flex align-items-center">
<span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px"> <span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px">
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i> <i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i>
@@ -66,7 +66,7 @@
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{ listing.established }}</p> <p class="mt-0 mb-1 text-700 line-height-3">Established: {{ listing.established }}</p>
<div class="icon-pos"> <div class="icon-pos">
<a routerLink="/details-user/{{ listing.userId }}" class="mr-2" <a routerLink="/details-user/{{ listing.userId }}" class="mr-2"
><img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.userId }}.avif?_ts={{ ts }}" (error)="imageErrorHandler(listing)" class="rounded-image" ><img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" (error)="imageErrorHandler(listing)" class="rounded-image"
/></a> /></a>
</div> </div>
</div> </div>

View File

@@ -13,7 +13,7 @@ import { StyleClassModule } from 'primeng/styleclass';
import { ToggleButtonModule } from 'primeng/togglebutton'; import { ToggleButtonModule } from 'primeng/togglebutton';
import { TooltipModule } from 'primeng/tooltip'; import { TooltipModule } from 'primeng/tooltip';
import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model'; import { ListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service'; import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
@@ -58,7 +58,7 @@ export class BusinessListingsComponent {
rows: number = 12; rows: number = 12;
env = environment; env = environment;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined; public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
emailToDirName = emailToDirName;
constructor( constructor(
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private listingsService: ListingsService, private listingsService: ListingsService,

View File

@@ -51,7 +51,11 @@
<article class="flex flex-column md:flex-row w-full gap-3 p-3 surface-card"> <article class="flex flex-column md:flex-row w-full gap-3 p-3 surface-card">
<div class="relative"> <div class="relative">
@if (listing.imageOrder?.length>0){ @if (listing.imageOrder?.length>0){
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" /> <img
src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}"
alt="Image"
class="border-round w-full h-full md:w-12rem md:h-9rem"
/>
} @else { } @else {
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" /> <img src="assets/images/placeholder_properties.jpg" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
} }

View File

@@ -8,10 +8,16 @@
<div class="flex gap-5 flex-column-reverse md:flex-row"> <div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid"> <div class="flex-auto p-fluid">
@if (user){ @if (user){
<div class="mb-4"> <div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label> <label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label>
<input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email" /> <input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email" />
<p class="font-italic text-sm line-height-1">You can only modify your email by contacting us at support&#64;bizmatch.net</p> <p class="font-italic text-xs line-height-1">You can only modify your email by contacting us at support&#64;bizmatch.net</p>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="customerType" class="block font-medium text-900 mb-2">Customer Type</label>
<p-dropdown id="customerType" [options]="selectOptions?.customerTypes" [(ngModel)]="user.customerType" optionLabel="name" optionValue="value" placeholder="State" [style]="{ width: '100%' }"></p-dropdown>
</div>
</div> </div>
<div class="grid"> <div class="grid">
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
@@ -23,6 +29,7 @@
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname" /> <input id="lastname" type="text" pInputText [(ngModel)]="user.lastname" />
</div> </div>
</div> </div>
@if (isProfessional){
<div class="grid"> <div class="grid">
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
<label for="firstname" class="block font-medium text-900 mb-2">Company Name</label> <label for="firstname" class="block font-medium text-900 mb-2">Company Name</label>
@@ -33,10 +40,11 @@
<input id="lastname" type="text" pInputText [(ngModel)]="user.description" /> <input id="lastname" type="text" pInputText [(ngModel)]="user.description" />
</div> </div>
</div> </div>
} @if (isProfessional){
<div class="grid"> <div class="grid">
<div class="mb-4 col-12 md:col-4"> <div class="mb-4 col-12 md:col-4">
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label> <label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
<input id="phoneNumber" type="text" pInputText [(ngModel)]="user.phoneNumber" /> <p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="user.phoneNumber"></p-inputMask>
</div> </div>
<div class="mb-4 col-12 md:col-4"> <div class="mb-4 col-12 md:col-4">
<label for="companyWebsite" class="block font-medium text-900 mb-2">Company Website</label> <label for="companyWebsite" class="block font-medium text-900 mb-2">Company Website</label>
@@ -47,6 +55,14 @@
<p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete> <p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
</div> </div>
</div> </div>
} @else {
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="user.phoneNumber"></p-inputMask>
</div>
</div>
} @if (isProfessional){
<div class="mb-4"> <div class="mb-4">
<label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label> <label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label>
<p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }" [modules]="editorModules"> <p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }" [modules]="editorModules">
@@ -59,7 +75,6 @@
<ng-template pTemplate="header"></ng-template> <ng-template pTemplate="header"></ng-template>
</p-editor> </p-editor>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label> <label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label>
@for (areasServed of user.areasServed; track areasServed){ @for (areasServed of user.areasServed; track areasServed){
@@ -118,11 +133,12 @@
<span class="text-xs">&nbsp;(Add more licenses or remove existing ones.)</span> <span class="text-xs">&nbsp;(Add more licenses or remove existing ones.)</span>
<!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> --> <!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> -->
</div> </div>
} } }
<div> <div>
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button> <button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
</div> </div>
</div> </div>
@if (isProfessional){
<div> <div>
<div class="flex flex-column align-items-center flex-or mb-8"> <div class="flex flex-column align-items-center flex-or mb-8">
<span class="font-medium text-900 mb-2">Company Logo</span> <span class="font-medium text-900 mb-2">Company Logo</span>
@@ -172,6 +188,7 @@
></p-fileUpload> ></p-fileUpload>
</div> </div>
</div> </div>
}
</div> </div>
<div class="text-900 font-semibold text-lg mt-3">Membership Level</div> <div class="text-900 font-semibold text-lg mt-3">Membership Level</div>
<p-divider></p-divider> <p-divider></p-divider>

View File

@@ -25,8 +25,8 @@
/* Stil für das FontAwesome Icon */ /* Stil für das FontAwesome Icon */
.image-wrap fa-icon { .image-wrap fa-icon {
position: absolute; position: absolute;
top: 5px; /* Positioniert das Icon am oberen Rand des Bildes */ top: -5px; /* Positioniert das Icon am oberen Rand des Bildes */
right: 5px; /* Positioniert das Icon am rechten Rand des Bildes */ right: -18px; /* Positioniert das Icon am rechten Rand des Bildes */
color: #fff; /* Weiße Farbe für das Icon */ color: #fff; /* Weiße Farbe für das Icon */
background-color: rgba(0, 0, 0, 0.5); /* Halbtransparenter Hintergrund für bessere Sichtbarkeit */ background-color: rgba(0, 0, 0, 0.5); /* Halbtransparenter Hintergrund für bessere Sichtbarkeit */
padding: 5px; /* Ein wenig Platz um das Icon */ padding: 5px; /* Ein wenig Platz um das Icon */

View File

@@ -10,10 +10,11 @@ import { DialogModule } from 'primeng/dialog';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog'; import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { EditorModule } from 'primeng/editor'; import { EditorModule } from 'primeng/editor';
import { FileUpload, FileUploadModule } from 'primeng/fileupload'; import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { InputMaskModule } from 'primeng/inputmask';
import { SelectButtonModule } from 'primeng/selectbutton'; import { SelectButtonModule } from 'primeng/selectbutton';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { User } from '../../../../../../bizmatch-server/src/models/db.model'; import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, Invoice, Subscription } from '../../../../../../bizmatch-server/src/models/main.model'; import { AutoCompleteCompleteEvent, Invoice, Subscription, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component'; import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
import { GeoService } from '../../../services/geo.service'; import { GeoService } from '../../../services/geo.service';
@@ -23,12 +24,12 @@ import { SelectOptionsService } from '../../../services/select-options.service';
import { SubscriptionsService } from '../../../services/subscriptions.service'; import { SubscriptionsService } from '../../../services/subscriptions.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { map2User } from '../../../utils/utils'; import { getDialogWidth, getImageDimensions, map2User } from '../../../utils/utils';
import { TOOLBAR_OPTIONS } from '../../utils/defaults'; import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({ @Component({
selector: 'app-account', selector: 'app-account',
standalone: true, standalone: true,
imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule, ConfirmDialogModule], imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule, ConfirmDialogModule, InputMaskModule],
providers: [MessageService, DialogService, ConfirmationService], providers: [MessageService, DialogService, ConfirmationService],
templateUrl: './account.component.html', templateUrl: './account.component.html',
styleUrl: './account.component.scss', styleUrl: './account.component.scss',
@@ -74,29 +75,29 @@ export class AccountComponent {
try { try {
this.user = await this.userService.getByMail(email); this.user = await this.userService.getByMail(email);
} catch (e) { } catch (e) {
this.user = { email, firstname: keycloakUser.firstName, lastname: keycloakUser.lastName, areasServed: [], licensedIn: [], companyOverview: '', offeredServices: '' }; this.user = { email, firstname: keycloakUser.firstName, lastname: keycloakUser.lastName, areasServed: [], licensedIn: [], companyOverview: '', offeredServices: '', customerType: 'broker' };
this.user = await this.userService.save(this.user); this.user = await this.userService.save(this.user);
} }
} }
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id)); this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id));
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`; this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
} }
printInvoice(invoice: Invoice) {} printInvoice(invoice: Invoice) {}
async updateProfile(user: User) { async updateProfile(user: User) {
await this.userService.save(this.user); await this.userService.save(this.user);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Acount changes have been persisted', life: 3000 }); this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Account changes have been persisted', life: 3000 });
} }
onUploadCompanyLogo(event: any) { onUploadCompanyLogo(event: any) {
const uniqueSuffix = '?_ts=' + new Date().getTime(); const uniqueSuffix = '?_ts=' + new Date().getTime();
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}${uniqueSuffix}`; this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}${uniqueSuffix}`;
} }
onUploadProfilePicture(event: any) { onUploadProfilePicture(event: any) {
const uniqueSuffix = '?_ts=' + new Date().getTime(); const uniqueSuffix = '?_ts=' + new Date().getTime();
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}${uniqueSuffix}`; this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}${uniqueSuffix}`;
} }
setImageToFallback(event: Event) { setImageToFallback(event: Event) {
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild (event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
@@ -120,10 +121,16 @@ export class AccountComponent {
removeArea() { removeArea() {
this.user.areasServed.splice(this.user.areasServed.length - 1, 1); this.user.areasServed.splice(this.user.areasServed.length - 1, 1);
} }
get isProfessional() {
return this.user.customerType === 'broker' || this.user.customerType === 'professional';
}
select(event: any, type: 'company' | 'profile') { select(event: any, type: 'company' | 'profile') {
const imageUrl = URL.createObjectURL(event.files[0]); const imageUrl = URL.createObjectURL(event.files[0]);
this.type = type; this.type = type;
const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value }; const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value };
getImageDimensions(imageUrl).then(dimensions => {
const dialogWidth = getDialogWidth(dimensions);
this.dialogRef = this.dialogService.open(ImageCropperComponent, { this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: { data: {
imageUrl: imageUrl, imageUrl: imageUrl,
@@ -132,30 +139,26 @@ export class AccountComponent {
ratioVariable: type === 'company' ? true : false, ratioVariable: type === 'company' ? true : false,
}, },
header: 'Edit Image', header: 'Edit Image',
width: '50vw', width: dialogWidth,
modal: true, modal: true,
closeOnEscape: true, closeOnEscape: true,
keepInViewport: true, keepInViewport: true,
closable: false, closable: false,
breakpoints: {
'960px': '75vw',
'640px': '90vw',
},
}); });
this.dialogRef.onClose.subscribe(cropper => { this.dialogRef.onClose.subscribe(cropper => {
if (cropper) { if (cropper) {
this.loadingService.startLoading('uploadImage'); this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async blob => { cropper.getCroppedCanvas().toBlob(async blob => {
this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', this.user.id).subscribe( this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', emailToDirName(this.user.email)).subscribe(
async event => { async event => {
if (event.type === HttpEventType.Response) { if (event.type === HttpEventType.Response) {
this.loadingService.stopLoading('uploadImage'); this.loadingService.stopLoading('uploadImage');
if (this.type === 'company') { if (this.type === 'company') {
this.user.hasCompanyLogo = true; // this.user.hasCompanyLogo = true; //
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}.avif?_ts=${new Date().getTime()}`; this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
} else { } else {
this.user.hasProfile = true; this.user.hasProfile = true;
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}.avif?_ts=${new Date().getTime()}`; this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
} }
await this.userService.save(this.user); await this.userService.save(this.user);
} }
@@ -165,6 +168,7 @@ export class AccountComponent {
}); });
} }
}); });
});
} }
deleteConfirm(type: 'profile' | 'logo') { deleteConfirm(type: 'profile' | 'logo') {
this.confirmationService.confirm({ this.confirmationService.confirm({
@@ -180,10 +184,10 @@ export class AccountComponent {
accept: async () => { accept: async () => {
if (type === 'profile') { if (type === 'profile') {
this.user.hasProfile = false; this.user.hasProfile = false;
await Promise.all([this.imageService.deleteProfileImagesById(this.user.id), this.userService.save(this.user)]); await Promise.all([this.imageService.deleteProfileImagesById(this.user.email), this.userService.save(this.user)]);
} else { } else {
this.user.hasCompanyLogo = false; this.user.hasCompanyLogo = false;
await Promise.all([this.imageService.deleteLogoImagesById(this.user.id), this.userService.save(this.user)]); await Promise.all([this.imageService.deleteLogoImagesById(this.user.email), this.userService.save(this.user)]);
} }
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' }); this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
this.user = await this.userService.getById(this.user.id); this.user = await this.userService.getById(this.user.id);

View File

@@ -18,7 +18,7 @@ import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dy
import { EditorModule } from 'primeng/editor'; import { EditorModule } from 'primeng/editor';
import { FileUpload, FileUploadModule } from 'primeng/fileupload'; import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty } from '../../../../../../bizmatch-server/src/models/main.model'; import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component'; import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe'; import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
@@ -60,9 +60,7 @@ export class EditBusinessListingComponent {
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User; user: User;
maxFileSize = 3000000; maxFileSize = 3000000;
uploadUrl: string;
environment = environment; environment = environment;
propertyImages: string[];
responsiveOptions = [ responsiveOptions = [
{ {
breakpoint: '1199px', breakpoint: '1199px',
@@ -123,14 +121,14 @@ export class EditBusinessListingComponent {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business')); this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
} else { } else {
this.listing = createDefaultBusinessListing(); this.listing = createDefaultBusinessListing();
this.listing.userId = await this.userService.getId(keycloakUser.email); const listingUser = await this.userService.getByMail(keycloakUser.email);
this.listing.userId = listingUser.id;
this.listing.imageName = emailToDirName(keycloakUser.email);
if (this.data) { if (this.data) {
this.listing.title = this.data?.title; this.listing.title = this.data?.title;
this.listing.description = this.data?.description; this.listing.description = this.data?.description;
} }
} }
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
} }
async save() { async save() {

View File

@@ -82,6 +82,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<p-inputSwitch inputId="draft" [(ngModel)]="listing.draft"></p-inputSwitch>
<span class="ml-2 text-900 absolute translate-y-5">Draft Mode (Will not be shown as public listing)</span>
</div>
</div>
<div>
<p-divider></p-divider> <p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row"> <div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid"> <div class="flex-auto p-fluid">
@@ -111,12 +118,12 @@
</div> </div>
</div> </div>
</div> </div>
@if (propertyImages?.length>0){ @if (listing && listing.imageOrder?.length>0){
<div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop (dropped)="onDrop($event)" cdkDropListOrientation="horizontal"> <div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop (dropped)="onDrop($event)" cdkDropListOrientation="horizontal">
@for (image of propertyImages; track image) { @for (image of listing.imageOrder; track listing.imageOrder) {
<span cdkDropList mixedCdkDropList> <span cdkDropList mixedCdkDropList>
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap"> <div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="shadow-2" cdkDrag /> <img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="shadow-2" cdkDrag />
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon> <fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
</div> </div>
</span> </span>
@@ -137,3 +144,4 @@
</div> </div>
<p-toast></p-toast> <p-toast></p-toast>
<p-confirmDialog></p-confirmDialog> <p-confirmDialog></p-confirmDialog>
</div>

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { createDefaultCommercialPropertyListing, map2User, routeListingWithState } from '../../../utils/utils'; import { createDefaultCommercialPropertyListing, getDialogWidth, getImageDimensions, map2User, routeListingWithState } from '../../../utils/utils';
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop'; import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { HttpEventType } from '@angular/common/http'; import { HttpEventType } from '@angular/common/http';
@@ -18,9 +18,8 @@ import { DialogModule } from 'primeng/dialog';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog'; import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { EditorModule } from 'primeng/editor'; import { EditorModule } from 'primeng/editor';
import { FileUpload, FileUploadModule } from 'primeng/fileupload'; import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { v4 as uuidv4 } from 'uuid';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty } from '../../../../../../bizmatch-server/src/models/main.model'; import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component'; import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component'; import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
@@ -63,9 +62,7 @@ export class EditCommercialPropertyListingComponent {
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User; user: User;
maxFileSize = 3000000; maxFileSize = 3000000;
uploadUrl: string;
environment = environment; environment = environment;
propertyImages: string[];
responsiveOptions = [ responsiveOptions = [
{ {
breakpoint: '1199px', breakpoint: '1199px',
@@ -131,15 +128,14 @@ export class EditCommercialPropertyListingComponent {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty')); this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
} else { } else {
this.listing = createDefaultCommercialPropertyListing(); this.listing = createDefaultCommercialPropertyListing();
this.listing.userId = await this.userService.getId(keycloakUser.email); const listingUser = await this.userService.getByMail(keycloakUser.email);
this.listing.imagePath = uuidv4(); this.listing.userId = listingUser.id;
this.listing.imagePath = `${emailToDirName(keycloakUser.email)}`;
if (this.data) { if (this.data) {
this.listing.title = this.data?.title; this.listing.title = this.data?.title;
this.listing.description = this.data?.description; this.listing.description = this.data?.description;
} }
} }
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
} }
async save() { async save() {
@@ -155,6 +151,8 @@ export class EditCommercialPropertyListingComponent {
select(event: any) { select(event: any) {
const imageUrl = URL.createObjectURL(event.files[0]); const imageUrl = URL.createObjectURL(event.files[0]);
getImageDimensions(imageUrl).then(dimensions => {
const dialogWidth = getDialogWidth(dimensions);
this.dialogRef = this.dialogService.open(ImageCropperComponent, { this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: { data: {
imageUrl: imageUrl, imageUrl: imageUrl,
@@ -162,7 +160,7 @@ export class EditCommercialPropertyListingComponent {
ratioVariable: false, ratioVariable: false,
}, },
header: 'Edit Image', header: 'Edit Image',
width: '50vw', width: dialogWidth,
modal: true, modal: true,
closeOnEscape: true, closeOnEscape: true,
keepInViewport: true, keepInViewport: true,
@@ -176,12 +174,13 @@ export class EditCommercialPropertyListingComponent {
if (cropper) { if (cropper) {
this.loadingService.startLoading('uploadImage'); this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async blob => { cropper.getCroppedCanvas().toBlob(async blob => {
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.imagePath).subscribe( this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
async event => { async event => {
if (event.type === HttpEventType.Response) { if (event.type === HttpEventType.Response) {
this.ts = new Date().getTime();
console.log('Upload abgeschlossen', event.body); console.log('Upload abgeschlossen', event.body);
this.loadingService.stopLoading('uploadImage'); this.loadingService.stopLoading('uploadImage');
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id); this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
} }
}, },
error => console.error('Fehler beim Upload:', error), error => console.error('Fehler beim Upload:', error),
@@ -190,6 +189,7 @@ export class EditCommercialPropertyListingComponent {
cropper.destroy(); cropper.destroy();
} }
}); });
});
} }
deleteConfirm(imageName: string) { deleteConfirm(imageName: string) {
@@ -205,9 +205,8 @@ export class EditCommercialPropertyListingComponent {
accept: async () => { accept: async () => {
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName); this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, imageName), this.listingsService.save(this.listing, 'commercialProperty')]); await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' }); this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
}, },
reject: () => { reject: () => {
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' }); // this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
@@ -217,8 +216,7 @@ export class EditCommercialPropertyListingComponent {
} }
onDrop(event: { previousIndex: number; currentIndex: number }) { onDrop(event: { previousIndex: number; currentIndex: number }) {
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex); moveItemInArray(this.listing.imageOrder, event.previousIndex, event.currentIndex);
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages);
} }
changeListingCategory(value: 'business' | 'commercialProperty') { changeListingCategory(value: 'business' | 'commercialProperty') {
routeListingWithState(this.router, value, this.listing); routeListingWithState(this.router, value, this.listing);

View File

@@ -1,4 +1,3 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full"> <div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
<div class="p-fluid flex flex-column lg:flex-row"> <div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account> <menu-account></menu-account>
@@ -6,27 +5,27 @@
<div class="text-900 font-semibold text-lg mt-3">Contact Us</div> <div class="text-900 font-semibold text-lg mt-3">Contact Us</div>
<p-divider></p-divider> <p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row"> <div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid"> <div class="flex-auto p-fluid formgrid">
<div class="grid"> <div class="grid">
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
<label for="name" class="block font-medium text-900 mb-2">Your name</label> <label for="name" class="block font-medium text-900 mb-2">Your name</label>
<input id="name" type="text" pInputText> <input [ngClass]="{ 'ng-invalid': containsError('name'), 'ng-dirty': containsError('name') }" name="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
</div> </div>
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
<label for="email" class="block font-medium text-900 mb-2">Your Email</label> <label for="email" class="block font-medium text-900 mb-2">Your Email</label>
<input id="email" type="text" pInputText> <input name="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email" />
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="phone" class="block font-medium text-900 mb-2">Your Phone</label> <label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone</label>
<input id="phone" type="text" pInputText> <p-inputMask id="phoneNumber" name="phoneNumber" mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
</div> </div>
<div class="mb-4"> <div class="mb-4">
<label for="help" class="block font-medium text-900 mb-2">How can we help you ?</label> <label for="help" class="block font-medium text-900 mb-2">How can we help you ?</label>
<textarea id="help" type="text" pInputTextarea rows="5" [autoResize]="true"></textarea> <textarea name="help" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="mailinfo.sender.comments"></textarea>
</div> </div>
<div> <div>
<button pButton pRipple label="Submit" class="w-auto"></button> <button pButton pRipple label="Submit" class="w-auto" (click)="mail()"></button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,28 +1,50 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button'; import { KeycloakService } from 'keycloak-angular';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext'; import { MessageService } from 'primeng/api';
import { StyleClassModule } from 'primeng/styleclass'; import { InputMaskModule } from 'primeng/inputmask';
import { SelectOptionsService } from '../../../services/select-options.service'; import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { DropdownModule } from 'primeng/dropdown'; import { ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { FormsModule } from '@angular/forms'; import { environment } from '../../../../environments/environment';
import { CommonModule } from '@angular/common'; import { MailService } from '../../../services/mail.service';
import { ToggleButtonModule } from 'primeng/togglebutton'; import { UserService } from '../../../services/user.service';
import { TagModule } from 'primeng/tag'; import { SharedModule } from '../../../shared/shared/shared.module';
import data from '../../../../assets/data/user.json'; import { map2User } from '../../../utils/utils';
import { ActivatedRoute } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
import { DividerModule } from 'primeng/divider';
import { TableModule } from 'primeng/table';
@Component({ @Component({
selector: 'app-email-us', selector: 'app-email-us',
standalone: true, standalone: true,
imports: [CommonModule, StyleClassModule, MenuAccountComponent, DividerModule,ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule], imports: [SharedModule, InputMaskModule],
providers: [MessageService],
templateUrl: './email-us.component.html', templateUrl: './email-us.component.html',
styleUrl: './email-us.component.scss' styleUrl: './email-us.component.scss',
}) })
export class EmailUsComponent { export class EmailUsComponent {
mailinfo: MailInfo;
keycloakUser: KeycloakUser;
user: User;
errorResponse: ErrorResponse;
constructor(private mailService: MailService, private userService: UserService, public keycloakService: KeycloakService, private messageService: MessageService) {
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
}
async ngOnInit() {
const token = await this.keycloakService.getToken();
this.keycloakUser = map2User(token);
if (this.keycloakUser) {
this.user = await this.userService.getByMail(this.keycloakUser.email);
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
}
}
async mail() {
this.mailinfo.email = 'support@bizmatch.net';
const result = await this.mailService.mail(this.mailinfo);
if (result) {
this.errorResponse = result as ErrorResponse;
} else {
this.errorResponse = null;
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Your request has been forwarded to the support team of bizmatch.', life: 3000 });
}
}
containsError(fieldname: string) {
return this.errorResponse?.fields.map(f => f.fieldname).includes(fieldname);
}
} }

View File

@@ -36,7 +36,7 @@ export class MyListingComponent {
const keycloakUser = map2User(token); const keycloakUser = map2User(token);
const email = keycloakUser.email; const email = keycloakUser.email;
this.user = await this.userService.getByMail(email); this.user = await this.userService.getByMail(email);
const result = await Promise.all([await this.listingsService.getListingByUserId(this.user.id, 'business'), await this.listingsService.getListingByUserId(this.user.id, 'commercialProperty')]); const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
this.myListings = [...result[0], ...result[1]]; this.myListings = [...result[0], ...result[1]];
} }
@@ -46,7 +46,7 @@ export class MyListingComponent {
} else { } else {
await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath); await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath);
} }
const result = await Promise.all([await this.listingsService.getListingByUserId(this.user.id, 'business'), await this.listingsService.getListingByUserId(this.user.id, 'commercialProperty')]); const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
this.myListings = [...result[0], ...result[1]]; this.myListings = [...result[0], ...result[1]];
} }

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { ImageType } from '../../../../bizmatch-server/src/models/main.model'; import { emailToDirName } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@Injectable({ @Injectable({
@@ -12,33 +12,29 @@ export class ImageService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string) { uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string, serialId?: number) {
const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`; let uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
if (type === 'uploadPropertyPicture') {
uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}/${serialId}`;
}
const formData = new FormData(); const formData = new FormData();
formData.append('file', imageBlob, 'image.png'); formData.append('file', imageBlob, 'image.png');
return this.http.post(uploadUrl, formData, { return this.http.post(uploadUrl, formData, {
// headers: this.headers,
//reportProgress: true,
observe: 'events', observe: 'events',
}); });
} }
async deleteUserImage(userid: string, type: ImageType, name?: string) {
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/${type.delete}${userid}`)); async deleteListingImage(imagePath: string, serial: number, name?: string) {
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/propertyPicture/${imagePath}/${serial}/${name}`));
} }
async deleteListingImage(imagePath: string, name?: string) {
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/propertyPicture/${imagePath}/${name}`)); async deleteLogoImagesById(email: string) {
const adjustedEmail = emailToDirName(email);
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/logo/${adjustedEmail}`));
} }
async getProfileImagesForUsers(userids: string[]) { async deleteProfileImagesById(email: string) {
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/profileImages/${userids.join(',')}`)); const adjustedEmail = emailToDirName(email);
} await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/profile/${adjustedEmail}`));
async getCompanyLogosForUsers(userids: string[]) {
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/companyLogos/${userids.join(',')}`));
}
async deleteLogoImagesById(userid: string) {
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/logo/${userid}`));
}
async deleteProfileImagesById(userid: string) {
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/profile/${userid}`));
} }
} }

View File

@@ -20,8 +20,8 @@ export class ListingsService {
const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`); const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`);
return result; return result;
} }
getListingByUserId(userid: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> { getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${userid}`)); return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`));
} }
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') { async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
if (listing.id) { if (listing.id) {
@@ -40,11 +40,4 @@ export class ListingsService {
async deleteCommercialPropertyListing(id: string, imagePath: string) { async deleteCommercialPropertyListing(id: string, imagePath: string) {
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/${id}/${imagePath}`)); await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/${id}/${imagePath}`));
} }
async getPropertyImages(id: string): Promise<string[]> {
return await lastValueFrom(this.http.get<string[]>(`${this.apiBaseUrl}/bizmatch/image/${id}`));
}
async changeImageOrder(id: string, propertyImages: string[]): Promise<string[]> {
return await lastValueFrom(this.http.put<string[]>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/imageOrder/${id}`, propertyImages));
}
} }

View File

@@ -1,9 +1,8 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { InitEditableRow } from 'primeng/table'; import { Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
import { KeyValue, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model'; import { KeyValue, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
@@ -13,15 +12,14 @@ export class SelectOptionsService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
async init() { async init() {
const allSelectOptions = await lastValueFrom( const allSelectOptions = await lastValueFrom(this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`));
this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`)
);
this.typesOfBusiness = allSelectOptions.typesOfBusiness; this.typesOfBusiness = allSelectOptions.typesOfBusiness;
this.prices = allSelectOptions.prices; this.prices = allSelectOptions.prices;
this.listingCategories = allSelectOptions.listingCategories; this.listingCategories = allSelectOptions.listingCategories;
this.categories = allSelectOptions.categories; this.customerTypes = allSelectOptions.customerTypes;
this.states = allSelectOptions.locations; this.states = allSelectOptions.locations;
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty this.gender = allSelectOptions.gender;
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty;
} }
public typesOfBusiness: Array<KeyValueStyle>; public typesOfBusiness: Array<KeyValueStyle>;
@@ -31,49 +29,41 @@ export class SelectOptionsService {
public listingCategories: Array<KeyValue>; public listingCategories: Array<KeyValue>;
public categories: Array<KeyValueStyle>; public customerTypes: Array<KeyValue>;
public gender: Array<KeyValue>;
public states: Array<any>; public states: Array<any>;
getState(value: string): string { getState(value: string): string {
return this.states.find(l=>l.value===value)?.name return this.states.find(l => l.value === value)?.name;
} }
getBusiness(value: number): string { getBusiness(value: number): string {
return this.typesOfBusiness.find(t=>t.value===String(value))?.name return this.typesOfBusiness.find(t => t.value === String(value))?.name;
} }
getCommercialProperty(value: number): string { getCommercialProperty(value: number): string {
return this.typesOfCommercialProperty.find(t=>t.value===String(value))?.name return this.typesOfCommercialProperty.find(t => t.value === String(value))?.name;
} }
getListingsCategory(value: string): string { getListingsCategory(value: string): string {
return this.listingCategories.find(l=>l.value===value)?.name return this.listingCategories.find(l => l.value === value)?.name;
} }
getCategory(value:string):string{ getCustomerType(value: string): string {
return this.categories.find(c=>c.value===value)?.name return this.customerTypes.find(c => c.value === value)?.name;
} }
getIcon(value:string):string{ getGender(value: string): string {
return this.categories.find(c=>c.value===value)?.icon return this.gender.find(c => c.value === value)?.name;
}
getTextColor(value:string):string{
return this.categories.find(c=>c.value===value)?.textColorClass
}
getBgColor(value:string):string{
return this.categories.find(c=>c.value===value)?.bgColorClass
}
getIconAndTextColor(value:string):string{
const category = this.categories.find(c=>c.value===value)
return `${category?.icon} ${category?.textColorClass}`
} }
getIconType(value: string): string { getIconType(value: string): string {
return this.typesOfBusiness.find(c=>c.value===value)?.icon return this.typesOfBusiness.find(c => c.value === value)?.icon;
} }
getTextColorType(value: string): string { getTextColorType(value: string): string {
return this.typesOfBusiness.find(c=>c.value===value)?.textColorClass return this.typesOfBusiness.find(c => c.value === value)?.textColorClass;
} }
getBgColorType(value: number): string { getBgColorType(value: number): string {
return this.typesOfBusiness.find(c=>c.value===String(value))?.bgColorClass return this.typesOfBusiness.find(c => c.value === String(value))?.bgColorClass;
} }
getIconAndTextColorType(value: number): string { getIconAndTextColorType(value: number): string {
const category = this.typesOfBusiness.find(c=>c.value===String(value)) const category = this.typesOfBusiness.find(c => c.value === String(value));
return `${category?.icon} ${category?.textColorClass}` return `${category?.icon} ${category?.textColorClass}`;
} }
} }

View File

@@ -33,13 +33,13 @@ export class UserService {
async getAllStates(): Promise<any> { async getAllStates(): Promise<any> {
return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`)); return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
} }
async getId(email: string): Promise<string> { // async getId(email: string): Promise<string> {
if (sessionStorage.getItem('USERID')) { // if (sessionStorage.getItem('USERID')) {
return sessionStorage.getItem('USERID'); // return sessionStorage.getItem('USERID');
} else { // } else {
const user = await this.getByMail(email); // const user = await this.getByMail(email);
sessionStorage.setItem('USERID', user.id); // sessionStorage.setItem('USERID', user.id);
return user.id; // return user.id;
} // }
} // }
} }

View File

@@ -87,7 +87,14 @@ export function createLogger(name: string, level: number = INFO, options: any =
...options, ...options,
}); });
} }
export function formatPhoneNumber(phone: string): string {
const cleaned = ('' + phone).replace(/\D/g, '');
const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
if (match) {
return '(' + match[1] + ') ' + match[2] + '-' + match[3];
}
return phone;
}
export const getSessionStorageHandler = function (path, value, previous, applyData) { export const getSessionStorageHandler = function (path, value, previous, applyData) {
sessionStorage.setItem('criteria', JSON.stringify(this)); sessionStorage.setItem('criteria', JSON.stringify(this));
}; };
@@ -130,3 +137,25 @@ export function map2User(jwt: string): KeycloakUser {
return null; return null;
} }
} }
export function getImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> {
return new Promise(resolve => {
const img = new Image();
img.onload = () => {
resolve({ width: img.width, height: img.height });
};
img.src = imageUrl;
});
}
export function getDialogWidth(dimensions): string {
const aspectRatio = dimensions.width / dimensions.height;
let dialogWidth = '50vw'; // Standardbreite
// Passen Sie die Breite basierend auf dem Seitenverhältnis an
if (aspectRatio < 1) {
dialogWidth = '30vw'; // Hochformat
} else if (aspectRatio > 1) {
dialogWidth = '50vw'; // Querformat
}
return dialogWidth;
}