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.production.local
.env.local
.env.prod
.env.dev
.env.test
# temp directory
.temp

View File

@@ -56,3 +56,4 @@ pids
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
pictures
pictures_base

View File

@@ -6,17 +6,15 @@
"request": "launch",
"name": "Debug Nest Framework",
"runtimeExecutable": "npm",
"runtimeArgs": [
"run",
"start:debug",
"--",
"--inspect-brk"
],
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
"autoAttachChildProcesses": true,
"restart": true,
"sourceMaps": true,
"stopOnEntry": false,
"console": "integratedTerminal",
"env": {
"HOST_NAME": "localhost"
}
},
{
"type": "node",
@@ -24,9 +22,7 @@
"name": "Debug Current TS File",
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
"sourceMaps": true,
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart"
@@ -35,31 +31,21 @@
"type": "node",
"request": "launch",
"name": "generateDefs",
"skipFiles": [
"<node_internals>/**"
],
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
"outFiles": [
"${workspaceFolder}/dist/src/drizzle/**/*.js"
],
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
"sourceMaps": true,
"smartStep": true,
},
{
"type": "node",
"request": "launch",
"name": "generateTypes",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
"outFiles": [
"${workspaceFolder}/dist/src/drizzle/**/*.js"
],
"sourceMaps": true,
"smartStep": true,
},
"smartStep": true
},
{
"type": "node",
"request": "launch",
"name": "generateTypes",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
"sourceMaps": true,
"smartStep": true
}
]
}

View File

@@ -9,10 +9,10 @@
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start": "HOST_NAME=localhost nest start",
"start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"start:prod": "HOST_NAME=www.bizmatch.net node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
@@ -37,7 +37,9 @@
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.30.8",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8",
"jwks-rsa": "^3.1.0",
"ky": "^1.2.0",
"nest-winston": "^1.9.4",
"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 { AuthService } from './auth/auth.service.js';
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
constructor(
private readonly appService: AppService,
private authService: AuthService,
) {}
@UseGuards(JwtAuthGuard)
@Get()
getHello(): string {
return this.appService.getHello();
getHello(@Request() req): string {
return req.user;
//return 'dfgdf';
}
}

View File

@@ -1,5 +1,8 @@
import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport';
import * as dotenv from 'dotenv';
import fs from 'fs-extra';
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
import path from 'path';
import { fileURLToPath } from 'url';
@@ -14,12 +17,37 @@ import { ListingsModule } from './listings/listings.module.js';
import { MailModule } from './mail/mail.module.js';
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
import { SelectOptionsModule } from './select-options/select-options.module.js';
import { UserModule } from './user/user.module.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function loadEnvFiles() {
// Load the .env file
dotenv.config();
console.log('Loaded .env file');
// Determine which additional env file to load
let envFilePath = '';
const host = process.env.HOST_NAME || '';
if (host.includes('localhost')) {
envFilePath = '.env.local';
} else if (host.includes('dev.bizmatch.net')) {
envFilePath = '.env.dev';
} else if (host.includes('www.bizmatch.net') || host.includes('bizmatch.net')) {
envFilePath = '.env.prod';
}
// Load the additional env file if it exists
if (fs.existsSync(envFilePath)) {
dotenv.config({ path: envFilePath });
console.log(`Loaded ${envFilePath} file`);
} else {
console.log(`No additional .env file found for HOST_NAME: ${host}`);
}
}
loadEnvFiles();
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
@@ -46,6 +74,7 @@ const __dirname = path.dirname(__filename);
ListingsModule,
SelectOptionsModule,
ImageModule,
PassportModule,
],
controllers: [AppController],
providers: [AppService, FileService],

View File

@@ -1,17 +1,16 @@
import { Module } from '@nestjs/common';
import { MailerModule } from '@nestjs-modules/mailer';
import path, { join } from 'path';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
import { PassportModule } from '@nestjs/passport';
import path from 'path';
import { fileURLToPath } from 'url';
import { AuthService } from './auth.service.js';
import { JwtStrategy } from '../jwt.strategy.js';
import { AuthController } from './auth.controller.js';
import { AuthService } from './auth.service.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@Module({
imports: [
],
providers: [AuthService],
imports: [PassportModule],
providers: [AuthService, JwtStrategy],
controllers: [AuthController],
exports:[AuthService]
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -1,11 +1,13 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
import fs from 'fs-extra';
import { join } from 'path';
import pkg from 'pg';
import { rimraf } from 'rimraf';
import sharp from 'sharp';
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';
const { Pool } = pkg;
@@ -32,6 +34,11 @@ const targetPathProfile = `./pictures/profile`;
deleteFilesOfDir(targetPathProfile);
const targetPathLogo = `./pictures/logo`;
deleteFilesOfDir(targetPathLogo);
const targetPathProperty = `./pictures/property`;
deleteFilesOfDir(targetPathProperty);
fs.ensureDirSync(`./pictures/logo`);
fs.ensureDirSync(`./pictures/profile`);
fs.ensureDirSync(`./pictures/property`);
for (const userData of usersData) {
const user: User = { firstname: '', lastname: '', email: '' };
user.licensedIn = [];
@@ -58,20 +65,21 @@ for (const userData of usersData) {
user.gender = userData.gender;
user.created = new Date();
user.updated = new Date();
const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender });
generatedUserData.push(u[0].insertedId);
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]);
i++;
if (u[0].gender === 'male') {
male++;
const data = readFileSync(`./pictures/profile_base/Mann_${male}.jpg`);
await storeProfilePicture(data, u[0].insertedId);
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
await storeProfilePicture(data, emailToDirName(u[0].email));
} else {
female++;
const data = readFileSync(`./pictures/profile_base/Frau_${female}.jpg`);
await storeProfilePicture(data, u[0].insertedId);
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
await storeProfilePicture(data, emailToDirName(u[0].email));
}
const data = readFileSync(`./pictures/logos_base/${i}.jpg`);
await storeCompanyLogo(data, u[0].insertedId);
const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
await storeCompanyLogo(data, emailToDirName(u[0].email));
}
//Business Listings
filePath = `./data/businesses.json`;
@@ -82,7 +90,9 @@ for (const business of businessJsonData) {
delete business.id;
business.created = 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);
}
//Corporate Listings
@@ -92,14 +102,21 @@ const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // E
for (const commercial of commercialJsonData) {
const id = commercial.id;
delete commercial.id;
const user = getRandomItem(generatedUserData);
commercial.imageOrder = getFilenames(id);
commercial.imagePath = id;
commercial.imagePath = emailToDirName(user.email);
const insertionDate = getRandomDateWithinLastYear();
commercial.created = insertionDate;
commercial.updated = insertionDate;
commercial.userId = getRandomItem(generatedUserData);
await db.insert(schema.commercials).values(commercial);
commercial.userId = user.insertedId;
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
@@ -115,7 +132,7 @@ function getRandomItem<T>(arr: T[]): T {
}
function getFilenames(id: string): string[] {
try {
let filePath = `./pictures/property/${id}`;
let filePath = `./pictures_base/property/${id}`;
return readdirSync(filePath);
} catch (e) {
return null;
@@ -141,14 +158,14 @@ async function storeProfilePicture(buffer: Buffer, userId: string) {
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;
const output = await sharp(buffer)
.resize({ width: 300 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`./pictures/logo/${userId}.avif`); // Ersetze Dateierweiterung
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
}

View File

@@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS "businesses" (
"reasonForSale" varchar(255),
"brokerLicencing" varchar(255),
"internals" text,
"imagePath" varchar(200),
"created" timestamp,
"updated" timestamp,
"visits" integer,
@@ -36,6 +37,7 @@ CREATE TABLE IF NOT EXISTS "businesses" (
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "commercials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"serial_id" serial NOT NULL,
"userId" uuid,
"type" integer,
"title" varchar(255),
@@ -53,7 +55,7 @@ CREATE TABLE IF NOT EXISTS "commercials" (
"website" varchar(255),
"phoneNumber" varchar(255),
"imageOrder" varchar(200)[],
"imagePath" varchar(50),
"imagePath" varchar(200),
"created" timestamp,
"updated" timestamp,
"visits" integer,
@@ -76,7 +78,9 @@ CREATE TABLE IF NOT EXISTS "users" (
"hasProfile" boolean,
"hasCompanyLogo" boolean,
"licensedIn" jsonb,
"gender" "gender"
"gender" "gender",
"created" timestamp,
"updated" timestamp
);
--> statement-breakpoint
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",
"version": "5",
"dialect": "pg",
@@ -147,6 +147,12 @@
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
@@ -202,6 +208,12 @@
"notNull": true,
"default": "gen_random_uuid()"
},
"serial_id": {
"name": "serial_id",
"type": "serial",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
@@ -306,7 +318,7 @@
},
"imagePath": {
"name": "imagePath",
"type": "varchar(50)",
"type": "varchar(200)",
"primaryKey": false,
"notNull": false
},
@@ -454,6 +466,18 @@
"type": "gender",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},

View File

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

View File

@@ -5,15 +5,15 @@
{
"idx": 0,
"version": "5",
"when": 1715627517508,
"tag": "0000_open_hannibal_king",
"when": 1716495198537,
"tag": "0000_burly_bruce_banner",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1715631674334,
"tag": "0001_charming_thundra",
"when": 1717085220861,
"tag": "0001_wet_mephistopheles",
"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';
export const PG_CONNECTION = 'PG_CONNECTION';
export const genderEnum = pgEnum('gender', ['male', 'female']);
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'broker', 'professional']);
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
firstname: varchar('firstname', { length: 255 }).notNull(),
@@ -20,6 +22,7 @@ export const users = pgTable('users', {
hasCompanyLogo: boolean('hasCompanyLogo'),
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
gender: genderEnum('gender'),
customerType: customerTypeEnum('customerType'),
created: timestamp('created'),
updated: timestamp('updated'),
});
@@ -48,6 +51,7 @@ export const businesses = pgTable('businesses', {
reasonForSale: varchar('reasonForSale', { length: 255 }),
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
internals: text('internals'),
imageName: varchar('imagePath', { length: 200 }),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
@@ -56,6 +60,7 @@ export const businesses = pgTable('businesses', {
export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(),
serialId: serial('serial_id'),
userId: uuid('userId').references(() => users.id),
type: integer('type'),
title: varchar('title', { length: 255 }),
@@ -73,7 +78,7 @@ export const commercials = pgTable('commercials', {
website: varchar('website', { length: 255 }),
phoneNumber: varchar('phoneNumber', { length: 255 }),
imageOrder: varchar('imageOrder', { length: 200 }).array(),
imagePath: varchar('imagePath', { length: 50 }),
imagePath: varchar('imagePath', { length: 200 }),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),

View File

@@ -20,6 +20,9 @@ export class FileService {
fs.ensureDirSync(`./pictures/logo`);
fs.ensureDirSync(`./pictures/property`);
}
// ############
// Subscriptions
// ############
private loadSubscriptions(): void {
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
const rawData = readFileSync(filePath, 'utf8');
@@ -28,36 +31,43 @@ export class FileService {
getSubscriptions(): Subscription[] {
return this.subscriptions;
}
async storeProfilePicture(file: Express.Multer.File, userId: string) {
// ############
// Profile
// ############
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
let quality = 50;
const output = await sharp(file.buffer)
.resize({ width: 300 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
await sharp(output).toFile(`./pictures/profile/${adjustedEmail}.avif`);
}
hasProfile(userId: string) {
return fs.existsSync(`./pictures/profile/${userId}.avif`);
hasProfile(adjustedEmail: string) {
return fs.existsSync(`./pictures/profile/${adjustedEmail}.avif`);
}
async storeCompanyLogo(file: Express.Multer.File, userId: string) {
// ############
// Logo
// ############
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
let quality = 50;
const output = await sharp(file.buffer)
.resize({ width: 300 })
.avif({ quality }) // Verwende AVIF
//.webp({ quality }) // Verwende Webp
.toBuffer();
await sharp(output).toFile(`./pictures/logo/${userId}.avif`); // Ersetze Dateierweiterung
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
}
hasCompanyLogo(userId: string) {
return fs.existsSync(`./pictures/logo/${userId}.avif`) ? true : false;
hasCompanyLogo(adjustedEmail: string) {
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 directory = `./pictures/property/${listingId}`;
const directory = `./pictures/property/${imagePath}/${serial}`;
if (fs.existsSync(directory)) {
const files = await fs.readdir(directory);
files.forEach(f => {
@@ -68,9 +78,9 @@ export class FileService {
return [];
}
}
async hasPropertyImages(listingId: string): Promise<boolean> {
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
const result: ImageProperty[] = [];
const directory = `./pictures/property/${listingId}`;
const directory = `./pictures/property/${imagePath}/${serial}`;
if (fs.existsSync(directory)) {
const files = await fs.readdir(directory);
return files.length > 0;
@@ -78,15 +88,18 @@ export class FileService {
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 directory = `./pictures/property/${listingId}`;
const directory = `./pictures/property/${imagePath}/${serial}`;
fs.ensureDirSync(`${directory}`);
const imageName = await this.getNextImageName(directory);
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
return `${imageName}.avif`;
}
// ############
// utils
// ############
async getNextImageName(directory) {
try {
const files = await fs.readdir(directory);
@@ -115,22 +128,6 @@ export class FileService {
let timeTaken = Date.now() - start;
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
}
getProfileImagesForUsers(userids: string) {
const ids = userids.split(',');
let result = {};
for (const id of ids) {
result = { ...result, [id]: fs.existsSync(`./pictures/profile/${id}.avif`) };
}
return result;
}
getCompanyLogosForUsers(userids: string) {
const ids = userids.split(',');
let result = {};
for (const id of ids) {
result = { ...result, [id]: fs.existsSync(`./pictures/logo/${id}.avif`) };
}
return result;
}
deleteImage(path: string) {
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 { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
@@ -6,9 +6,6 @@ import { FileService } from '../file/file.service.js';
import { ListingsService } from '../listings/listings.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')
export class ImageController {
constructor(
@@ -17,58 +14,42 @@ export class ImageController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
private selectOptions: SelectOptionsService,
) {}
@Post('uploadPropertyPicture/:imagePath')
// ############
// Property
// ############
@Post('uploadPropertyPicture/:imagePath/:serial')
@UseInterceptors(FileInterceptor('file'))
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string) {
const imagename = await this.fileService.storePropertyPicture(file, imagePath);
await this.listingService.addImage(imagePath, imagename);
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
await this.listingService.addImage(imagePath, serial, imagename);
}
@Post('uploadProfile/:id')
@Delete('propertyPicture/:imagePath/:serial/:imagename')
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
await this.listingService.deleteImage(imagePath, serial, imagename);
}
// ############
// Profile
// ############
@Post('uploadProfile/:email')
@UseInterceptors(FileInterceptor('file'))
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
await this.fileService.storeProfilePicture(file, id);
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
await this.fileService.storeProfilePicture(file, adjustedEmail);
}
@Post('uploadCompanyLogo/:id')
@Delete('profile/:email/')
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
}
// ############
// Logo
// ############
@Post('uploadCompanyLogo/:email')
@UseInterceptors(FileInterceptor('file'))
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
await this.fileService.storeCompanyLogo(file, id);
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
await this.fileService.storeCompanyLogo(file, adjustedEmail);
}
@Get(':id')
async getPropertyImagesById(@Param('id') id: string): Promise<any> {
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`);
@Delete('logo/:email/')
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.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 { Logger } from 'winston';
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';
@Controller('listings/business')
@@ -12,18 +13,22 @@ export class BusinessListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
@UseGuards(OptionalJwtAuthGuard)
@Get(':id')
findById(@Param('id') id: string): any {
return this.listingsService.findById(id, businesses);
}
@Get('user/:userid')
findByUserId(@Param('userid') userid: string): any {
return this.listingsService.findByUserId(userid, businesses);
findById(@Request() req, @Param('id') id: string): any {
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
}
@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')
find(@Body() criteria: ListingCriteria): any {
return this.listingsService.findListingsByCriteria(criteria, businesses);
find(@Request() req, @Body() criteria: ListingCriteria): any {
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
}
@Post()
@@ -34,7 +39,7 @@ export class BusinessListingsController {
@Put()
update(@Body() listing: any) {
this.logger.info(`Save Listing`);
return this.listingsService.updateListing(listing.id, listing, businesses);
return this.listingsService.updateBusinessListing(listing.id, listing);
}
@Delete(':id')
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 { CommercialPropertyListing } from 'src/models/db.model.js';
import { Logger } from 'winston';
import { commercials } from '../drizzle/schema.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';
@Controller('listings/commercialProperty')
@@ -14,17 +16,21 @@ export class CommercialPropertyListingsController {
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
@UseGuards(OptionalJwtAuthGuard)
@Get(':id')
findById(@Param('id') id: string): any {
return this.listingsService.findById(id, commercials);
findById(@Request() req, @Param('id') id: string): any {
return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
}
@Get('user/:userid')
findByUserId(@Param('userid') userid: string): any {
return this.listingsService.findByUserId(userid, commercials);
@UseGuards(OptionalJwtAuthGuard)
@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')
async find(@Body() criteria: ListingCriteria): Promise<any> {
return await this.listingsService.findListingsByCriteria(criteria, commercials);
async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> {
return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
}
@Get('states/all')
getStates(): any {
@@ -38,16 +44,11 @@ export class CommercialPropertyListingsController {
@Put()
async update(@Body() listing: any) {
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')
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
this.listingsService.deleteListing(id, commercials);
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 { AuthModule } from '../auth/auth.module.js';
import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { FileService } from '../file/file.service.js';
import { UserService } from '../user/user.service.js';
@@ -9,7 +10,7 @@ import { ListingsService } from './listings.service.js';
import { UnknownListingsController } from './unknown-listings.controller.js';
@Module({
imports: [DrizzleModule],
imports: [DrizzleModule, AuthModule],
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
providers: [ListingsService, FileService, UserService],
exports: [ListingsService],

View File

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

View File

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

View File

@@ -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 { AppModule } from './app.module.js';
import * as express from 'express';
import path, { join } from 'path';
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import { AppModule } from './app.module.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function bootstrap() {
const server = express();
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('bizmatch');
app.enableCors({
origin: '*',
//origin: 'http://localhost:4200', // Die URL Ihrer Angular-App
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: 'Content-Type, Accept',
allowedHeaders: 'Content-Type, Accept, Authorization',
});
//origin: 'http://localhost:4200',
await app.listen(3000);

View File

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

View File

@@ -83,6 +83,11 @@ export interface KeycloakUser {
notBefore?: number;
access?: Access;
}
export interface JwtUser {
userId: string;
username: string;
roles: string[];
}
export interface Access {
manageGroupMembership: boolean;
view: boolean;
@@ -130,6 +135,14 @@ export interface JwtToken {
email: string;
user_id: string;
}
export interface JwtPayload {
sub: string;
preferred_username: string;
realm_access?: {
roles?: string[];
};
[key: string]: any; // für andere optionale Felder im JWT-Payload
}
interface Resourceaccess {
account: Realmaccess;
}
@@ -193,6 +206,9 @@ export function isEmpty(value: any): boolean {
return false;
}
export function emailToDirName(email: string): string {
if (email === undefined || email === null) {
return null;
}
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');

View File

@@ -1,5 +1,5 @@
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
import { NextFunction, Request, Response } from 'express';
@Injectable()
export class RequestDurationMiddleware implements NestMiddleware {
@@ -8,8 +8,17 @@ export class RequestDurationMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
// const duration = Date.now() - start;
// this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
const duration = Date.now() - start;
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();
}

View File

@@ -3,16 +3,16 @@ import { SelectOptionsService } from './select-options.service.js';
@Controller('select-options')
export class SelectOptionsController {
constructor(private selectOptionsService:SelectOptionsService){}
@Get()
getSelectOption():any{
return {
typesOfBusiness:this.selectOptionsService.typesOfBusiness,
prices:this.selectOptionsService.prices,
listingCategories:this.selectOptionsService.listingCategories,
categories:this.selectOptionsService.categories,
locations:this.selectOptionsService.locations,
typesOfCommercialProperty:this.selectOptionsService.typesOfCommercialProperty,
}
}
constructor(private selectOptionsService: SelectOptionsService) {}
@Get()
getSelectOption(): any {
return {
typesOfBusiness: this.selectOptionsService.typesOfBusiness,
prices: this.selectOptionsService.prices,
listingCategories: this.selectOptionsService.listingCategories,
customerTypes: this.selectOptionsService.customerTypes,
locations: this.selectOptionsService.locations,
typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty,
};
}
}

View File

@@ -39,9 +39,14 @@ export class SelectOptionsService {
{ name: 'Business', value: 'business' },
{ name: 'Commercial Property', value: 'commercialProperty' },
];
public categories: Array<KeyValueStyle> = [
{ name: 'Broker', value: 'broker', icon: 'pi-image', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' },
{ name: 'Professional', value: 'professional', icon: 'pi-globe', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
public customerTypes: Array<KeyValue> = [
{ name: 'Buyer', value: 'buyer' },
{ name: 'Broker', value: 'broker' },
{ name: 'Professional', value: 'professional' },
];
public gender: Array<KeyValue> = [
{ name: 'Male', value: 'male' },
{ name: 'Female', value: 'female' },
];
public imageTypes: ImageType[] = [
{ 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 { FileService } from '../file/file.service.js';
import { User } from '../models/db.model.js';
import { ListingCriteria } from '../models/main.model.js';
import { ListingCriteria, emailToDirName } from '../models/main.model.js';
@Injectable()
export class UserService {
@@ -33,8 +33,8 @@ export class UserService {
.from(schema.users)
.where(sql`email = ${email}`)) as User[];
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(user.id);
user.hasProfile = this.fileService.hasProfile(user.id);
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user;
}
async getUserById(id: string) {
@@ -43,8 +43,8 @@ export class UserService {
.from(schema.users)
.where(sql`id = ${id}`)) as User[];
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(id);
user.hasProfile = this.fileService.hasProfile(id);
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return 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 { provideAnimations } from '@angular/platform-browser/animations';
import { KeycloakService } from 'keycloak-angular';
import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
import { environment } from '../environments/environment';
import { customKeycloakAdapter } from '../keycloak';
import { routes } from './app.routes';
@@ -37,6 +37,11 @@ export const appConfig: ApplicationConfig = {
useClass: LoadingInterceptor,
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: KeycloakBearerInterceptor,
multi: true,
},
provideRouter(
routes,
withEnabledBlockingInitialNavigation(),
@@ -89,6 +94,10 @@ function initializeKeycloak(keycloak: KeycloakService) {
onLoad: 'check-sso',
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
},
bearerExcludedUrls: ['/assets'],
shouldUpdateToken(request) {
return !request.headers.get('token-update') === false;
},
});
logger.info(`+++>${authenticated}`);
};

View File

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

View File

@@ -18,4 +18,7 @@ export class FooterComponent {
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',
routerLink: '/businessListings',
state: {},
},
{
label: 'Commercial Property',
routerLink: '/commercialPropertyListings',
state: {},
},
{
label: 'Professionals/Brokers Directory',
routerLink: '/brokerListings',
state: {},
},
];
this.loginItems = [

View File

@@ -1,15 +1,14 @@
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="cropperConfig"></angular-cropper>
<div class="flex justify-content-between mt-3">
@if(ratioVariable){
<div>
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)"
optionLabel="label" optionValue="value"></p-selectButton>
</div>
} @else {
<div></div>
}
<div>
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true"></p-button>
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button>
</div>
@if(ratioVariable){
<div>
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)" optionLabel="label" optionValue="value" class="small"></p-selectButton>
</div>
} @else {
<div></div>
}
<div class="flex justify-content-between">
<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" size="small"></p-button>
</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,67 +1,52 @@
import { Component, ViewChild } from '@angular/core';
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
import { LoadingService } from '../../services/loading.service';
import { ImageService } from '../../services/image.service';
import { HttpEventType } from '@angular/common/http';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { environment } from '../../../environments/environment';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { SharedModule } from '../../shared/shared/shared.module';
import { SelectButtonModule } from 'primeng/selectbutton';
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
export const stateOptions:KeyValueRatio[]=[
{label:'16/9',value:16/9},
{label:'1/1',value:1},
{label:'2/3',value:2/3},
]
import { ImageService } from '../../services/image.service';
import { LoadingService } from '../../services/loading.service';
import { SharedModule } from '../../shared/shared/shared.module';
export const stateOptions: KeyValueRatio[] = [
{ label: '16/9', value: 16 / 9 },
{ label: '1/1', value: 1 },
{ label: 'Free', value: NaN },
];
@Component({
selector: 'app-image-cropper',
standalone: true,
imports: [SharedModule,FileUploadModule,AngularCropperjsModule,SelectButtonModule],
imports: [SharedModule, FileUploadModule, AngularCropperjsModule, SelectButtonModule],
templateUrl: './image-cropper.component.html',
styleUrl: './image-cropper.component.scss'
styleUrl: './image-cropper.component.scss',
})
export class ImageCropperComponent {
@ViewChild(CropperComponent) public angularCropper: CropperComponent;
imageUrl:string; //wird im Template verwendet
fileUpload:FileUpload
value:number = stateOptions[0].value;
cropperConfig={aspectRatio: this.value}
ratioVariable:boolean
stateOptions=stateOptions
constructor(
private loadingService:LoadingService,
private imageUploadService: ImageService,
public config: DynamicDialogConfig,
public ref: DynamicDialogRef
){}
imageUrl: string; //wird im Template verwendet
fileUpload: FileUpload;
value: number = stateOptions[0].value;
cropperConfig = { aspectRatio: this.value };
ratioVariable: boolean;
stateOptions = stateOptions;
constructor(private loadingService: LoadingService, private imageUploadService: ImageService, public config: DynamicDialogConfig, public ref: DynamicDialogRef) {}
ngOnInit(): void {
if (this.config.data) {
this.imageUrl = this.config.data.imageUrl;
this.fileUpload = this.config.data.fileUpload;
this.cropperConfig = this.config.data.config ? this.config.data.config: this.cropperConfig;
this.ratioVariable = this.config.data.ratioVariable;
this.imageUrl = this.config.data.imageUrl;
this.fileUpload = this.config.data.fileUpload;
this.cropperConfig = this.config.data.config ? this.config.data.config : this.cropperConfig;
this.ratioVariable = this.config.data.ratioVariable;
}
}
sendImage(){
// setTimeout(()=>{
// this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => {
// this.ref.close(blob);
// this.fileUpload.clear()
// }, 'image/jpg');
// },0)
this.fileUpload.clear()
sendImage() {
this.fileUpload.clear();
this.ref.close(this.angularCropper.cropper);
}
cancelUpload(){
cancelUpload() {
this.fileUpload.clear();
this.ref.close();
}
changeAspectRation(ratio:number){
this.cropperConfig={aspectRatio: ratio}
changeAspectRation(ratio: number) {
this.cropperConfig = { aspectRatio: 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.
const authenticated = this.keycloak.isLoggedIn();
//this.keycloak.isTokenExpired()
if (!this.authenticated && !authenticated) {
await this.keycloak.login({
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">
<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">
<div class="text-500 w-full md:w-2 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-500 w-full md:w-3 font-medium flex">Description</div>
<div class="text-900 w-full md:w-9 line-height-3 flex flex-column" [innerHTML]="description"></div>
</li>
<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-900 w-full md:w-10">
<div class="text-500 w-full md:w-3 font-medium">Category</div>
<div class="text-900 w-full md:w-9">
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
</div>
</li>
<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-900 w-full md:w-10">{{ selectOptions.getState(listing.state) }}</div>
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
<div class="text-900 w-full md:w-9">{{ listing.city }}, {{ selectOptions.getState(listing.state) }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.price | currency }}</div>
<div class="text-500 w-full md:w-3 font-medium">Asking Price</div>
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.realEstateIncluded ? 'Yes' : 'No' }}</div>
<div class="text-500 w-full md:w-3 font-medium">Sales revenue</div>
<div class="text-900 w-full md:w-9">{{ listing.salesRevenue | currency }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.salesRevenue | currency }}</div>
<div class="text-500 w-full md:w-3 font-medium">Cash flow</div>
<div class="text-900 w-full md:w-9">{{ listing.cashFlow | currency }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.cashFlow | currency }}</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-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 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-900 w-full md:w-10">{{ listing.employees }}</div>
<div class="text-500 w-full md:w-3 font-medium">Employees</div>
<div class="text-900 w-full md:w-9">{{ listing.employees }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.supportAndTraining }}</div>
<div class="text-500 w-full md:w-3 font-medium">Established since</div>
<div class="text-900 w-full md:w-9">{{ listing.established }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.reasonForSale }}</div>
<div class="text-500 w-full md:w-3 font-medium">Support & Training</div>
<div class="text-900 w-full md:w-9">{{ listing.supportAndTraining }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.brokerLicencing }}</div>
<div class="text-500 w-full md:w-3 font-medium">Reason for Sale</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>
</ul>
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
@@ -79,7 +91,8 @@
</div>
<div class="field mb-4 col-12 md:col-6">
<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 class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
@@ -92,9 +105,9 @@
</div>
@if(listingUser){
<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){
<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>
}

View File

@@ -5,6 +5,7 @@ import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
import { InputMaskModule } from 'primeng/inputmask';
import { lastValueFrom } from 'rxjs';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.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 { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../../../utils/utils';
@Component({
selector: 'app-details-business-listing',
standalone: true,
imports: [SharedModule, GalleriaModule],
imports: [SharedModule, GalleriaModule, InputMaskModule],
providers: [MessageService],
templateUrl: './details-business-listing.component.html',
styleUrl: './details-business-listing.component.scss',
@@ -49,7 +49,8 @@ export class DetailsBusinessListingComponent {
criteria: ListingCriteria;
mailinfo: MailInfo;
environment = environment;
user: KeycloakUser;
keycloakUser: KeycloakUser;
user: User;
listingUser: User;
description: SafeHtml;
private history: string[] = [];
@@ -73,11 +74,16 @@ export class DetailsBusinessListingComponent {
}
});
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
this.user;
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
}
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.listingUser = await this.userService.getById(this.listing.userId);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);

View File

@@ -14,32 +14,32 @@
<div class="col-12 md:col-6">
<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">
<div class="text-500 w-full md:w-2 font-medium flex">Description</div>
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div>
<div class="text-500 w-full md:w-3 font-medium flex">Description</div>
<div class="text-900 w-full md:w-9 line-height-3" [innerHTML]="description"></div>
</li>
<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-900 w-full md:w-10">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
<div class="text-500 w-full md:w-3 font-medium">Property Category</div>
<div class="text-900 w-full md:w-9">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
</li>
<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-900 w-full md:w-10">{{ selectOptions.getState(listing.state) }}</div>
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
<div class="text-900 w-full md:w-9">{{ selectOptions.getState(listing.state) }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.city }}</div>
<div class="text-500 w-full md:w-3 font-medium">City</div>
<div class="text-900 w-full md:w-9">{{ listing.city }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.zipCode }}</div>
<div class="text-500 w-full md:w-3 font-medium">Zip Code</div>
<div class="text-900 w-full md:w-9">{{ listing.zipCode }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.county }}</div>
<div class="text-500 w-full md:w-3 font-medium">County</div>
<div class="text-900 w-full md:w-9">{{ listing.county }}</div>
</li>
<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-900 w-full md:w-10">{{ listing.price | currency }}</div>
<div class="text-500 w-full md:w-3 font-medium">Asking Price:</div>
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
</li>
</ul>
@@ -49,9 +49,9 @@
</div>
<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>
<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>
</p-galleria>
@if (mailinfo){
@@ -69,7 +69,8 @@
</div>
<div class="field mb-4 col-12 md:col-6">
<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 class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
@@ -82,9 +83,9 @@
</div>
@if(listingUser){
<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){
<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>
}

View File

@@ -5,11 +5,13 @@ import { KeycloakService } from 'keycloak-angular';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
import { InputMaskModule } from 'primeng/inputmask';
import { lastValueFrom } from 'rxjs';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';
@@ -20,13 +22,12 @@ import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../.
@Component({
selector: 'app-details-commercial-property-listing',
standalone: true,
imports: [SharedModule, GalleriaModule],
imports: [SharedModule, GalleriaModule, InputMaskModule],
providers: [MessageService],
templateUrl: './details-commercial-property-listing.component.html',
styleUrl: './details-commercial-property-listing.component.scss',
})
export class DetailsCommercialPropertyListingComponent {
// listings: Array<BusinessListing>;
responsiveOptions = [
{
breakpoint: '1199px',
@@ -48,9 +49,9 @@ export class DetailsCommercialPropertyListingComponent {
listing: CommercialPropertyListing;
criteria: ListingCriteria;
mailinfo: MailInfo;
propertyImages: string[] = [];
environment = environment;
user: KeycloakUser;
keycloakUser: KeycloakUser;
user: User;
listingUser: User;
description: SafeHtml;
ts = new Date().getTime();
@@ -67,6 +68,7 @@ export class DetailsCommercialPropertyListingComponent {
private sanitizer: DomSanitizer,
public historyService: HistoryService,
public keycloakService: KeycloakService,
private imageService: ImageService,
) {
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
@@ -75,9 +77,12 @@ export class DetailsCommercialPropertyListingComponent {
async ngOnInit() {
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.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
this.listingUser = await this.userService.getById(this.listing.userId);
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 md:flex-row">
@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 {
<img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
}
@@ -20,7 +20,7 @@
</div>
<div class="mr-5 mt-3">
<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 class="mr-5 mt-3">
<span class="font-medium text-500">Sold</span>
@@ -30,7 +30,7 @@
<!-- <span class="font-medium text-500">Logo</span> -->
<div>
@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"
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> -->
@@ -57,7 +57,7 @@
</li>
<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-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 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>
@@ -121,7 +121,7 @@
<div class="p-3 border-1 surface-border border-round surface-card">
<div class="text-900 mb-2 flex align-items-center">
@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 {
<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 { Observable } from 'rxjs';
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 { HistoryService } from '../../../services/history.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 { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { map2User } from '../../../utils/utils';
import { formatPhoneNumber, map2User } from '../../../utils/utils';
@Component({
selector: 'app-details-user',
@@ -37,6 +37,8 @@ export class DetailsUserComponent {
offeredServices: SafeHtml;
ts = new Date().getTime();
env = environment;
emailToDirName = emailToDirName;
formatPhoneNumber = formatPhoneNumber;
constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
@@ -52,8 +54,7 @@ export class DetailsUserComponent {
async ngOnInit() {
this.user = await this.userService.getById(this.id);
this.user.email;
const results = await Promise.all([await this.listingsService.getListingByUserId(this.id, 'business'), await this.listingsService.getListingByUserId(this.id, 'commercialProperty')]);
const results = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
this.businessListings = results[0];
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">
<span>
@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 {
<img src="assets/images/person_placeholder.jpg" class="w-5rem" />
}
@@ -45,7 +45,7 @@
</div>
<div class="px-4 py-3 text-right flex justify-content-between align-items-center">
@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 {
<img src="assets/images/placeholder.png" class="rounded-image" />
}

View File

@@ -1,23 +1,23 @@
#sky-line {
background-image: url(../../../../assets/images/bw-sky.jpg);
height: 204px;
background-position: bottom;
background-size: cover;
margin-bottom: -1px;
background-image: url(../../../../assets/images/bw-sky.jpg);
height: 204px;
background-position: bottom;
background-size: cover;
margin-bottom: -1px;
}
.search{
background-color: #343F69;
.search {
background-color: #343f69;
}
::ng-deep p-paginator div {
background-color: var(--surface-200) !important;
// background-color: var(--surface-400) !important;
background-color: var(--surface-200) !important;
// background-color: var(--surface-400) !important;
}
.rounded-image {
border-radius: 6px;
// width: 100px;
max-width: 100px;
height: 45px;
border: 1px solid rgba(0,0,0,0.2);
padding: 1px 1px;
object-fit: contain;
border-radius: 6px;
// width: 100px;
max-width: 100px;
max-height: 45px;
border: 1px solid rgba(0, 0, 0, 0.2);
padding: 1px 1px;
object-fit: contain;
}

View File

@@ -12,7 +12,7 @@ import { PaginatorModule } from 'primeng/paginator';
import { StyleClassModule } from 'primeng/styleclass';
import { ToggleButtonModule } from 'primeng/togglebutton';
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 { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
@@ -60,7 +60,7 @@ export class BrokerListingsComponent {
ts = new Date().getTime();
env = environment;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
emailToDirName = emailToDirName;
constructor(
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,

View File

@@ -51,7 +51,7 @@
@for (listing of listings; track listing.id) {
<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="p-4 flex flex-column relative">
<div class="p-4 flex flex-column relative h-full">
<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">
<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>
<div class="icon-pos">
<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>
</div>
</div>

View File

@@ -13,7 +13,7 @@ import { StyleClassModule } from 'primeng/styleclass';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TooltipModule } from 'primeng/tooltip';
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 { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
@@ -58,7 +58,7 @@ export class BusinessListingsComponent {
rows: number = 12;
env = environment;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
emailToDirName = emailToDirName;
constructor(
public selectOptions: SelectOptionsService,
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">
<div class="relative">
@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 {
<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-auto p-fluid">
@if (user){
<div class="mb-4">
<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" />
<p class="font-italic text-sm line-height-1">You can only modify your email by contacting us at support&#64;bizmatch.net</p>
<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>
<input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email" />
<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 class="grid">
<div class="mb-4 col-12 md:col-6">
@@ -23,6 +29,7 @@
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname" />
</div>
</div>
@if (isProfessional){
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<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" />
</div>
</div>
} @if (isProfessional){
<div class="grid">
<div class="mb-4 col-12 md:col-4">
<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 class="mb-4 col-12 md:col-4">
<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>
</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">
<label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label>
<p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }" [modules]="editorModules">
@@ -59,7 +75,6 @@
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
<div class="mb-4">
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label>
@for (areasServed of user.areasServed; track areasServed){
@@ -118,11 +133,12 @@
<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> -->
</div>
}
} }
<div>
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
</div>
</div>
@if (isProfessional){
<div>
<div class="flex flex-column align-items-center flex-or mb-8">
<span class="font-medium text-900 mb-2">Company Logo</span>
@@ -172,6 +188,7 @@
></p-fileUpload>
</div>
</div>
}
</div>
<div class="text-900 font-semibold text-lg mt-3">Membership Level</div>
<p-divider></p-divider>

View File

@@ -25,8 +25,8 @@
/* Stil für das FontAwesome Icon */
.image-wrap fa-icon {
position: absolute;
top: 5px; /* Positioniert das Icon am oberen Rand des Bildes */
right: 5px; /* Positioniert das Icon am rechten Rand des Bildes */
top: -5px; /* Positioniert das Icon am oberen Rand des Bildes */
right: -18px; /* Positioniert das Icon am rechten Rand des Bildes */
color: #fff; /* Weiße Farbe für das Icon */
background-color: rgba(0, 0, 0, 0.5); /* Halbtransparenter Hintergrund für bessere Sichtbarkeit */
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 { EditorModule } from 'primeng/editor';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { InputMaskModule } from 'primeng/inputmask';
import { SelectButtonModule } from 'primeng/selectbutton';
import { lastValueFrom } from 'rxjs';
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 { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
import { GeoService } from '../../../services/geo.service';
@@ -23,12 +24,12 @@ import { SelectOptionsService } from '../../../services/select-options.service';
import { SubscriptionsService } from '../../../services/subscriptions.service';
import { UserService } from '../../../services/user.service';
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';
@Component({
selector: 'app-account',
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],
templateUrl: './account.component.html',
styleUrl: './account.component.scss',
@@ -74,29 +75,29 @@ export class AccountComponent {
try {
this.user = await this.userService.getByMail(email);
} 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.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.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${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/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
}
printInvoice(invoice: Invoice) {}
async updateProfile(user: 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) {
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) {
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) {
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
@@ -120,50 +121,53 @@ export class AccountComponent {
removeArea() {
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') {
const imageUrl = URL.createObjectURL(event.files[0]);
this.type = type;
const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value };
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: {
imageUrl: imageUrl,
fileUpload: type === 'company' ? this.companyUpload : this.profileUpload,
config: config,
ratioVariable: type === 'company' ? true : false,
},
header: 'Edit Image',
width: '50vw',
modal: true,
closeOnEscape: true,
keepInViewport: true,
closable: false,
breakpoints: {
'960px': '75vw',
'640px': '90vw',
},
});
this.dialogRef.onClose.subscribe(cropper => {
if (cropper) {
this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async blob => {
this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', this.user.id).subscribe(
async event => {
if (event.type === HttpEventType.Response) {
this.loadingService.stopLoading('uploadImage');
if (this.type === 'company') {
this.user.hasCompanyLogo = true; //
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}.avif?_ts=${new Date().getTime()}`;
} else {
this.user.hasProfile = true;
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}.avif?_ts=${new Date().getTime()}`;
getImageDimensions(imageUrl).then(dimensions => {
const dialogWidth = getDialogWidth(dimensions);
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: {
imageUrl: imageUrl,
fileUpload: type === 'company' ? this.companyUpload : this.profileUpload,
config: config,
ratioVariable: type === 'company' ? true : false,
},
header: 'Edit Image',
width: dialogWidth,
modal: true,
closeOnEscape: true,
keepInViewport: true,
closable: false,
});
this.dialogRef.onClose.subscribe(cropper => {
if (cropper) {
this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async blob => {
this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', emailToDirName(this.user.email)).subscribe(
async event => {
if (event.type === HttpEventType.Response) {
this.loadingService.stopLoading('uploadImage');
if (this.type === 'company') {
this.user.hasCompanyLogo = true; //
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
} else {
this.user.hasProfile = true;
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);
}
},
error => console.error('Fehler beim Upload:', error),
);
});
}
},
error => console.error('Fehler beim Upload:', error),
);
});
}
});
});
}
deleteConfirm(type: 'profile' | 'logo') {
@@ -180,10 +184,10 @@ export class AccountComponent {
accept: async () => {
if (type === 'profile') {
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 {
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.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 { FileUpload, FileUploadModule } from 'primeng/fileupload';
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 { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
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;
user: User;
maxFileSize = 3000000;
uploadUrl: string;
environment = environment;
propertyImages: string[];
responsiveOptions = [
{
breakpoint: '1199px',
@@ -123,14 +121,14 @@ export class EditBusinessListingComponent {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
} else {
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) {
this.listing.title = this.data?.title;
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() {

View File

@@ -82,58 +82,66 @@
</div>
</div>
</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Property Pictures</span>
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
<p-fileUpload
mode="basic"
chooseLabel="Upload"
[customUpload]="true"
name="file"
accept="image/*"
[maxFileSize]="maxFileSize"
(onSelect)="select($event)"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
[disabled]="!listing.id"
>
</p-fileUpload>
<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>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Property Pictures</span>
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
<p-fileUpload
mode="basic"
chooseLabel="Upload"
[customUpload]="true"
name="file"
accept="image/*"
[maxFileSize]="maxFileSize"
(onSelect)="select($event)"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
[disabled]="!listing.id"
>
</p-fileUpload>
</div>
</div>
</div>
</div>
@if (propertyImages?.length>0){
<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) {
<span cdkDropList mixedCdkDropList>
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="shadow-2" cdkDrag />
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
</div>
</span>
}
</div>
}
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
@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">
@for (image of listing.imageOrder; track listing.imageOrder) {
<span cdkDropList mixedCdkDropList>
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
<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>
</div>
</span>
}
</div>
}
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
}
</div>
</div>
</div>
</div>
</div>
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>

View File

@@ -3,7 +3,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.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 { HttpEventType } from '@angular/common/http';
@@ -18,9 +18,8 @@ import { DialogModule } from 'primeng/dialog';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { EditorModule } from 'primeng/editor';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { v4 as uuidv4 } from 'uuid';
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 { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.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;
user: User;
maxFileSize = 3000000;
uploadUrl: string;
environment = environment;
propertyImages: string[];
responsiveOptions = [
{
breakpoint: '1199px',
@@ -131,15 +128,14 @@ export class EditCommercialPropertyListingComponent {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
} else {
this.listing = createDefaultCommercialPropertyListing();
this.listing.userId = await this.userService.getId(keycloakUser.email);
this.listing.imagePath = uuidv4();
const listingUser = await this.userService.getByMail(keycloakUser.email);
this.listing.userId = listingUser.id;
this.listing.imagePath = `${emailToDirName(keycloakUser.email)}`;
if (this.data) {
this.listing.title = this.data?.title;
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() {
@@ -155,40 +151,44 @@ export class EditCommercialPropertyListingComponent {
select(event: any) {
const imageUrl = URL.createObjectURL(event.files[0]);
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: {
imageUrl: imageUrl,
fileUpload: this.fileUpload,
ratioVariable: false,
},
header: 'Edit Image',
width: '50vw',
modal: true,
closeOnEscape: true,
keepInViewport: true,
closable: false,
breakpoints: {
'960px': '75vw',
'640px': '90vw',
},
});
this.dialogRef.onClose.subscribe(cropper => {
if (cropper) {
this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async blob => {
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.imagePath).subscribe(
async event => {
if (event.type === HttpEventType.Response) {
console.log('Upload abgeschlossen', event.body);
this.loadingService.stopLoading('uploadImage');
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
}
},
error => console.error('Fehler beim Upload:', error),
);
}, 'image/jpg');
cropper.destroy();
}
getImageDimensions(imageUrl).then(dimensions => {
const dialogWidth = getDialogWidth(dimensions);
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: {
imageUrl: imageUrl,
fileUpload: this.fileUpload,
ratioVariable: false,
},
header: 'Edit Image',
width: dialogWidth,
modal: true,
closeOnEscape: true,
keepInViewport: true,
closable: false,
breakpoints: {
'960px': '75vw',
'640px': '90vw',
},
});
this.dialogRef.onClose.subscribe(cropper => {
if (cropper) {
this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async blob => {
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
async event => {
if (event.type === HttpEventType.Response) {
this.ts = new Date().getTime();
console.log('Upload abgeschlossen', event.body);
this.loadingService.stopLoading('uploadImage');
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
}
},
error => console.error('Fehler beim Upload:', error),
);
}, 'image/jpg');
cropper.destroy();
}
});
});
}
@@ -205,9 +205,8 @@ export class EditCommercialPropertyListingComponent {
accept: async () => {
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.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
},
reject: () => {
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
@@ -217,8 +216,7 @@ export class EditCommercialPropertyListingComponent {
}
onDrop(event: { previousIndex: number; currentIndex: number }) {
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages);
moveItemInArray(this.listing.imageOrder, event.previousIndex, event.currentIndex);
}
changeListingCategory(value: 'business' | 'commercialProperty') {
routeListingWithState(this.router, value, this.listing);

View File

@@ -1,35 +1,34 @@
<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">
<menu-account></menu-account>
<div class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">Contact Us</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="name" class="block font-medium text-900 mb-2">Your name</label>
<input id="name" type="text" pInputText>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="email" class="block font-medium text-900 mb-2">Your Email</label>
<input id="email" type="text" pInputText>
</div>
</div>
<div class="mb-4">
<label for="phone" class="block font-medium text-900 mb-2">Your Phone</label>
<input id="phone" type="text" pInputText>
</div>
<div class="mb-4">
<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>
</div>
<div>
<button pButton pRipple label="Submit" class="w-auto"></button>
</div>
</div>
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<div class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">Contact Us</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid formgrid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="name" class="block font-medium text-900 mb-2">Your name</label>
<input [ngClass]="{ 'ng-invalid': containsError('name'), 'ng-dirty': containsError('name') }" name="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
</div>
<div class="mb-4 col-12 md:col-6">
<label for="email" class="block font-medium text-900 mb-2">Your Email</label>
<input name="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email" />
</div>
</div>
<div class="mb-4">
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone</label>
<p-inputMask id="phoneNumber" name="phoneNumber" mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
</div>
<div class="mb-4">
<label for="help" class="block font-medium text-900 mb-2">How can we help you ?</label>
<textarea name="help" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="mailinfo.sender.comments"></textarea>
</div>
<div>
<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 { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { SelectOptionsService } from '../../../services/select-options.service';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { TagModule } from 'primeng/tag';
import data from '../../../../assets/data/user.json';
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';
import { KeycloakService } from 'keycloak-angular';
import { MessageService } from 'primeng/api';
import { InputMaskModule } from 'primeng/inputmask';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { MailService } from '../../../services/mail.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { map2User } from '../../../utils/utils';
@Component({
selector: 'app-email-us',
standalone: true,
imports: [CommonModule, StyleClassModule, MenuAccountComponent, DividerModule,ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule],
imports: [SharedModule, InputMaskModule],
providers: [MessageService],
templateUrl: './email-us.component.html',
styleUrl: './email-us.component.scss'
styleUrl: './email-us.component.scss',
})
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 email = keycloakUser.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]];
}
@@ -46,7 +46,7 @@ export class MyListingComponent {
} else {
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]];
}

View File

@@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
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';
@Injectable({
@@ -12,33 +12,29 @@ export class ImageService {
constructor(private http: HttpClient) {}
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string) {
const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string, serialId?: number) {
let uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
if (type === 'uploadPropertyPicture') {
uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}/${serialId}`;
}
const formData = new FormData();
formData.append('file', imageBlob, 'image.png');
return this.http.post(uploadUrl, formData, {
// headers: this.headers,
//reportProgress: true,
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[]) {
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/profileImages/${userids.join(',')}`));
}
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}`));
async deleteProfileImagesById(email: string) {
const adjustedEmail = emailToDirName(email);
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/profile/${adjustedEmail}`));
}
}

View File

@@ -20,8 +20,8 @@ export class ListingsService {
const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`);
return result;
}
getListingByUserId(userid: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${userid}`));
getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`));
}
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
if (listing.id) {
@@ -40,11 +40,4 @@ export class ListingsService {
async deleteCommercialPropertyListing(id: string, imagePath: string) {
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 { InitEditableRow } from 'primeng/table';
import { Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
import { KeyValue, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
@@ -13,15 +12,14 @@ export class SelectOptionsService {
constructor(private http: HttpClient) {}
async init() {
const allSelectOptions = await lastValueFrom(
this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`)
);
const allSelectOptions = await lastValueFrom(this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`));
this.typesOfBusiness = allSelectOptions.typesOfBusiness;
this.prices = allSelectOptions.prices;
this.listingCategories = allSelectOptions.listingCategories;
this.categories = allSelectOptions.categories;
this.customerTypes = allSelectOptions.customerTypes;
this.states = allSelectOptions.locations;
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty
this.gender = allSelectOptions.gender;
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty;
}
public typesOfBusiness: Array<KeyValueStyle>;
@@ -31,49 +29,41 @@ export class SelectOptionsService {
public listingCategories: Array<KeyValue>;
public categories: Array<KeyValueStyle>;
public customerTypes: Array<KeyValue>;
public gender: Array<KeyValue>;
public states: Array<any>;
getState(value:string):string{
return this.states.find(l=>l.value===value)?.name
getState(value: string): string {
return this.states.find(l => l.value === value)?.name;
}
getBusiness(value:number):string{
return this.typesOfBusiness.find(t=>t.value===String(value))?.name
getBusiness(value: number): string {
return this.typesOfBusiness.find(t => t.value === String(value))?.name;
}
getCommercialProperty(value:number):string{
return this.typesOfCommercialProperty.find(t=>t.value===String(value))?.name
getCommercialProperty(value: number): string {
return this.typesOfCommercialProperty.find(t => t.value === String(value))?.name;
}
getListingsCategory(value:string):string{
return this.listingCategories.find(l=>l.value===value)?.name
getListingsCategory(value: string): string {
return this.listingCategories.find(l => l.value === value)?.name;
}
getCategory(value:string):string{
return this.categories.find(c=>c.value===value)?.name
getCustomerType(value: string): string {
return this.customerTypes.find(c => c.value === value)?.name;
}
getIcon(value:string):string{
return this.categories.find(c=>c.value===value)?.icon
getGender(value: string): string {
return this.gender.find(c => c.value === value)?.name;
}
getTextColor(value:string):string{
return this.categories.find(c=>c.value===value)?.textColorClass
getIconType(value: string): string {
return this.typesOfBusiness.find(c => c.value === value)?.icon;
}
getBgColor(value:string):string{
return this.categories.find(c=>c.value===value)?.bgColorClass
getTextColorType(value: string): string {
return this.typesOfBusiness.find(c => c.value === value)?.textColorClass;
}
getIconAndTextColor(value:string):string{
const category = this.categories.find(c=>c.value===value)
return `${category?.icon} ${category?.textColorClass}`
getBgColorType(value: number): string {
return this.typesOfBusiness.find(c => c.value === String(value))?.bgColorClass;
}
getIconType(value:string):string{
return this.typesOfBusiness.find(c=>c.value===value)?.icon
}
getTextColorType(value:string):string{
return this.typesOfBusiness.find(c=>c.value===value)?.textColorClass
}
getBgColorType(value:number):string{
return this.typesOfBusiness.find(c=>c.value===String(value))?.bgColorClass
}
getIconAndTextColorType(value:number):string{
const category = this.typesOfBusiness.find(c=>c.value===String(value))
return `${category?.icon} ${category?.textColorClass}`
getIconAndTextColorType(value: number): string {
const category = this.typesOfBusiness.find(c => c.value === String(value));
return `${category?.icon} ${category?.textColorClass}`;
}
}

View File

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

View File

@@ -87,7 +87,14 @@ export function createLogger(name: string, level: number = INFO, options: any =
...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) {
sessionStorage.setItem('criteria', JSON.stringify(this));
};
@@ -130,3 +137,25 @@ export function map2User(jwt: string): KeycloakUser {
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;
}