Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d5b7e3f39 | |||
| d488f90f48 | |||
| 044f8efa0f | |||
| 24a3d210f0 | |||
| 2465b8966b | |||
| e87222d3c1 | |||
| 902ab9caed | |||
| 44acbcd4d0 | |||
| b4cf17b8ea | |||
| 226d2ebc1e | |||
| 0473f74241 | |||
| c9d94e973a | |||
| f9d9c6ad9e | |||
| 5dc893da38 | |||
| c471629c6d | |||
| 13fb3cd4b8 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -48,6 +48,9 @@ public
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.prod
|
||||||
|
.env.dev
|
||||||
|
.env.test
|
||||||
|
|
||||||
# temp directory
|
# temp directory
|
||||||
.temp
|
.temp
|
||||||
|
|||||||
1
bizmatch-server/.gitignore
vendored
1
bizmatch-server/.gitignore
vendored
@@ -56,3 +56,4 @@ pids
|
|||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
pictures
|
pictures
|
||||||
|
pictures_base
|
||||||
52
bizmatch-server/.vscode/launch.json
vendored
52
bizmatch-server/.vscode/launch.json
vendored
@@ -6,17 +6,15 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug Nest Framework",
|
"name": "Debug Nest Framework",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||||
"run",
|
|
||||||
"start:debug",
|
|
||||||
"--",
|
|
||||||
"--inspect-brk"
|
|
||||||
],
|
|
||||||
"autoAttachChildProcesses": true,
|
"autoAttachChildProcesses": true,
|
||||||
"restart": true,
|
"restart": true,
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"stopOnEntry": false,
|
"stopOnEntry": false,
|
||||||
"console": "integratedTerminal",
|
"console": "integratedTerminal",
|
||||||
|
"env": {
|
||||||
|
"HOST_NAME": "localhost"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
@@ -24,9 +22,7 @@
|
|||||||
"name": "Debug Current TS File",
|
"name": "Debug Current TS File",
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
|
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
|
||||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||||
"outFiles": [
|
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||||
"${workspaceFolder}/out/**/*.js"
|
|
||||||
],
|
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"smartStep": true,
|
"smartStep": true,
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
"internalConsoleOptions": "openOnSessionStart"
|
||||||
@@ -35,31 +31,21 @@
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "generateDefs",
|
"name": "generateDefs",
|
||||||
"skipFiles": [
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"<node_internals>/**"
|
|
||||||
],
|
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
|
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
|
||||||
"outFiles": [
|
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||||
"${workspaceFolder}/dist/src/drizzle/**/*.js"
|
|
||||||
],
|
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"smartStep": true,
|
"smartStep": true
|
||||||
|
},
|
||||||
},
|
{
|
||||||
{
|
"type": "node",
|
||||||
"type": "node",
|
"request": "launch",
|
||||||
"request": "launch",
|
"name": "generateTypes",
|
||||||
"name": "generateTypes",
|
"skipFiles": ["<node_internals>/**"],
|
||||||
"skipFiles": [
|
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
|
||||||
"<node_internals>/**"
|
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||||
],
|
"sourceMaps": true,
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
|
"smartStep": true
|
||||||
"outFiles": [
|
}
|
||||||
"${workspaceFolder}/dist/src/drizzle/**/*.js"
|
|
||||||
],
|
|
||||||
"sourceMaps": true,
|
|
||||||
"smartStep": true,
|
|
||||||
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -9,10 +9,10 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "HOST_NAME=localhost nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "HOST_NAME=www.bizmatch.net node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@@ -37,7 +37,9 @@
|
|||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-orm": "^0.30.8",
|
"drizzle-orm": "^0.30.8",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
|
"jwks-rsa": "^3.1.0",
|
||||||
"ky": "^1.2.0",
|
"ky": "^1.2.0",
|
||||||
"nest-winston": "^1.9.4",
|
"nest-winston": "^1.9.4",
|
||||||
"nodemailer": "^6.9.10",
|
"nodemailer": "^6.9.10",
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
|
||||||
import { AppService } from './app.service.js';
|
import { AppService } from './app.service.js';
|
||||||
|
import { AuthService } from './auth/auth.service.js';
|
||||||
|
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(
|
||||||
|
private readonly appService: AppService,
|
||||||
|
private authService: AuthService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@Get()
|
@Get()
|
||||||
getHello(): string {
|
getHello(@Request() req): string {
|
||||||
return this.appService.getHello();
|
return req.user;
|
||||||
|
//return 'dfgdf';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import fs from 'fs-extra';
|
||||||
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
@@ -14,12 +17,37 @@ import { ListingsModule } from './listings/listings.module.js';
|
|||||||
import { MailModule } from './mail/mail.module.js';
|
import { MailModule } from './mail/mail.module.js';
|
||||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
||||||
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
||||||
|
|
||||||
import { UserModule } from './user/user.module.js';
|
import { UserModule } from './user/user.module.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
function loadEnvFiles() {
|
||||||
|
// Load the .env file
|
||||||
|
dotenv.config();
|
||||||
|
console.log('Loaded .env file');
|
||||||
|
|
||||||
|
// Determine which additional env file to load
|
||||||
|
let envFilePath = '';
|
||||||
|
const host = process.env.HOST_NAME || '';
|
||||||
|
|
||||||
|
if (host.includes('localhost')) {
|
||||||
|
envFilePath = '.env.local';
|
||||||
|
} else if (host.includes('dev.bizmatch.net')) {
|
||||||
|
envFilePath = '.env.dev';
|
||||||
|
} else if (host.includes('www.bizmatch.net') || host.includes('bizmatch.net')) {
|
||||||
|
envFilePath = '.env.prod';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the additional env file if it exists
|
||||||
|
if (fs.existsSync(envFilePath)) {
|
||||||
|
dotenv.config({ path: envFilePath });
|
||||||
|
console.log(`Loaded ${envFilePath} file`);
|
||||||
|
} else {
|
||||||
|
console.log(`No additional .env file found for HOST_NAME: ${host}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEnvFiles();
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
@@ -46,6 +74,7 @@ const __dirname = path.dirname(__filename);
|
|||||||
ListingsModule,
|
ListingsModule,
|
||||||
SelectOptionsModule,
|
SelectOptionsModule,
|
||||||
ImageModule,
|
ImageModule,
|
||||||
|
PassportModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, FileService],
|
providers: [AppService, FileService],
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { MailerModule } from '@nestjs-modules/mailer';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import path, { join } from 'path';
|
import path from 'path';
|
||||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { AuthService } from './auth.service.js';
|
import { JwtStrategy } from '../jwt.strategy.js';
|
||||||
import { AuthController } from './auth.controller.js';
|
import { AuthController } from './auth.controller.js';
|
||||||
|
import { AuthService } from './auth.service.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [PassportModule],
|
||||||
],
|
providers: [AuthService, JwtStrategy],
|
||||||
providers: [AuthService],
|
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports:[AuthService]
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
|
||||||
|
import fs from 'fs-extra';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import pkg from 'pg';
|
import pkg from 'pg';
|
||||||
import { rimraf } from 'rimraf';
|
import { rimraf } from 'rimraf';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { BusinessListing, CommercialPropertyListing, User, UserData } from 'src/models/db.model.js';
|
import { BusinessListing, CommercialPropertyListing, User, UserData } from 'src/models/db.model.js';
|
||||||
|
import { emailToDirName } from 'src/models/main.model.js';
|
||||||
import * as schema from './schema.js';
|
import * as schema from './schema.js';
|
||||||
const { Pool } = pkg;
|
const { Pool } = pkg;
|
||||||
|
|
||||||
@@ -32,6 +34,11 @@ const targetPathProfile = `./pictures/profile`;
|
|||||||
deleteFilesOfDir(targetPathProfile);
|
deleteFilesOfDir(targetPathProfile);
|
||||||
const targetPathLogo = `./pictures/logo`;
|
const targetPathLogo = `./pictures/logo`;
|
||||||
deleteFilesOfDir(targetPathLogo);
|
deleteFilesOfDir(targetPathLogo);
|
||||||
|
const targetPathProperty = `./pictures/property`;
|
||||||
|
deleteFilesOfDir(targetPathProperty);
|
||||||
|
fs.ensureDirSync(`./pictures/logo`);
|
||||||
|
fs.ensureDirSync(`./pictures/profile`);
|
||||||
|
fs.ensureDirSync(`./pictures/property`);
|
||||||
for (const userData of usersData) {
|
for (const userData of usersData) {
|
||||||
const user: User = { firstname: '', lastname: '', email: '' };
|
const user: User = { firstname: '', lastname: '', email: '' };
|
||||||
user.licensedIn = [];
|
user.licensedIn = [];
|
||||||
@@ -58,20 +65,21 @@ for (const userData of usersData) {
|
|||||||
user.gender = userData.gender;
|
user.gender = userData.gender;
|
||||||
user.created = new Date();
|
user.created = new Date();
|
||||||
user.updated = new Date();
|
user.updated = new Date();
|
||||||
const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender });
|
const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email });
|
||||||
generatedUserData.push(u[0].insertedId);
|
generatedUserData.push(u[0]);
|
||||||
i++;
|
i++;
|
||||||
|
|
||||||
if (u[0].gender === 'male') {
|
if (u[0].gender === 'male') {
|
||||||
male++;
|
male++;
|
||||||
const data = readFileSync(`./pictures/profile_base/Mann_${male}.jpg`);
|
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
|
||||||
await storeProfilePicture(data, u[0].insertedId);
|
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||||
} else {
|
} else {
|
||||||
female++;
|
female++;
|
||||||
const data = readFileSync(`./pictures/profile_base/Frau_${female}.jpg`);
|
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
|
||||||
await storeProfilePicture(data, u[0].insertedId);
|
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||||
}
|
}
|
||||||
const data = readFileSync(`./pictures/logos_base/${i}.jpg`);
|
const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
|
||||||
await storeCompanyLogo(data, u[0].insertedId);
|
await storeCompanyLogo(data, emailToDirName(u[0].email));
|
||||||
}
|
}
|
||||||
//Business Listings
|
//Business Listings
|
||||||
filePath = `./data/businesses.json`;
|
filePath = `./data/businesses.json`;
|
||||||
@@ -82,7 +90,9 @@ for (const business of businessJsonData) {
|
|||||||
delete business.id;
|
delete business.id;
|
||||||
business.created = new Date(business.created);
|
business.created = new Date(business.created);
|
||||||
business.updated = new Date(business.created);
|
business.updated = new Date(business.created);
|
||||||
business.userId = getRandomItem(generatedUserData);
|
const user = getRandomItem(generatedUserData);
|
||||||
|
business.userId = user.insertedId;
|
||||||
|
business.imageName = emailToDirName(user.email);
|
||||||
await db.insert(schema.businesses).values(business);
|
await db.insert(schema.businesses).values(business);
|
||||||
}
|
}
|
||||||
//Corporate Listings
|
//Corporate Listings
|
||||||
@@ -92,14 +102,21 @@ const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // E
|
|||||||
for (const commercial of commercialJsonData) {
|
for (const commercial of commercialJsonData) {
|
||||||
const id = commercial.id;
|
const id = commercial.id;
|
||||||
delete commercial.id;
|
delete commercial.id;
|
||||||
|
const user = getRandomItem(generatedUserData);
|
||||||
commercial.imageOrder = getFilenames(id);
|
commercial.imageOrder = getFilenames(id);
|
||||||
commercial.imagePath = id;
|
commercial.imagePath = emailToDirName(user.email);
|
||||||
const insertionDate = getRandomDateWithinLastYear();
|
const insertionDate = getRandomDateWithinLastYear();
|
||||||
commercial.created = insertionDate;
|
commercial.created = insertionDate;
|
||||||
commercial.updated = insertionDate;
|
commercial.updated = insertionDate;
|
||||||
commercial.userId = getRandomItem(generatedUserData);
|
commercial.userId = user.insertedId;
|
||||||
await db.insert(schema.commercials).values(commercial);
|
commercial.draft = false;
|
||||||
|
const result = await db.insert(schema.commercials).values(commercial).returning();
|
||||||
|
//fs.ensureDirSync(`./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
|
||||||
|
try {
|
||||||
|
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`----- No pictures available for ${id} ------`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//End
|
//End
|
||||||
@@ -115,7 +132,7 @@ function getRandomItem<T>(arr: T[]): T {
|
|||||||
}
|
}
|
||||||
function getFilenames(id: string): string[] {
|
function getFilenames(id: string): string[] {
|
||||||
try {
|
try {
|
||||||
let filePath = `./pictures/property/${id}`;
|
let filePath = `./pictures_base/property/${id}`;
|
||||||
return readdirSync(filePath);
|
return readdirSync(filePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -141,14 +158,14 @@ async function storeProfilePicture(buffer: Buffer, userId: string) {
|
|||||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function storeCompanyLogo(buffer: Buffer, userId: string) {
|
async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) {
|
||||||
let quality = 50;
|
let quality = 50;
|
||||||
const output = await sharp(buffer)
|
const output = await sharp(buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/logo/${userId}.avif`); // Ersetze Dateierweiterung
|
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS "businesses" (
|
|||||||
"reasonForSale" varchar(255),
|
"reasonForSale" varchar(255),
|
||||||
"brokerLicencing" varchar(255),
|
"brokerLicencing" varchar(255),
|
||||||
"internals" text,
|
"internals" text,
|
||||||
|
"imagePath" varchar(200),
|
||||||
"created" timestamp,
|
"created" timestamp,
|
||||||
"updated" timestamp,
|
"updated" timestamp,
|
||||||
"visits" integer,
|
"visits" integer,
|
||||||
@@ -36,6 +37,7 @@ CREATE TABLE IF NOT EXISTS "businesses" (
|
|||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE IF NOT EXISTS "commercials" (
|
CREATE TABLE IF NOT EXISTS "commercials" (
|
||||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"serial_id" serial NOT NULL,
|
||||||
"userId" uuid,
|
"userId" uuid,
|
||||||
"type" integer,
|
"type" integer,
|
||||||
"title" varchar(255),
|
"title" varchar(255),
|
||||||
@@ -53,7 +55,7 @@ CREATE TABLE IF NOT EXISTS "commercials" (
|
|||||||
"website" varchar(255),
|
"website" varchar(255),
|
||||||
"phoneNumber" varchar(255),
|
"phoneNumber" varchar(255),
|
||||||
"imageOrder" varchar(200)[],
|
"imageOrder" varchar(200)[],
|
||||||
"imagePath" varchar(50),
|
"imagePath" varchar(200),
|
||||||
"created" timestamp,
|
"created" timestamp,
|
||||||
"updated" timestamp,
|
"updated" timestamp,
|
||||||
"visits" integer,
|
"visits" integer,
|
||||||
@@ -76,7 +78,9 @@ CREATE TABLE IF NOT EXISTS "users" (
|
|||||||
"hasProfile" boolean,
|
"hasProfile" boolean,
|
||||||
"hasCompanyLogo" boolean,
|
"hasCompanyLogo" boolean,
|
||||||
"licensedIn" jsonb,
|
"licensedIn" jsonb,
|
||||||
"gender" "gender"
|
"gender" "gender",
|
||||||
|
"created" timestamp,
|
||||||
|
"updated" timestamp
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE "users" ADD COLUMN "created" timestamp;--> statement-breakpoint
|
|
||||||
ALTER TABLE "users" ADD COLUMN "updated" timestamp;
|
|
||||||
@@ -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";
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "98e2be90-3301-49a8-b323-78d9d8f79cb5",
|
"id": "fc58c59b-ac5c-406e-8fdb-b05de40aed17",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"dialect": "pg",
|
"dialect": "pg",
|
||||||
@@ -147,6 +147,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"imagePath": {
|
||||||
|
"name": "imagePath",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"created": {
|
"created": {
|
||||||
"name": "created",
|
"name": "created",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
@@ -202,6 +208,12 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "gen_random_uuid()"
|
"default": "gen_random_uuid()"
|
||||||
},
|
},
|
||||||
|
"serial_id": {
|
||||||
|
"name": "serial_id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
"userId": {
|
"userId": {
|
||||||
"name": "userId",
|
"name": "userId",
|
||||||
"type": "uuid",
|
"type": "uuid",
|
||||||
@@ -306,7 +318,7 @@
|
|||||||
},
|
},
|
||||||
"imagePath": {
|
"imagePath": {
|
||||||
"name": "imagePath",
|
"name": "imagePath",
|
||||||
"type": "varchar(50)",
|
"type": "varchar(200)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -454,6 +466,18 @@
|
|||||||
"type": "gender",
|
"type": "gender",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"name": "created",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated": {
|
||||||
|
"name": "updated",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "41802273-1335-433f-97cb-77774ddb3362",
|
"id": "0bc02618-4414-4e90-8c44-808737611da7",
|
||||||
"prevId": "98e2be90-3301-49a8-b323-78d9d8f79cb5",
|
"prevId": "fc58c59b-ac5c-406e-8fdb-b05de40aed17",
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"dialect": "pg",
|
"dialect": "pg",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -147,6 +147,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"imagePath": {
|
||||||
|
"name": "imagePath",
|
||||||
|
"type": "varchar(200)",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"created": {
|
"created": {
|
||||||
"name": "created",
|
"name": "created",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
@@ -202,6 +208,12 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": "gen_random_uuid()"
|
"default": "gen_random_uuid()"
|
||||||
},
|
},
|
||||||
|
"serial_id": {
|
||||||
|
"name": "serial_id",
|
||||||
|
"type": "serial",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
"userId": {
|
"userId": {
|
||||||
"name": "userId",
|
"name": "userId",
|
||||||
"type": "uuid",
|
"type": "uuid",
|
||||||
@@ -306,7 +318,7 @@
|
|||||||
},
|
},
|
||||||
"imagePath": {
|
"imagePath": {
|
||||||
"name": "imagePath",
|
"name": "imagePath",
|
||||||
"type": "varchar(50)",
|
"type": "varchar(200)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
@@ -455,6 +467,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"customerType": {
|
||||||
|
"name": "customerType",
|
||||||
|
"type": "customerType",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"created": {
|
"created": {
|
||||||
"name": "created",
|
"name": "created",
|
||||||
"type": "timestamp",
|
"type": "timestamp",
|
||||||
@@ -475,6 +493,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
|
"customerType": {
|
||||||
|
"name": "customerType",
|
||||||
|
"values": {
|
||||||
|
"buyer": "buyer",
|
||||||
|
"broker": "broker",
|
||||||
|
"professional": "professional"
|
||||||
|
}
|
||||||
|
},
|
||||||
"gender": {
|
"gender": {
|
||||||
"name": "gender",
|
"name": "gender",
|
||||||
"values": {
|
"values": {
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1715627517508,
|
"when": 1716495198537,
|
||||||
"tag": "0000_open_hannibal_king",
|
"tag": "0000_burly_bruce_banner",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"version": "5",
|
"version": "5",
|
||||||
"when": 1715631674334,
|
"when": 1717085220861,
|
||||||
"tag": "0001_charming_thundra",
|
"tag": "0001_wet_mephistopheles",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
||||||
import { AreasServed, LicensedIn } from 'src/models/db.model';
|
import { AreasServed, LicensedIn } from 'src/models/db.model';
|
||||||
|
|
||||||
export const PG_CONNECTION = 'PG_CONNECTION';
|
export const PG_CONNECTION = 'PG_CONNECTION';
|
||||||
export const genderEnum = pgEnum('gender', ['male', 'female']);
|
export const genderEnum = pgEnum('gender', ['male', 'female']);
|
||||||
|
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'broker', 'professional']);
|
||||||
|
|
||||||
export const users = pgTable('users', {
|
export const users = pgTable('users', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
firstname: varchar('firstname', { length: 255 }).notNull(),
|
firstname: varchar('firstname', { length: 255 }).notNull(),
|
||||||
@@ -20,6 +22,7 @@ export const users = pgTable('users', {
|
|||||||
hasCompanyLogo: boolean('hasCompanyLogo'),
|
hasCompanyLogo: boolean('hasCompanyLogo'),
|
||||||
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
||||||
gender: genderEnum('gender'),
|
gender: genderEnum('gender'),
|
||||||
|
customerType: customerTypeEnum('customerType'),
|
||||||
created: timestamp('created'),
|
created: timestamp('created'),
|
||||||
updated: timestamp('updated'),
|
updated: timestamp('updated'),
|
||||||
});
|
});
|
||||||
@@ -48,6 +51,7 @@ export const businesses = pgTable('businesses', {
|
|||||||
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
||||||
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
||||||
internals: text('internals'),
|
internals: text('internals'),
|
||||||
|
imageName: varchar('imagePath', { length: 200 }),
|
||||||
created: timestamp('created'),
|
created: timestamp('created'),
|
||||||
updated: timestamp('updated'),
|
updated: timestamp('updated'),
|
||||||
visits: integer('visits'),
|
visits: integer('visits'),
|
||||||
@@ -56,6 +60,7 @@ export const businesses = pgTable('businesses', {
|
|||||||
|
|
||||||
export const commercials = pgTable('commercials', {
|
export const commercials = pgTable('commercials', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
serialId: serial('serial_id'),
|
||||||
userId: uuid('userId').references(() => users.id),
|
userId: uuid('userId').references(() => users.id),
|
||||||
type: integer('type'),
|
type: integer('type'),
|
||||||
title: varchar('title', { length: 255 }),
|
title: varchar('title', { length: 255 }),
|
||||||
@@ -73,7 +78,7 @@ export const commercials = pgTable('commercials', {
|
|||||||
website: varchar('website', { length: 255 }),
|
website: varchar('website', { length: 255 }),
|
||||||
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
||||||
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
||||||
imagePath: varchar('imagePath', { length: 50 }),
|
imagePath: varchar('imagePath', { length: 200 }),
|
||||||
created: timestamp('created'),
|
created: timestamp('created'),
|
||||||
updated: timestamp('updated'),
|
updated: timestamp('updated'),
|
||||||
visits: integer('visits'),
|
visits: integer('visits'),
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export class FileService {
|
|||||||
fs.ensureDirSync(`./pictures/logo`);
|
fs.ensureDirSync(`./pictures/logo`);
|
||||||
fs.ensureDirSync(`./pictures/property`);
|
fs.ensureDirSync(`./pictures/property`);
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
|
// Subscriptions
|
||||||
|
// ############
|
||||||
private loadSubscriptions(): void {
|
private loadSubscriptions(): void {
|
||||||
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
||||||
const rawData = readFileSync(filePath, 'utf8');
|
const rawData = readFileSync(filePath, 'utf8');
|
||||||
@@ -28,36 +31,43 @@ export class FileService {
|
|||||||
getSubscriptions(): Subscription[] {
|
getSubscriptions(): Subscription[] {
|
||||||
return this.subscriptions;
|
return this.subscriptions;
|
||||||
}
|
}
|
||||||
async storeProfilePicture(file: Express.Multer.File, userId: string) {
|
// ############
|
||||||
|
// Profile
|
||||||
|
// ############
|
||||||
|
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
|
||||||
let quality = 50;
|
let quality = 50;
|
||||||
const output = await sharp(file.buffer)
|
const output = await sharp(file.buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
await sharp(output).toFile(`./pictures/profile/${adjustedEmail}.avif`);
|
||||||
}
|
}
|
||||||
hasProfile(userId: string) {
|
hasProfile(adjustedEmail: string) {
|
||||||
return fs.existsSync(`./pictures/profile/${userId}.avif`);
|
return fs.existsSync(`./pictures/profile/${adjustedEmail}.avif`);
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
async storeCompanyLogo(file: Express.Multer.File, userId: string) {
|
// Logo
|
||||||
|
// ############
|
||||||
|
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
|
||||||
let quality = 50;
|
let quality = 50;
|
||||||
const output = await sharp(file.buffer)
|
const output = await sharp(file.buffer)
|
||||||
.resize({ width: 300 })
|
.resize({ width: 300 })
|
||||||
.avif({ quality }) // Verwende AVIF
|
.avif({ quality }) // Verwende AVIF
|
||||||
//.webp({ quality }) // Verwende Webp
|
//.webp({ quality }) // Verwende Webp
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
await sharp(output).toFile(`./pictures/logo/${userId}.avif`); // Ersetze Dateierweiterung
|
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||||
}
|
}
|
||||||
hasCompanyLogo(userId: string) {
|
hasCompanyLogo(adjustedEmail: string) {
|
||||||
return fs.existsSync(`./pictures/logo/${userId}.avif`) ? true : false;
|
return fs.existsSync(`./pictures/logo/${adjustedEmail}.avif`) ? true : false;
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
async getPropertyImages(listingId: string): Promise<string[]> {
|
// Property
|
||||||
|
// ############
|
||||||
|
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
const directory = `./pictures/property/${listingId}`;
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
files.forEach(f => {
|
files.forEach(f => {
|
||||||
@@ -68,9 +78,9 @@ export class FileService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async hasPropertyImages(listingId: string): Promise<boolean> {
|
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
|
||||||
const result: ImageProperty[] = [];
|
const result: ImageProperty[] = [];
|
||||||
const directory = `./pictures/property/${listingId}`;
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
if (fs.existsSync(directory)) {
|
if (fs.existsSync(directory)) {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
return files.length > 0;
|
return files.length > 0;
|
||||||
@@ -78,15 +88,18 @@ export class FileService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async storePropertyPicture(file: Express.Multer.File, listingId: string): Promise<string> {
|
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
|
||||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
|
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
|
||||||
const directory = `./pictures/property/${listingId}`;
|
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||||
fs.ensureDirSync(`${directory}`);
|
fs.ensureDirSync(`${directory}`);
|
||||||
const imageName = await this.getNextImageName(directory);
|
const imageName = await this.getNextImageName(directory);
|
||||||
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
||||||
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
|
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
|
||||||
return `${imageName}.avif`;
|
return `${imageName}.avif`;
|
||||||
}
|
}
|
||||||
|
// ############
|
||||||
|
// utils
|
||||||
|
// ############
|
||||||
async getNextImageName(directory) {
|
async getNextImageName(directory) {
|
||||||
try {
|
try {
|
||||||
const files = await fs.readdir(directory);
|
const files = await fs.readdir(directory);
|
||||||
@@ -115,22 +128,6 @@ export class FileService {
|
|||||||
let timeTaken = Date.now() - start;
|
let timeTaken = Date.now() - start;
|
||||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
||||||
}
|
}
|
||||||
getProfileImagesForUsers(userids: string) {
|
|
||||||
const ids = userids.split(',');
|
|
||||||
let result = {};
|
|
||||||
for (const id of ids) {
|
|
||||||
result = { ...result, [id]: fs.existsSync(`./pictures/profile/${id}.avif`) };
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
getCompanyLogosForUsers(userids: string) {
|
|
||||||
const ids = userids.split(',');
|
|
||||||
let result = {};
|
|
||||||
for (const id of ids) {
|
|
||||||
result = { ...result, [id]: fs.existsSync(`./pictures/logo/${id}.avif`) };
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
deleteImage(path: string) {
|
deleteImage(path: string) {
|
||||||
fs.unlinkSync(path);
|
fs.unlinkSync(path);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Controller, Delete, Get, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
@@ -6,9 +6,6 @@ import { FileService } from '../file/file.service.js';
|
|||||||
import { ListingsService } from '../listings/listings.service.js';
|
import { ListingsService } from '../listings/listings.service.js';
|
||||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||||
|
|
||||||
import { commercials } from '../drizzle/schema.js';
|
|
||||||
import { CommercialPropertyListing } from '../models/db.model.js';
|
|
||||||
|
|
||||||
@Controller('image')
|
@Controller('image')
|
||||||
export class ImageController {
|
export class ImageController {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -17,58 +14,42 @@ export class ImageController {
|
|||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
private selectOptions: SelectOptionsService,
|
private selectOptions: SelectOptionsService,
|
||||||
) {}
|
) {}
|
||||||
|
// ############
|
||||||
@Post('uploadPropertyPicture/:imagePath')
|
// Property
|
||||||
|
// ############
|
||||||
|
@Post('uploadPropertyPicture/:imagePath/:serial')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string) {
|
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
|
||||||
const imagename = await this.fileService.storePropertyPicture(file, imagePath);
|
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
|
||||||
await this.listingService.addImage(imagePath, imagename);
|
await this.listingService.addImage(imagePath, serial, imagename);
|
||||||
}
|
}
|
||||||
|
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
||||||
@Post('uploadProfile/:id')
|
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
|
||||||
|
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
|
||||||
|
await this.listingService.deleteImage(imagePath, serial, imagename);
|
||||||
|
}
|
||||||
|
// ############
|
||||||
|
// Profile
|
||||||
|
// ############
|
||||||
|
@Post('uploadProfile/:email')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
|
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||||
await this.fileService.storeProfilePicture(file, id);
|
await this.fileService.storeProfilePicture(file, adjustedEmail);
|
||||||
}
|
}
|
||||||
|
@Delete('profile/:email/')
|
||||||
@Post('uploadCompanyLogo/:id')
|
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||||
|
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
||||||
|
}
|
||||||
|
// ############
|
||||||
|
// Logo
|
||||||
|
// ############
|
||||||
|
@Post('uploadCompanyLogo/:email')
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
|
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||||
await this.fileService.storeCompanyLogo(file, id);
|
await this.fileService.storeCompanyLogo(file, adjustedEmail);
|
||||||
}
|
}
|
||||||
|
@Delete('logo/:email/')
|
||||||
@Get(':id')
|
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||||
async getPropertyImagesById(@Param('id') id: string): Promise<any> {
|
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
||||||
const result = await this.listingService.findById(id, commercials);
|
|
||||||
const listing = result as CommercialPropertyListing;
|
|
||||||
if (listing.imageOrder) {
|
|
||||||
return listing.imageOrder;
|
|
||||||
} else {
|
|
||||||
const imageOrder = await this.fileService.getPropertyImages(id);
|
|
||||||
listing.imageOrder = imageOrder;
|
|
||||||
this.listingService.updateListing(listing.id, listing, commercials);
|
|
||||||
return imageOrder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Get('profileImages/:userids')
|
|
||||||
async getProfileImagesForUsers(@Param('userids') userids: string): Promise<any> {
|
|
||||||
return await this.fileService.getProfileImagesForUsers(userids);
|
|
||||||
}
|
|
||||||
@Get('companyLogos/:userids')
|
|
||||||
async getCompanyLogosForUsers(@Param('userids') userids: string): Promise<any> {
|
|
||||||
return await this.fileService.getCompanyLogosForUsers(userids);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Delete('propertyPicture/:imagePath/:imagename')
|
|
||||||
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('imagename') imagename: string): Promise<any> {
|
|
||||||
this.fileService.deleteImage(`pictures/property/${imagePath}/${imagename}`);
|
|
||||||
}
|
|
||||||
@Delete('logo/:userid/')
|
|
||||||
async deleteLogoImagesById(@Param('userid') userid: string): Promise<any> {
|
|
||||||
this.fileService.deleteImage(`pictures/logo/${userid}.avif`);
|
|
||||||
}
|
|
||||||
@Delete('profile/:userid/')
|
|
||||||
async deleteProfileImagesById(@Param('userid') userid: string): Promise<any> {
|
|
||||||
this.fileService.deleteImage(`pictures/profile/${userid}.avif`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
bizmatch-server/src/jwt-auth/jwt-auth.guard.ts
Normal file
18
bizmatch-server/src/jwt-auth/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
// Add your custom authentication logic here
|
||||||
|
// for example, call super.logIn(request) to establish a session.
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
handleRequest(err, user, info) {
|
||||||
|
// You can throw an exception based on either "info" or "err" arguments
|
||||||
|
if (err || !user) {
|
||||||
|
throw err || new UnauthorizedException(info);
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts
Normal file
13
bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
handleRequest(err, user, info) {
|
||||||
|
// Wenn der Benutzer nicht authentifiziert ist, aber kein Fehler vorliegt, geben Sie null zurück
|
||||||
|
if (err || !user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
bizmatch-server/src/jwt.strategy.ts
Normal file
45
bizmatch-server/src/jwt.strategy.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { passportJwtSecret } from 'jwks-rsa';
|
||||||
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { Logger } from 'winston';
|
||||||
|
import { JwtPayload, JwtUser } from './models/main.model';
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
configService: ConfigService,
|
||||||
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
|
) {
|
||||||
|
const realm = configService.get<string>('REALM');
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKeyProvider: passportJwtSecret({
|
||||||
|
cache: true,
|
||||||
|
rateLimit: true,
|
||||||
|
jwksRequestsPerMinute: 5,
|
||||||
|
jwksUri: `https://auth.bizmatch.net/realms/${realm}/protocol/openid-connect/certs`,
|
||||||
|
}),
|
||||||
|
audience: 'account', // Keycloak Client ID
|
||||||
|
authorize: '',
|
||||||
|
issuer: `https://auth.bizmatch.net/realms/${realm}`,
|
||||||
|
algorithms: ['RS256'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: JwtPayload): Promise<JwtUser> {
|
||||||
|
if (!payload) {
|
||||||
|
this.logger.error('Invalid payload');
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
if (!payload.sub || !payload.preferred_username) {
|
||||||
|
this.logger.error('Missing required claims');
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
}
|
||||||
|
const result = { userId: payload.sub, username: payload.preferred_username, roles: payload.realm_access?.roles };
|
||||||
|
this.logger.info(`JWT User: ${JSON.stringify(result)}`); // Debugging: JWT Payload anzeigen
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { businesses } from '../drizzle/schema.js';
|
import { businesses } from '../drizzle/schema.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||||
|
import { JwtUser, ListingCriteria } from '../models/main.model.js';
|
||||||
import { ListingsService } from './listings.service.js';
|
import { ListingsService } from './listings.service.js';
|
||||||
|
|
||||||
@Controller('listings/business')
|
@Controller('listings/business')
|
||||||
@@ -12,18 +13,22 @@ export class BusinessListingsController {
|
|||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findById(@Param('id') id: string): any {
|
findById(@Request() req, @Param('id') id: string): any {
|
||||||
return this.listingsService.findById(id, businesses);
|
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||||
}
|
|
||||||
@Get('user/:userid')
|
|
||||||
findByUserId(@Param('userid') userid: string): any {
|
|
||||||
return this.listingsService.findByUserId(userid, businesses);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
|
@Get('user/:userid')
|
||||||
|
findByUserId(@Request() req, @Param('userid') userid: string): any {
|
||||||
|
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Post('search')
|
@Post('search')
|
||||||
find(@Body() criteria: ListingCriteria): any {
|
find(@Request() req, @Body() criteria: ListingCriteria): any {
|
||||||
return this.listingsService.findListingsByCriteria(criteria, businesses);
|
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@@ -34,7 +39,7 @@ export class BusinessListingsController {
|
|||||||
@Put()
|
@Put()
|
||||||
update(@Body() listing: any) {
|
update(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
return this.listingsService.updateListing(listing.id, listing, businesses);
|
return this.listingsService.updateBusinessListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
deleteById(@Param('id') id: string) {
|
deleteById(@Param('id') id: string) {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
|
import { CommercialPropertyListing } from 'src/models/db.model.js';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { commercials } from '../drizzle/schema.js';
|
import { commercials } from '../drizzle/schema.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||||
|
import { JwtUser, ListingCriteria } from '../models/main.model.js';
|
||||||
import { ListingsService } from './listings.service.js';
|
import { ListingsService } from './listings.service.js';
|
||||||
|
|
||||||
@Controller('listings/commercialProperty')
|
@Controller('listings/commercialProperty')
|
||||||
@@ -14,17 +16,21 @@ export class CommercialPropertyListingsController {
|
|||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findById(@Param('id') id: string): any {
|
findById(@Request() req, @Param('id') id: string): any {
|
||||||
return this.listingsService.findById(id, commercials);
|
return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
@Get('user/:userid')
|
|
||||||
findByUserId(@Param('userid') userid: string): any {
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
return this.listingsService.findByUserId(userid, commercials);
|
@Get('user/:email')
|
||||||
|
findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||||
|
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Post('search')
|
@Post('search')
|
||||||
async find(@Body() criteria: ListingCriteria): Promise<any> {
|
async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> {
|
||||||
return await this.listingsService.findListingsByCriteria(criteria, commercials);
|
return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
@Get('states/all')
|
@Get('states/all')
|
||||||
getStates(): any {
|
getStates(): any {
|
||||||
@@ -38,16 +44,11 @@ export class CommercialPropertyListingsController {
|
|||||||
@Put()
|
@Put()
|
||||||
async update(@Body() listing: any) {
|
async update(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
return await this.listingsService.updateListing(listing.id, listing, commercials);
|
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
@Delete(':id/:imagePath')
|
@Delete(':id/:imagePath')
|
||||||
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||||
this.listingsService.deleteListing(id, commercials);
|
this.listingsService.deleteListing(id, commercials);
|
||||||
this.fileService.deleteDirectoryIfExists(imagePath);
|
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('imageOrder/:id')
|
|
||||||
async changeImageOrder(@Param('id') id: string, @Body() imageOrder: string[]) {
|
|
||||||
this.listingsService.updateImageOrder(id, imageOrder);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AuthModule } from '../auth/auth.module.js';
|
||||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { UserService } from '../user/user.service.js';
|
import { UserService } from '../user/user.service.js';
|
||||||
@@ -9,7 +10,7 @@ import { ListingsService } from './listings.service.js';
|
|||||||
import { UnknownListingsController } from './unknown-listings.controller.js';
|
import { UnknownListingsController } from './unknown-listings.controller.js';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DrizzleModule],
|
imports: [DrizzleModule, AuthModule],
|
||||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||||
providers: [ListingsService, FileService, UserService],
|
providers: [ListingsService, FileService, UserService],
|
||||||
exports: [ListingsService],
|
exports: [ListingsService],
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { and, eq, gte, ilike, lte, sql } from 'drizzle-orm';
|
import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
|
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as schema from '../drizzle/schema.js';
|
import * as schema from '../drizzle/schema.js';
|
||||||
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
|
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
|
import { JwtUser, ListingCriteria, emailToDirName } from '../models/main.model.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ListingsService {
|
export class ListingsService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
|
private fileService: FileService,
|
||||||
) {}
|
) {}
|
||||||
private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials): any[] {
|
private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
if (criteria.type) {
|
if (criteria.type) {
|
||||||
conditions.push(eq(table.type, criteria.type));
|
conditions.push(eq(table.type, criteria.type));
|
||||||
@@ -39,46 +41,96 @@ export class ListingsService {
|
|||||||
// ##############################################################
|
// ##############################################################
|
||||||
// Listings general
|
// Listings general
|
||||||
// ##############################################################
|
// ##############################################################
|
||||||
async findListingsByCriteria(criteria: ListingCriteria, table: typeof businesses | typeof commercials): Promise<{ data: Record<string, any>[]; total: number }> {
|
|
||||||
|
async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
|
||||||
const start = criteria.start ? criteria.start : 0;
|
const start = criteria.start ? criteria.start : 0;
|
||||||
const length = criteria.length ? criteria.length : 12;
|
const length = criteria.length ? criteria.length : 12;
|
||||||
return await this.findListings(table, criteria, start, length);
|
const conditions = this.getConditions(criteria, commercials, user);
|
||||||
}
|
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||||
private async findListings(table: typeof businesses | typeof commercials, criteria: ListingCriteria, start = 0, length = 12): Promise<any> {
|
conditions.push(or(eq(commercials.draft, false), eq(commercials.imagePath, emailToDirName(user?.username))));
|
||||||
const conditions = this.getConditions(criteria, table);
|
}
|
||||||
const [data, total] = await Promise.all([
|
const [data, total] = await Promise.all([
|
||||||
this.conn
|
this.conn
|
||||||
.select()
|
.select()
|
||||||
.from(table)
|
.from(commercials)
|
||||||
.where(and(...conditions))
|
.where(and(...conditions))
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(length),
|
.limit(length),
|
||||||
this.conn
|
this.conn
|
||||||
.select({ count: sql`count(*)` })
|
.select({ count: sql`count(*)` })
|
||||||
.from(table)
|
.from(commercials)
|
||||||
.where(and(...conditions))
|
.where(and(...conditions))
|
||||||
.then(result => Number(result[0].count)),
|
.then(result => Number(result[0].count)),
|
||||||
]);
|
]);
|
||||||
return { total, data };
|
return { total, data };
|
||||||
}
|
}
|
||||||
async findById(id: string, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
async findBusinessListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
|
||||||
const result = await this.conn
|
const start = criteria.start ? criteria.start : 0;
|
||||||
.select()
|
const length = criteria.length ? criteria.length : 12;
|
||||||
.from(table)
|
const conditions = this.getConditions(criteria, businesses, user);
|
||||||
.where(sql`${table.id} = ${id}`);
|
if (!user || (!user?.roles?.includes('ADMIN') ?? false)) {
|
||||||
return result[0] as BusinessListing | CommercialPropertyListing;
|
conditions.push(or(eq(businesses.draft, false), eq(businesses.imageName, emailToDirName(user?.username))));
|
||||||
|
}
|
||||||
|
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
|
const result = await this.conn
|
||||||
.select()
|
.select()
|
||||||
.from(commercials)
|
.from(commercials)
|
||||||
.where(sql`${commercials.imagePath} = ${imagePath}`);
|
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
|
||||||
return result[0] as CommercialPropertyListing;
|
return result[0] as CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
async findByUserId(userId: string, table: typeof businesses | typeof commercials): Promise<BusinessListing[] | CommercialPropertyListing[]> {
|
|
||||||
return (await this.conn.select().from(table).where(eq(table.userId, userId))) as BusinessListing[] | CommercialPropertyListing[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
data.created = new Date();
|
data.created = new Date();
|
||||||
data.updated = new Date();
|
data.updated = new Date();
|
||||||
@@ -86,13 +138,24 @@ export class ListingsService {
|
|||||||
return createdListing as BusinessListing | CommercialPropertyListing;
|
return createdListing as BusinessListing | CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateListing(id: string, data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
data.updated = new Date();
|
data.updated = new Date();
|
||||||
data.created = new Date(data.created);
|
data.created = new Date(data.created);
|
||||||
const [updateListing] = await this.conn.update(table).set(data).where(eq(table.id, id)).returning();
|
const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId));
|
||||||
|
let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x)));
|
||||||
|
if (difference.length > 0) {
|
||||||
|
this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`);
|
||||||
|
data.imageOrder = imageOrder;
|
||||||
|
}
|
||||||
|
const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
|
||||||
|
return updateListing as BusinessListing | CommercialPropertyListing;
|
||||||
|
}
|
||||||
|
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
|
data.updated = new Date();
|
||||||
|
data.created = new Date(data.created);
|
||||||
|
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
|
||||||
return updateListing as BusinessListing | CommercialPropertyListing;
|
return updateListing as BusinessListing | CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
|
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
|
||||||
await this.conn.delete(table).where(eq(table.id, id));
|
await this.conn.delete(table).where(eq(table.id, id));
|
||||||
}
|
}
|
||||||
@@ -106,23 +169,17 @@ export class ListingsService {
|
|||||||
// ##############################################################
|
// ##############################################################
|
||||||
// Images for commercial Properties
|
// Images for commercial Properties
|
||||||
// ##############################################################
|
// ##############################################################
|
||||||
|
async deleteImage(imagePath: string, serial: string, name: string) {
|
||||||
async updateImageOrder(id: string, imageOrder: string[]) {
|
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||||
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
|
|
||||||
listing.imageOrder = imageOrder;
|
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
|
||||||
}
|
|
||||||
async deleteImage(id: string, name: string) {
|
|
||||||
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
|
|
||||||
const index = listing.imageOrder.findIndex(im => im === name);
|
const index = listing.imageOrder.findIndex(im => im === name);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
listing.imageOrder.splice(index, 1);
|
listing.imageOrder.splice(index, 1);
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async addImage(imagePath: string, imagename: string) {
|
async addImage(imagePath: string, serial: string, imagename: string) {
|
||||||
const listing = (await this.findByImagePath(imagePath)) as unknown as CommercialPropertyListing;
|
const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing;
|
||||||
listing.imageOrder.push(imagename);
|
listing.imageOrder.push(imagename);
|
||||||
await this.updateListing(listing.id, listing, commercials);
|
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Controller, Get, Inject, Param } from '@nestjs/common';
|
import { Controller, Inject } from '@nestjs/common';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import { businesses, commercials } from '../drizzle/schema.js';
|
|
||||||
import { ListingsService } from './listings.service.js';
|
import { ListingsService } from './listings.service.js';
|
||||||
|
|
||||||
@Controller('listings/undefined')
|
@Controller('listings/undefined')
|
||||||
@@ -11,13 +10,13 @@ export class UnknownListingsController {
|
|||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get(':id')
|
// @Get(':id')
|
||||||
async findById(@Param('id') id: string): Promise<any> {
|
// async findById(@Param('id') id: string): Promise<any> {
|
||||||
const result = await this.listingsService.findById(id, businesses);
|
// const result = await this.listingsService.findById(id, businesses);
|
||||||
if (result) {
|
// if (result) {
|
||||||
return result;
|
// return result;
|
||||||
} else {
|
// } else {
|
||||||
return await this.listingsService.findById(id, commercials);
|
// return await this.listingsService.findById(id, commercials);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export class MailController {
|
|||||||
constructor(private mailService: MailService) {}
|
constructor(private mailService: MailService) {}
|
||||||
@Post()
|
@Post()
|
||||||
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||||
return this.mailService.sendInquiry(mailInfo);
|
if (mailInfo.listing) {
|
||||||
|
return this.mailService.sendInquiry(mailInfo);
|
||||||
|
} else {
|
||||||
|
return this.mailService.sendRequest(mailInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
83
bizmatch-server/src/mail/templates/request.hbs
Normal file
83
bizmatch-server/src/mail/templates/request.hbs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Notification: New User Request</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.subheader {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
color: #1E90FF;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 2px solid #1E90FF;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.info:nth-child(even) {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
.info-value {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #555555;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">Notification: New request from a user of the Bizmatch Network</div>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Requester Information</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="info-label">Contact Name:</span>
|
||||||
|
<span class="info-value">{{iname}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="info-label">Contact Email:</span>
|
||||||
|
<span class="info-value">{{email}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="info-label">Contact Phone:</span>
|
||||||
|
<span class="info-value">{{phone}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info">
|
||||||
|
<span class="info-label">Question:</span>
|
||||||
|
<span class="info-value">{{request}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module.js';
|
import express from 'express';
|
||||||
import * as express from 'express';
|
import path from 'path';
|
||||||
import path, { join } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
|
import { AppModule } from './app.module.js';
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
const server = express();
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.setGlobalPrefix('bizmatch');
|
app.setGlobalPrefix('bizmatch');
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: '*',
|
origin: '*',
|
||||||
|
//origin: 'http://localhost:4200', // Die URL Ihrer Angular-App
|
||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
allowedHeaders: 'Content-Type, Accept',
|
allowedHeaders: 'Content-Type, Accept, Authorization',
|
||||||
});
|
});
|
||||||
//origin: 'http://localhost:4200',
|
//origin: 'http://localhost:4200',
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export interface User {
|
|||||||
hasCompanyLogo?: boolean;
|
hasCompanyLogo?: boolean;
|
||||||
licensedIn?: LicensedIn[];
|
licensedIn?: LicensedIn[];
|
||||||
gender?: 'male' | 'female';
|
gender?: 'male' | 'female';
|
||||||
|
customerType?: 'buyer' | 'broker' | 'professional';
|
||||||
created?: Date;
|
created?: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
}
|
}
|
||||||
@@ -35,6 +36,7 @@ export interface UserData {
|
|||||||
hasCompanyLogo?: boolean;
|
hasCompanyLogo?: boolean;
|
||||||
licensedIn?: string[];
|
licensedIn?: string[];
|
||||||
gender?: 'male' | 'female';
|
gender?: 'male' | 'female';
|
||||||
|
customerType?: 'buyer' | 'broker' | 'professional';
|
||||||
created?: Date;
|
created?: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
}
|
}
|
||||||
@@ -61,6 +63,7 @@ export interface BusinessListing {
|
|||||||
reasonForSale?: string;
|
reasonForSale?: string;
|
||||||
brokerLicencing?: string;
|
brokerLicencing?: string;
|
||||||
internals?: string;
|
internals?: string;
|
||||||
|
imageName?: string;
|
||||||
created?: Date;
|
created?: Date;
|
||||||
updated?: Date;
|
updated?: Date;
|
||||||
visits?: number;
|
visits?: number;
|
||||||
@@ -70,6 +73,7 @@ export interface BusinessListing {
|
|||||||
|
|
||||||
export interface CommercialPropertyListing {
|
export interface CommercialPropertyListing {
|
||||||
id: string;
|
id: string;
|
||||||
|
serialId?: number;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
type?: number;
|
type?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ export interface KeycloakUser {
|
|||||||
notBefore?: number;
|
notBefore?: number;
|
||||||
access?: Access;
|
access?: Access;
|
||||||
}
|
}
|
||||||
|
export interface JwtUser {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
export interface Access {
|
export interface Access {
|
||||||
manageGroupMembership: boolean;
|
manageGroupMembership: boolean;
|
||||||
view: boolean;
|
view: boolean;
|
||||||
@@ -130,6 +135,14 @@ export interface JwtToken {
|
|||||||
email: string;
|
email: string;
|
||||||
user_id: string;
|
user_id: string;
|
||||||
}
|
}
|
||||||
|
export interface JwtPayload {
|
||||||
|
sub: string;
|
||||||
|
preferred_username: string;
|
||||||
|
realm_access?: {
|
||||||
|
roles?: string[];
|
||||||
|
};
|
||||||
|
[key: string]: any; // für andere optionale Felder im JWT-Payload
|
||||||
|
}
|
||||||
interface Resourceaccess {
|
interface Resourceaccess {
|
||||||
account: Realmaccess;
|
account: Realmaccess;
|
||||||
}
|
}
|
||||||
@@ -193,6 +206,9 @@ export function isEmpty(value: any): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
export function emailToDirName(email: string): string {
|
export function emailToDirName(email: string): string {
|
||||||
|
if (email === undefined || email === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche
|
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche
|
||||||
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');
|
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
|
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RequestDurationMiddleware implements NestMiddleware {
|
export class RequestDurationMiddleware implements NestMiddleware {
|
||||||
@@ -8,8 +8,17 @@ export class RequestDurationMiddleware implements NestMiddleware {
|
|||||||
use(req: Request, res: Response, next: NextFunction) {
|
use(req: Request, res: Response, next: NextFunction) {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
res.on('finish', () => {
|
res.on('finish', () => {
|
||||||
|
// const duration = Date.now() - start;
|
||||||
|
// this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
|
||||||
const duration = Date.now() - start;
|
const duration = Date.now() - start;
|
||||||
this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
|
let logMessage = `${req.method} ${req.url} - ${duration}ms`;
|
||||||
|
|
||||||
|
if (req.method === 'POST' || req.method === 'PUT') {
|
||||||
|
const body = JSON.stringify(req.body);
|
||||||
|
logMessage += ` - Body: ${body}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(logMessage);
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import { SelectOptionsService } from './select-options.service.js';
|
|||||||
|
|
||||||
@Controller('select-options')
|
@Controller('select-options')
|
||||||
export class SelectOptionsController {
|
export class SelectOptionsController {
|
||||||
constructor(private selectOptionsService:SelectOptionsService){}
|
constructor(private selectOptionsService: SelectOptionsService) {}
|
||||||
@Get()
|
@Get()
|
||||||
getSelectOption():any{
|
getSelectOption(): any {
|
||||||
return {
|
return {
|
||||||
typesOfBusiness:this.selectOptionsService.typesOfBusiness,
|
typesOfBusiness: this.selectOptionsService.typesOfBusiness,
|
||||||
prices:this.selectOptionsService.prices,
|
prices: this.selectOptionsService.prices,
|
||||||
listingCategories:this.selectOptionsService.listingCategories,
|
listingCategories: this.selectOptionsService.listingCategories,
|
||||||
categories:this.selectOptionsService.categories,
|
customerTypes: this.selectOptionsService.customerTypes,
|
||||||
locations:this.selectOptionsService.locations,
|
locations: this.selectOptionsService.locations,
|
||||||
typesOfCommercialProperty:this.selectOptionsService.typesOfCommercialProperty,
|
typesOfCommercialProperty: this.selectOptionsService.typesOfCommercialProperty,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,14 @@ export class SelectOptionsService {
|
|||||||
{ name: 'Business', value: 'business' },
|
{ name: 'Business', value: 'business' },
|
||||||
{ name: 'Commercial Property', value: 'commercialProperty' },
|
{ name: 'Commercial Property', value: 'commercialProperty' },
|
||||||
];
|
];
|
||||||
public categories: Array<KeyValueStyle> = [
|
public customerTypes: Array<KeyValue> = [
|
||||||
{ name: 'Broker', value: 'broker', icon: 'pi-image', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' },
|
{ name: 'Buyer', value: 'buyer' },
|
||||||
{ name: 'Professional', value: 'professional', icon: 'pi-globe', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
|
{ name: 'Broker', value: 'broker' },
|
||||||
|
{ name: 'Professional', value: 'professional' },
|
||||||
|
];
|
||||||
|
public gender: Array<KeyValue> = [
|
||||||
|
{ name: 'Male', value: 'male' },
|
||||||
|
{ name: 'Female', value: 'female' },
|
||||||
];
|
];
|
||||||
public imageTypes: ImageType[] = [
|
public imageTypes: ImageType[] = [
|
||||||
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
|
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as schema from '../drizzle/schema.js';
|
|||||||
import { PG_CONNECTION } from '../drizzle/schema.js';
|
import { PG_CONNECTION } from '../drizzle/schema.js';
|
||||||
import { FileService } from '../file/file.service.js';
|
import { FileService } from '../file/file.service.js';
|
||||||
import { User } from '../models/db.model.js';
|
import { User } from '../models/db.model.js';
|
||||||
import { ListingCriteria } from '../models/main.model.js';
|
import { ListingCriteria, emailToDirName } from '../models/main.model.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
@@ -33,8 +33,8 @@ export class UserService {
|
|||||||
.from(schema.users)
|
.from(schema.users)
|
||||||
.where(sql`email = ${email}`)) as User[];
|
.where(sql`email = ${email}`)) as User[];
|
||||||
const user = users[0];
|
const user = users[0];
|
||||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(user.id);
|
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||||
user.hasProfile = this.fileService.hasProfile(user.id);
|
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
async getUserById(id: string) {
|
async getUserById(id: string) {
|
||||||
@@ -43,8 +43,8 @@ export class UserService {
|
|||||||
.from(schema.users)
|
.from(schema.users)
|
||||||
.where(sql`id = ${id}`)) as User[];
|
.where(sql`id = ${id}`)) as User[];
|
||||||
const user = users[0];
|
const user = users[0];
|
||||||
user.hasCompanyLogo = this.fileService.hasCompanyLogo(id);
|
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
|
||||||
user.hasProfile = this.fileService.hasProfile(id);
|
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
async saveUser(user: any): Promise<User> {
|
async saveUser(user: any): Promise<User> {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { provideRouter, withEnabledBlockingInitialNavigation, withInMemoryScroll
|
|||||||
|
|
||||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { KeycloakBearerInterceptor, KeycloakService } from 'keycloak-angular';
|
||||||
import { environment } from '../environments/environment';
|
import { environment } from '../environments/environment';
|
||||||
import { customKeycloakAdapter } from '../keycloak';
|
import { customKeycloakAdapter } from '../keycloak';
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
@@ -37,6 +37,11 @@ export const appConfig: ApplicationConfig = {
|
|||||||
useClass: LoadingInterceptor,
|
useClass: LoadingInterceptor,
|
||||||
multi: true,
|
multi: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: KeycloakBearerInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
provideRouter(
|
provideRouter(
|
||||||
routes,
|
routes,
|
||||||
withEnabledBlockingInitialNavigation(),
|
withEnabledBlockingInitialNavigation(),
|
||||||
@@ -89,6 +94,10 @@ function initializeKeycloak(keycloak: KeycloakService) {
|
|||||||
onLoad: 'check-sso',
|
onLoad: 'check-sso',
|
||||||
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
silentCheckSsoRedirectUri: (<any>window).location.origin + '/assets/silent-check-sso.html',
|
||||||
},
|
},
|
||||||
|
bearerExcludedUrls: ['/assets'],
|
||||||
|
shouldUpdateToken(request) {
|
||||||
|
return !request.headers.get('token-update') === false;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
logger.info(`+++>${authenticated}`);
|
logger.info(`+++>${authenticated}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<div class="col-12 md:col-3 text-500">
|
<div class="col-12 md:col-3 text-500">
|
||||||
<div class="text-black font-bold line-height-3 mb-3">Actions</div>
|
<div class="text-black font-bold line-height-3 mb-3">Actions</div>
|
||||||
<a *ngIf="!keycloakService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
|
<a *ngIf="!keycloakService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
|
||||||
|
<a *ngIf="!keycloakService.isLoggedIn()" (click)="register()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Register</a>
|
||||||
<a *ngIf="keycloakService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
|
<a *ngIf="keycloakService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
|
||||||
<a *ngIf="keycloakService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="keycloakService.logout()">Log Out</a>
|
<a *ngIf="keycloakService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="keycloakService.logout()">Log Out</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,4 +18,7 @@ export class FooterComponent {
|
|||||||
redirectUri: window.location.href,
|
redirectUri: window.location.href,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
register() {
|
||||||
|
this.keycloakService.register({ redirectUri: `${window.location.origin}/account` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,14 +91,17 @@ export class HeaderComponent {
|
|||||||
{
|
{
|
||||||
label: 'Businesses for Sale',
|
label: 'Businesses for Sale',
|
||||||
routerLink: '/businessListings',
|
routerLink: '/businessListings',
|
||||||
|
state: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Commercial Property',
|
label: 'Commercial Property',
|
||||||
routerLink: '/commercialPropertyListings',
|
routerLink: '/commercialPropertyListings',
|
||||||
|
state: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Professionals/Brokers Directory',
|
label: 'Professionals/Brokers Directory',
|
||||||
routerLink: '/brokerListings',
|
routerLink: '/brokerListings',
|
||||||
|
state: {},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
this.loginItems = [
|
this.loginItems = [
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="cropperConfig"></angular-cropper>
|
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="cropperConfig"></angular-cropper>
|
||||||
<div class="flex justify-content-between mt-3">
|
<div class="flex justify-content-between mt-3">
|
||||||
@if(ratioVariable){
|
@if(ratioVariable){
|
||||||
<div>
|
<div>
|
||||||
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)"
|
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)" optionLabel="label" optionValue="value" class="small"></p-selectButton>
|
||||||
optionLabel="label" optionValue="value"></p-selectButton>
|
</div>
|
||||||
</div>
|
} @else {
|
||||||
} @else {
|
<div></div>
|
||||||
<div></div>
|
}
|
||||||
}
|
<div class="flex justify-content-between">
|
||||||
<div>
|
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true" size="small" class="mr-2"></p-button>
|
||||||
<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" size="small"></p-button>
|
||||||
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
::ng-deep p-selectbutton.small .p-button {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.65625rem 1.09375rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,67 +1,52 @@
|
|||||||
import { Component, ViewChild } from '@angular/core';
|
import { Component, ViewChild } from '@angular/core';
|
||||||
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
|
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
|
||||||
import { LoadingService } from '../../services/loading.service';
|
|
||||||
import { ImageService } from '../../services/image.service';
|
|
||||||
import { HttpEventType } from '@angular/common/http';
|
|
||||||
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { User } from '../../../../../bizmatch-server/src/models/db.model';
|
|
||||||
import { SharedModule } from '../../shared/shared/shared.module';
|
|
||||||
import { SelectButtonModule } from 'primeng/selectbutton';
|
import { SelectButtonModule } from 'primeng/selectbutton';
|
||||||
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
|
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
export const stateOptions:KeyValueRatio[]=[
|
import { ImageService } from '../../services/image.service';
|
||||||
{label:'16/9',value:16/9},
|
import { LoadingService } from '../../services/loading.service';
|
||||||
{label:'1/1',value:1},
|
import { SharedModule } from '../../shared/shared/shared.module';
|
||||||
{label:'2/3',value:2/3},
|
export const stateOptions: KeyValueRatio[] = [
|
||||||
]
|
{ label: '16/9', value: 16 / 9 },
|
||||||
|
{ label: '1/1', value: 1 },
|
||||||
|
{ label: 'Free', value: NaN },
|
||||||
|
];
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-image-cropper',
|
selector: 'app-image-cropper',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule,FileUploadModule,AngularCropperjsModule,SelectButtonModule],
|
imports: [SharedModule, FileUploadModule, AngularCropperjsModule, SelectButtonModule],
|
||||||
templateUrl: './image-cropper.component.html',
|
templateUrl: './image-cropper.component.html',
|
||||||
styleUrl: './image-cropper.component.scss'
|
styleUrl: './image-cropper.component.scss',
|
||||||
})
|
})
|
||||||
export class ImageCropperComponent {
|
export class ImageCropperComponent {
|
||||||
@ViewChild(CropperComponent) public angularCropper: CropperComponent;
|
@ViewChild(CropperComponent) public angularCropper: CropperComponent;
|
||||||
imageUrl:string; //wird im Template verwendet
|
imageUrl: string; //wird im Template verwendet
|
||||||
fileUpload:FileUpload
|
fileUpload: FileUpload;
|
||||||
value:number = stateOptions[0].value;
|
value: number = stateOptions[0].value;
|
||||||
cropperConfig={aspectRatio: this.value}
|
cropperConfig = { aspectRatio: this.value };
|
||||||
ratioVariable:boolean
|
ratioVariable: boolean;
|
||||||
stateOptions=stateOptions
|
stateOptions = stateOptions;
|
||||||
constructor(
|
constructor(private loadingService: LoadingService, private imageUploadService: ImageService, public config: DynamicDialogConfig, public ref: DynamicDialogRef) {}
|
||||||
private loadingService:LoadingService,
|
|
||||||
private imageUploadService: ImageService,
|
|
||||||
public config: DynamicDialogConfig,
|
|
||||||
public ref: DynamicDialogRef
|
|
||||||
){}
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
if (this.config.data) {
|
if (this.config.data) {
|
||||||
this.imageUrl = this.config.data.imageUrl;
|
this.imageUrl = this.config.data.imageUrl;
|
||||||
this.fileUpload = this.config.data.fileUpload;
|
this.fileUpload = this.config.data.fileUpload;
|
||||||
this.cropperConfig = this.config.data.config ? this.config.data.config: this.cropperConfig;
|
this.cropperConfig = this.config.data.config ? this.config.data.config : this.cropperConfig;
|
||||||
this.ratioVariable = this.config.data.ratioVariable;
|
this.ratioVariable = this.config.data.ratioVariable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sendImage(){
|
sendImage() {
|
||||||
// setTimeout(()=>{
|
this.fileUpload.clear();
|
||||||
// this.angularCropper.cropper.getCroppedCanvas().toBlob(async(blob) => {
|
|
||||||
// this.ref.close(blob);
|
|
||||||
// this.fileUpload.clear()
|
|
||||||
// }, 'image/jpg');
|
|
||||||
// },0)
|
|
||||||
|
|
||||||
this.fileUpload.clear()
|
|
||||||
this.ref.close(this.angularCropper.cropper);
|
this.ref.close(this.angularCropper.cropper);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelUpload(){
|
cancelUpload() {
|
||||||
this.fileUpload.clear();
|
this.fileUpload.clear();
|
||||||
this.ref.close();
|
this.ref.close();
|
||||||
}
|
}
|
||||||
changeAspectRation(ratio:number){
|
changeAspectRation(ratio: number) {
|
||||||
this.cropperConfig={aspectRatio: ratio}
|
this.cropperConfig = { aspectRatio: ratio };
|
||||||
this.angularCropper.cropper.setAspectRatio(ratio);
|
this.angularCropper.cropper.setAspectRatio(ratio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export class AuthGuard extends KeycloakAuthGuard {
|
|||||||
}
|
}
|
||||||
// Force the user to log in if currently unauthenticated.
|
// Force the user to log in if currently unauthenticated.
|
||||||
const authenticated = this.keycloak.isLoggedIn();
|
const authenticated = this.keycloak.isLoggedIn();
|
||||||
|
//this.keycloak.isTokenExpired()
|
||||||
if (!this.authenticated && !authenticated) {
|
if (!this.authenticated && !authenticated) {
|
||||||
await this.keycloak.login({
|
await this.keycloak.login({
|
||||||
redirectUri: window.location.origin + state.url,
|
redirectUri: window.location.origin + state.url,
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,50 +14,62 @@
|
|||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium flex">Description</div>
|
<div class="text-500 w-full md:w-3 font-medium flex">Description</div>
|
||||||
<div class="text-900 w-full md:w-10 line-height-3 flex flex-column" [innerHTML]="description"></div>
|
<div class="text-900 w-full md:w-9 line-height-3 flex flex-column" [innerHTML]="description"></div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Category</div>
|
<div class="text-500 w-full md:w-3 font-medium">Category</div>
|
||||||
<div class="text-900 w-full md:w-10">
|
<div class="text-900 w-full md:w-9">
|
||||||
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
|
<p-chip [label]="selectOptions.getBusiness(listing.type)"></p-chip>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
|
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ selectOptions.getState(listing.state) }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.city }}, {{ selectOptions.getState(listing.state) }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Asking Price</div>
|
<div class="text-500 w-full md:w-3 font-medium">Asking Price</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.price | currency }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Real Estate Included</div>
|
<div class="text-500 w-full md:w-3 font-medium">Sales revenue</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.realEstateIncluded ? 'Yes' : 'No' }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.salesRevenue | currency }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Sales revenue</div>
|
<div class="text-500 w-full md:w-3 font-medium">Cash flow</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.salesRevenue | currency }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.cashFlow | currency }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Cash flow</div>
|
<div class="text-500 w-full md:w-3 font-medium">Type of Real Estate</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.cashFlow | currency }}</div>
|
<div class="text-900 w-full md:w-9">
|
||||||
|
@if (listing.realEstateIncluded){
|
||||||
|
<p-chip label="Real Estate Included"></p-chip>
|
||||||
|
} @if (listing.leasedLocation){
|
||||||
|
<p-chip label="Leased Location"></p-chip>
|
||||||
|
} @if (listing.franchiseResale){
|
||||||
|
<p-chip label="Franchise Re-Sale"></p-chip>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Employees</div>
|
<div class="text-500 w-full md:w-3 font-medium">Employees</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.employees }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.employees }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Support & Training</div>
|
<div class="text-500 w-full md:w-3 font-medium">Established since</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.supportAndTraining }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.established }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Reason for Sale</div>
|
<div class="text-500 w-full md:w-3 font-medium">Support & Training</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.reasonForSale }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.supportAndTraining }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Broker licensing</div>
|
<div class="text-500 w-full md:w-3 font-medium">Reason for Sale</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.brokerLicencing }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.reasonForSale }}</div>
|
||||||
|
</li>
|
||||||
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
|
<div class="text-500 w-full md:w-3 font-medium">Broker licensing</div>
|
||||||
|
<div class="text-900 w-full md:w-9">{{ listing.brokerLicencing }}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
|
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
|
||||||
@@ -79,7 +91,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field mb-4 col-12 md:col-6">
|
<div class="field mb-4 col-12 md:col-6">
|
||||||
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
|
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
|
||||||
<input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" />
|
<!-- <input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" /> -->
|
||||||
|
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
|
||||||
</div>
|
</div>
|
||||||
<div class="field mb-4 col-12 md:col-6">
|
<div class="field mb-4 col-12 md:col-6">
|
||||||
<label for="state" class="font-medium text-900">Country/State</label>
|
<label for="state" class="font-medium text-900">Country/State</label>
|
||||||
@@ -92,9 +105,9 @@
|
|||||||
</div>
|
</div>
|
||||||
@if(listingUser){
|
@if(listingUser){
|
||||||
<div class="surface-border mb-4 col-12 flex align-items-center">
|
<div class="surface-border mb-4 col-12 flex align-items-center">
|
||||||
Listing by <a routerLink="/details-user/{{ listingUser.id }}" class="mr-2">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
Listing by <a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
||||||
@if(listingUser.hasCompanyLogo){
|
@if(listingUser.hasCompanyLogo){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listingUser.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 30px; max-width: 100px" />
|
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { KeycloakService } from 'keycloak-angular';
|
|||||||
import onChange from 'on-change';
|
import onChange from 'on-change';
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
import { GalleriaModule } from 'primeng/galleria';
|
import { GalleriaModule } from 'primeng/galleria';
|
||||||
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
@@ -15,12 +16,11 @@ import { MailService } from '../../../services/mail.service';
|
|||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
import { getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
|
import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../../../utils/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-details-business-listing',
|
selector: 'app-details-business-listing',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, GalleriaModule],
|
imports: [SharedModule, GalleriaModule, InputMaskModule],
|
||||||
providers: [MessageService],
|
providers: [MessageService],
|
||||||
templateUrl: './details-business-listing.component.html',
|
templateUrl: './details-business-listing.component.html',
|
||||||
styleUrl: './details-business-listing.component.scss',
|
styleUrl: './details-business-listing.component.scss',
|
||||||
@@ -49,7 +49,8 @@ export class DetailsBusinessListingComponent {
|
|||||||
criteria: ListingCriteria;
|
criteria: ListingCriteria;
|
||||||
mailinfo: MailInfo;
|
mailinfo: MailInfo;
|
||||||
environment = environment;
|
environment = environment;
|
||||||
user: KeycloakUser;
|
keycloakUser: KeycloakUser;
|
||||||
|
user: User;
|
||||||
listingUser: User;
|
listingUser: User;
|
||||||
description: SafeHtml;
|
description: SafeHtml;
|
||||||
private history: string[] = [];
|
private history: string[] = [];
|
||||||
@@ -73,11 +74,16 @@ export class DetailsBusinessListingComponent {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
||||||
this.user;
|
|
||||||
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
|
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
const token = await this.keycloakService.getToken();
|
||||||
|
this.keycloakUser = map2User(token);
|
||||||
|
if (this.keycloakUser) {
|
||||||
|
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||||
|
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
|
||||||
|
}
|
||||||
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
|
||||||
this.listingUser = await this.userService.getById(this.listing.userId);
|
this.listingUser = await this.userService.getById(this.listing.userId);
|
||||||
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
|
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
|
||||||
|
|||||||
@@ -14,32 +14,32 @@
|
|||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
<ul class="list-none p-0 m-0 border-top-1 border-300">
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium flex">Description</div>
|
<div class="text-500 w-full md:w-3 font-medium flex">Description</div>
|
||||||
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div>
|
<div class="text-900 w-full md:w-9 line-height-3" [innerHTML]="description"></div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Property Category</div>
|
<div class="text-500 w-full md:w-3 font-medium">Property Category</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
|
<div class="text-900 w-full md:w-9">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Located in</div>
|
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ selectOptions.getState(listing.state) }}</div>
|
<div class="text-900 w-full md:w-9">{{ selectOptions.getState(listing.state) }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">City</div>
|
<div class="text-500 w-full md:w-3 font-medium">City</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.city }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.city }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Zip Code</div>
|
<div class="text-500 w-full md:w-3 font-medium">Zip Code</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.zipCode }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.zipCode }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">County</div>
|
<div class="text-500 w-full md:w-3 font-medium">County</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.county }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.county }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Asking Price:</div>
|
<div class="text-500 w-full md:w-3 font-medium">Asking Price:</div>
|
||||||
<div class="text-900 w-full md:w-10">{{ listing.price | currency }}</div>
|
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@@ -49,9 +49,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-6">
|
<div class="col-12 md:col-6">
|
||||||
<p-galleria [value]="propertyImages" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
|
<p-galleria [value]="listing.imageOrder" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
|
||||||
<ng-template pTemplate="item" let-item>
|
<ng-template pTemplate="item" let-item>
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ item }}" style="width: 100%" />
|
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ item }}" style="width: 100%" />
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</p-galleria>
|
</p-galleria>
|
||||||
@if (mailinfo){
|
@if (mailinfo){
|
||||||
@@ -69,7 +69,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field mb-4 col-12 md:col-6">
|
<div class="field mb-4 col-12 md:col-6">
|
||||||
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
|
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
|
||||||
<input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" />
|
<!-- <input id="phoneNumber" type="text" pInputText [(ngModel)]="mailinfo.sender.phoneNumber" /> -->
|
||||||
|
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
|
||||||
</div>
|
</div>
|
||||||
<div class="field mb-4 col-12 md:col-6">
|
<div class="field mb-4 col-12 md:col-6">
|
||||||
<label for="state" class="font-medium text-900">Country/State</label>
|
<label for="state" class="font-medium text-900">Country/State</label>
|
||||||
@@ -82,9 +83,9 @@
|
|||||||
</div>
|
</div>
|
||||||
@if(listingUser){
|
@if(listingUser){
|
||||||
<div class="surface-border mb-4 col-12 flex align-items-center">
|
<div class="surface-border mb-4 col-12 flex align-items-center">
|
||||||
Listing by <a routerLink="/details-user/{{ listingUser.id }}" class="mr-2">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
Listing by <a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
|
||||||
@if(listingUser.hasCompanyLogo){
|
@if(listingUser.hasCompanyLogo){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listingUser.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 30px; max-width: 100px" />
|
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imagePath }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { KeycloakService } from 'keycloak-angular';
|
|||||||
import onChange from 'on-change';
|
import onChange from 'on-change';
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
import { GalleriaModule } from 'primeng/galleria';
|
import { GalleriaModule } from 'primeng/galleria';
|
||||||
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { ErrorResponse, KeycloakUser, ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { HistoryService } from '../../../services/history.service';
|
import { HistoryService } from '../../../services/history.service';
|
||||||
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { MailService } from '../../../services/mail.service';
|
import { MailService } from '../../../services/mail.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
@@ -20,13 +22,12 @@ import { getCriteriaStateObject, getSessionStorageHandler, map2User } from '../.
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-details-commercial-property-listing',
|
selector: 'app-details-commercial-property-listing',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, GalleriaModule],
|
imports: [SharedModule, GalleriaModule, InputMaskModule],
|
||||||
providers: [MessageService],
|
providers: [MessageService],
|
||||||
templateUrl: './details-commercial-property-listing.component.html',
|
templateUrl: './details-commercial-property-listing.component.html',
|
||||||
styleUrl: './details-commercial-property-listing.component.scss',
|
styleUrl: './details-commercial-property-listing.component.scss',
|
||||||
})
|
})
|
||||||
export class DetailsCommercialPropertyListingComponent {
|
export class DetailsCommercialPropertyListingComponent {
|
||||||
// listings: Array<BusinessListing>;
|
|
||||||
responsiveOptions = [
|
responsiveOptions = [
|
||||||
{
|
{
|
||||||
breakpoint: '1199px',
|
breakpoint: '1199px',
|
||||||
@@ -48,9 +49,9 @@ export class DetailsCommercialPropertyListingComponent {
|
|||||||
listing: CommercialPropertyListing;
|
listing: CommercialPropertyListing;
|
||||||
criteria: ListingCriteria;
|
criteria: ListingCriteria;
|
||||||
mailinfo: MailInfo;
|
mailinfo: MailInfo;
|
||||||
propertyImages: string[] = [];
|
|
||||||
environment = environment;
|
environment = environment;
|
||||||
user: KeycloakUser;
|
keycloakUser: KeycloakUser;
|
||||||
|
user: User;
|
||||||
listingUser: User;
|
listingUser: User;
|
||||||
description: SafeHtml;
|
description: SafeHtml;
|
||||||
ts = new Date().getTime();
|
ts = new Date().getTime();
|
||||||
@@ -67,6 +68,7 @@ export class DetailsCommercialPropertyListingComponent {
|
|||||||
private sanitizer: DomSanitizer,
|
private sanitizer: DomSanitizer,
|
||||||
public historyService: HistoryService,
|
public historyService: HistoryService,
|
||||||
public keycloakService: KeycloakService,
|
public keycloakService: KeycloakService,
|
||||||
|
private imageService: ImageService,
|
||||||
) {
|
) {
|
||||||
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
||||||
|
|
||||||
@@ -75,9 +77,12 @@ export class DetailsCommercialPropertyListingComponent {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const token = await this.keycloakService.getToken();
|
const token = await this.keycloakService.getToken();
|
||||||
this.user = map2User(token);
|
this.keycloakUser = map2User(token);
|
||||||
|
if (this.keycloakUser) {
|
||||||
|
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||||
|
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
|
||||||
|
}
|
||||||
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
|
||||||
this.listingUser = await this.userService.getById(this.listing.userId);
|
this.listingUser = await this.userService.getById(this.listing.userId);
|
||||||
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
|
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between">
|
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between">
|
||||||
<div class="flex align-items-start flex-column md:flex-row">
|
<div class="flex align-items-start flex-column md:flex-row">
|
||||||
@if(user.hasProfile){
|
@if(user.hasProfile){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures//profile/{{ user.id }}.avif?_ts={{ ts }}" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
|
<img src="{{ env.imageBaseUrl }}/pictures//profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
|
<img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mr-5 mt-3">
|
<div class="mr-5 mt-3">
|
||||||
<span class="font-medium text-500">For Sale</span>
|
<span class="font-medium text-500">For Sale</span>
|
||||||
<div class="text-700 mt-2">{{ businessListings.length + commercialPropListings.length }}</div>
|
<div class="text-700 mt-2">{{ businessListings?.length + commercialPropListings?.length }}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="mr-5 mt-3">
|
<!-- <div class="mr-5 mt-3">
|
||||||
<span class="font-medium text-500">Sold</span>
|
<span class="font-medium text-500">Sold</span>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<!-- <span class="font-medium text-500">Logo</span> -->
|
<!-- <span class="font-medium text-500">Logo</span> -->
|
||||||
<div>
|
<div>
|
||||||
@if(user.hasCompanyLogo){
|
@if(user.hasCompanyLogo){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ user.id }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="height: 60px; max-width: 100px" />
|
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 60px; max-width: 100px" />
|
||||||
}
|
}
|
||||||
<!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png"
|
<!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png"
|
||||||
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> -->
|
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> -->
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
<li class="flex align-items-center py-3 px-2 flex-wrap">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">Phone Number</div>
|
<div class="text-500 w-full md:w-2 font-medium">Phone Number</div>
|
||||||
<div class="text-900 w-full md:w-10 line-height-3">{{ user.phoneNumber }}</div>
|
<div class="text-900 w-full md:w-10 line-height-3">{{ formatPhoneNumber(user.phoneNumber) }}</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
|
||||||
<div class="text-500 w-full md:w-2 font-medium">EMail Address</div>
|
<div class="text-500 w-full md:w-2 font-medium">EMail Address</div>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
<div class="p-3 border-1 surface-border border-round surface-card">
|
<div class="p-3 border-1 surface-border border-round surface-card">
|
||||||
<div class="text-900 mb-2 flex align-items-center">
|
<div class="text-900 mb-2 flex align-items-center">
|
||||||
@if (listing.imageOrder?.length>0){
|
@if (listing.imageOrder?.length>0){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" />
|
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" />
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" />
|
<img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { MessageService } from 'primeng/api';
|
|||||||
import { GalleriaModule } from 'primeng/galleria';
|
import { GalleriaModule } from 'primeng/galleria';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { KeycloakUser, ListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { KeycloakUser, ListingCriteria, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { HistoryService } from '../../../services/history.service';
|
import { HistoryService } from '../../../services/history.service';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
@@ -14,7 +14,7 @@ import { ListingsService } from '../../../services/listings.service';
|
|||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
import { map2User } from '../../../utils/utils';
|
import { formatPhoneNumber, map2User } from '../../../utils/utils';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-details-user',
|
selector: 'app-details-user',
|
||||||
@@ -37,6 +37,8 @@ export class DetailsUserComponent {
|
|||||||
offeredServices: SafeHtml;
|
offeredServices: SafeHtml;
|
||||||
ts = new Date().getTime();
|
ts = new Date().getTime();
|
||||||
env = environment;
|
env = environment;
|
||||||
|
emailToDirName = emailToDirName;
|
||||||
|
formatPhoneNumber = formatPhoneNumber;
|
||||||
constructor(
|
constructor(
|
||||||
private activatedRoute: ActivatedRoute,
|
private activatedRoute: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -52,8 +54,7 @@ export class DetailsUserComponent {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.user = await this.userService.getById(this.id);
|
this.user = await this.userService.getById(this.id);
|
||||||
this.user.email;
|
const results = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
||||||
const results = await Promise.all([await this.listingsService.getListingByUserId(this.id, 'business'), await this.listingsService.getListingByUserId(this.id, 'commercialProperty')]);
|
|
||||||
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
|
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
|
||||||
this.businessListings = results[0];
|
this.businessListings = results[0];
|
||||||
this.commercialPropListings = results[1];
|
this.commercialPropListings = results[1];
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="surface-card p-4 flex flex-column align-items-center md:flex-row md:align-items-stretch h-full">
|
<div class="surface-card p-4 flex flex-column align-items-center md:flex-row md:align-items-stretch h-full">
|
||||||
<span>
|
<span>
|
||||||
@if(user.hasProfile){
|
@if(user.hasProfile){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/profile/{{ user.id }}.avif?_ts={{ ts }}" class="w-5rem" />
|
<img src="{{ env.imageBaseUrl }}/pictures/profile/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="w-5rem" />
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/person_placeholder.jpg" class="w-5rem" />
|
<img src="assets/images/person_placeholder.jpg" class="w-5rem" />
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="px-4 py-3 text-right flex justify-content-between align-items-center">
|
<div class="px-4 py-3 text-right flex justify-content-between align-items-center">
|
||||||
@if(user.hasCompanyLogo){
|
@if(user.hasCompanyLogo){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ user.id }}.avif?_ts={{ ts }}" class="rounded-image" />
|
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ emailToDirName(user.email) }}.avif?_ts={{ ts }}" class="rounded-image" />
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/placeholder.png" class="rounded-image" />
|
<img src="assets/images/placeholder.png" class="rounded-image" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
#sky-line {
|
#sky-line {
|
||||||
background-image: url(../../../../assets/images/bw-sky.jpg);
|
background-image: url(../../../../assets/images/bw-sky.jpg);
|
||||||
height: 204px;
|
height: 204px;
|
||||||
background-position: bottom;
|
background-position: bottom;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
}
|
}
|
||||||
.search{
|
.search {
|
||||||
background-color: #343F69;
|
background-color: #343f69;
|
||||||
}
|
}
|
||||||
::ng-deep p-paginator div {
|
::ng-deep p-paginator div {
|
||||||
background-color: var(--surface-200) !important;
|
background-color: var(--surface-200) !important;
|
||||||
// background-color: var(--surface-400) !important;
|
// background-color: var(--surface-400) !important;
|
||||||
}
|
}
|
||||||
.rounded-image {
|
.rounded-image {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
// width: 100px;
|
// width: 100px;
|
||||||
max-width: 100px;
|
max-width: 100px;
|
||||||
height: 45px;
|
max-height: 45px;
|
||||||
border: 1px solid rgba(0,0,0,0.2);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
padding: 1px 1px;
|
padding: 1px 1px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ import { PaginatorModule } from 'primeng/paginator';
|
|||||||
import { StyleClassModule } from 'primeng/styleclass';
|
import { StyleClassModule } from 'primeng/styleclass';
|
||||||
import { ToggleButtonModule } from 'primeng/togglebutton';
|
import { ToggleButtonModule } from 'primeng/togglebutton';
|
||||||
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { ListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
@@ -60,7 +60,7 @@ export class BrokerListingsComponent {
|
|||||||
ts = new Date().getTime();
|
ts = new Date().getTime();
|
||||||
env = environment;
|
env = environment;
|
||||||
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
||||||
|
emailToDirName = emailToDirName;
|
||||||
constructor(
|
constructor(
|
||||||
public selectOptions: SelectOptionsService,
|
public selectOptions: SelectOptionsService,
|
||||||
private listingsService: ListingsService,
|
private listingsService: ListingsService,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
@for (listing of listings; track listing.id) {
|
@for (listing of listings; track listing.id) {
|
||||||
<div class="col-12 lg:col-3 p-3">
|
<div class="col-12 lg:col-3 p-3">
|
||||||
<div class="shadow-2 border-round surface-card h-full flex-column justify-content-between flex">
|
<div class="shadow-2 border-round surface-card h-full flex-column justify-content-between flex">
|
||||||
<div class="p-4 flex flex-column relative">
|
<div class="p-4 flex flex-column relative h-full">
|
||||||
<div class="flex align-items-center">
|
<div class="flex align-items-center">
|
||||||
<span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px">
|
<span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px">
|
||||||
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i>
|
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{ listing.established }}</p>
|
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{ listing.established }}</p>
|
||||||
<div class="icon-pos">
|
<div class="icon-pos">
|
||||||
<a routerLink="/details-user/{{ listing.userId }}" class="mr-2"
|
<a routerLink="/details-user/{{ listing.userId }}" class="mr-2"
|
||||||
><img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.userId }}.avif?_ts={{ ts }}" (error)="imageErrorHandler(listing)" class="rounded-image"
|
><img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" (error)="imageErrorHandler(listing)" class="rounded-image"
|
||||||
/></a>
|
/></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { StyleClassModule } from 'primeng/styleclass';
|
|||||||
import { ToggleButtonModule } from 'primeng/togglebutton';
|
import { ToggleButtonModule } from 'primeng/togglebutton';
|
||||||
import { TooltipModule } from 'primeng/tooltip';
|
import { TooltipModule } from 'primeng/tooltip';
|
||||||
import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { ListingCriteria, ListingType, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ImageService } from '../../../services/image.service';
|
import { ImageService } from '../../../services/image.service';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
@@ -58,7 +58,7 @@ export class BusinessListingsComponent {
|
|||||||
rows: number = 12;
|
rows: number = 12;
|
||||||
env = environment;
|
env = environment;
|
||||||
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
|
||||||
|
emailToDirName = emailToDirName;
|
||||||
constructor(
|
constructor(
|
||||||
public selectOptions: SelectOptionsService,
|
public selectOptions: SelectOptionsService,
|
||||||
private listingsService: ListingsService,
|
private listingsService: ListingsService,
|
||||||
|
|||||||
@@ -51,7 +51,11 @@
|
|||||||
<article class="flex flex-column md:flex-row w-full gap-3 p-3 surface-card">
|
<article class="flex flex-column md:flex-row w-full gap-3 p-3 surface-card">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@if (listing.imageOrder?.length>0){
|
@if (listing.imageOrder?.length>0){
|
||||||
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
|
<img
|
||||||
|
src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}"
|
||||||
|
alt="Image"
|
||||||
|
class="border-round w-full h-full md:w-12rem md:h-9rem"
|
||||||
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
|
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,16 @@
|
|||||||
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
||||||
<div class="flex-auto p-fluid">
|
<div class="flex-auto p-fluid">
|
||||||
@if (user){
|
@if (user){
|
||||||
<div class="mb-4">
|
<div class="grid">
|
||||||
<label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label>
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email" />
|
<label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label>
|
||||||
<p class="font-italic text-sm line-height-1">You can only modify your email by contacting us at support@bizmatch.net</p>
|
<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@bizmatch.net</p>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4 col-12 md:col-6">
|
||||||
|
<label for="customerType" class="block font-medium text-900 mb-2">Customer Type</label>
|
||||||
|
<p-dropdown id="customerType" [options]="selectOptions?.customerTypes" [(ngModel)]="user.customerType" optionLabel="name" optionValue="value" placeholder="State" [style]="{ width: '100%' }"></p-dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="mb-4 col-12 md:col-6">
|
<div class="mb-4 col-12 md:col-6">
|
||||||
@@ -23,6 +29,7 @@
|
|||||||
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname" />
|
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (isProfessional){
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="mb-4 col-12 md:col-6">
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<label for="firstname" class="block font-medium text-900 mb-2">Company Name</label>
|
<label for="firstname" class="block font-medium text-900 mb-2">Company Name</label>
|
||||||
@@ -33,10 +40,11 @@
|
|||||||
<input id="lastname" type="text" pInputText [(ngModel)]="user.description" />
|
<input id="lastname" type="text" pInputText [(ngModel)]="user.description" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
} @if (isProfessional){
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="mb-4 col-12 md:col-4">
|
<div class="mb-4 col-12 md:col-4">
|
||||||
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
|
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
|
||||||
<input id="phoneNumber" type="text" pInputText [(ngModel)]="user.phoneNumber" />
|
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="user.phoneNumber"></p-inputMask>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4 col-12 md:col-4">
|
<div class="mb-4 col-12 md:col-4">
|
||||||
<label for="companyWebsite" class="block font-medium text-900 mb-2">Company Website</label>
|
<label for="companyWebsite" class="block font-medium text-900 mb-2">Company Website</label>
|
||||||
@@ -47,6 +55,14 @@
|
|||||||
<p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
|
<p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="grid">
|
||||||
|
<div class="mb-4 col-12 md:col-6">
|
||||||
|
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
|
||||||
|
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="user.phoneNumber"></p-inputMask>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @if (isProfessional){
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label>
|
<label for="companyOverview" class="block font-medium text-900 mb-2">Company Overview</label>
|
||||||
<p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }" [modules]="editorModules">
|
<p-editor [(ngModel)]="user.companyOverview" [style]="{ height: '320px' }" [modules]="editorModules">
|
||||||
@@ -59,7 +75,6 @@
|
|||||||
<ng-template pTemplate="header"></ng-template>
|
<ng-template pTemplate="header"></ng-template>
|
||||||
</p-editor>
|
</p-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label>
|
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label>
|
||||||
@for (areasServed of user.areasServed; track areasServed){
|
@for (areasServed of user.areasServed; track areasServed){
|
||||||
@@ -118,11 +133,12 @@
|
|||||||
<span class="text-xs"> (Add more licenses or remove existing ones.)</span>
|
<span class="text-xs"> (Add more licenses or remove existing ones.)</span>
|
||||||
<!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> -->
|
<!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> -->
|
||||||
</div>
|
</div>
|
||||||
}
|
} }
|
||||||
<div>
|
<div>
|
||||||
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
|
<button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@if (isProfessional){
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-column align-items-center flex-or mb-8">
|
<div class="flex flex-column align-items-center flex-or mb-8">
|
||||||
<span class="font-medium text-900 mb-2">Company Logo</span>
|
<span class="font-medium text-900 mb-2">Company Logo</span>
|
||||||
@@ -172,6 +188,7 @@
|
|||||||
></p-fileUpload>
|
></p-fileUpload>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-900 font-semibold text-lg mt-3">Membership Level</div>
|
<div class="text-900 font-semibold text-lg mt-3">Membership Level</div>
|
||||||
<p-divider></p-divider>
|
<p-divider></p-divider>
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
/* Stil für das FontAwesome Icon */
|
/* Stil für das FontAwesome Icon */
|
||||||
.image-wrap fa-icon {
|
.image-wrap fa-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 5px; /* Positioniert das Icon am oberen Rand des Bildes */
|
top: -5px; /* Positioniert das Icon am oberen Rand des Bildes */
|
||||||
right: 5px; /* Positioniert das Icon am rechten Rand des Bildes */
|
right: -18px; /* Positioniert das Icon am rechten Rand des Bildes */
|
||||||
color: #fff; /* Weiße Farbe für das Icon */
|
color: #fff; /* Weiße Farbe für das Icon */
|
||||||
background-color: rgba(0, 0, 0, 0.5); /* Halbtransparenter Hintergrund für bessere Sichtbarkeit */
|
background-color: rgba(0, 0, 0, 0.5); /* Halbtransparenter Hintergrund für bessere Sichtbarkeit */
|
||||||
padding: 5px; /* Ein wenig Platz um das Icon */
|
padding: 5px; /* Ein wenig Platz um das Icon */
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ import { DialogModule } from 'primeng/dialog';
|
|||||||
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
|
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { EditorModule } from 'primeng/editor';
|
import { EditorModule } from 'primeng/editor';
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
||||||
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { SelectButtonModule } from 'primeng/selectbutton';
|
import { SelectButtonModule } from 'primeng/selectbutton';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { AutoCompleteCompleteEvent, Invoice, Subscription } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { AutoCompleteCompleteEvent, Invoice, Subscription, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
|
import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
|
||||||
import { GeoService } from '../../../services/geo.service';
|
import { GeoService } from '../../../services/geo.service';
|
||||||
@@ -23,12 +24,12 @@ import { SelectOptionsService } from '../../../services/select-options.service';
|
|||||||
import { SubscriptionsService } from '../../../services/subscriptions.service';
|
import { SubscriptionsService } from '../../../services/subscriptions.service';
|
||||||
import { UserService } from '../../../services/user.service';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { SharedModule } from '../../../shared/shared/shared.module';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
import { map2User } from '../../../utils/utils';
|
import { getDialogWidth, getImageDimensions, map2User } from '../../../utils/utils';
|
||||||
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-account',
|
selector: 'app-account',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule, ConfirmDialogModule],
|
imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule, ConfirmDialogModule, InputMaskModule],
|
||||||
providers: [MessageService, DialogService, ConfirmationService],
|
providers: [MessageService, DialogService, ConfirmationService],
|
||||||
templateUrl: './account.component.html',
|
templateUrl: './account.component.html',
|
||||||
styleUrl: './account.component.scss',
|
styleUrl: './account.component.scss',
|
||||||
@@ -74,29 +75,29 @@ export class AccountComponent {
|
|||||||
try {
|
try {
|
||||||
this.user = await this.userService.getByMail(email);
|
this.user = await this.userService.getByMail(email);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.user = { email, firstname: keycloakUser.firstName, lastname: keycloakUser.lastName, areasServed: [], licensedIn: [], companyOverview: '', offeredServices: '' };
|
this.user = { email, firstname: keycloakUser.firstName, lastname: keycloakUser.lastName, areasServed: [], licensedIn: [], companyOverview: '', offeredServices: '', customerType: 'broker' };
|
||||||
this.user = await this.userService.save(this.user);
|
this.user = await this.userService.save(this.user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id));
|
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions(this.user.id));
|
||||||
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||||
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||||
}
|
}
|
||||||
printInvoice(invoice: Invoice) {}
|
printInvoice(invoice: Invoice) {}
|
||||||
|
|
||||||
async updateProfile(user: User) {
|
async updateProfile(user: User) {
|
||||||
await this.userService.save(this.user);
|
await this.userService.save(this.user);
|
||||||
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Acount changes have been persisted', life: 3000 });
|
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Account changes have been persisted', life: 3000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
onUploadCompanyLogo(event: any) {
|
onUploadCompanyLogo(event: any) {
|
||||||
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
||||||
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}${uniqueSuffix}`;
|
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}${uniqueSuffix}`;
|
||||||
}
|
}
|
||||||
onUploadProfilePicture(event: any) {
|
onUploadProfilePicture(event: any) {
|
||||||
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
const uniqueSuffix = '?_ts=' + new Date().getTime();
|
||||||
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}${uniqueSuffix}`;
|
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}${uniqueSuffix}`;
|
||||||
}
|
}
|
||||||
setImageToFallback(event: Event) {
|
setImageToFallback(event: Event) {
|
||||||
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
|
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
|
||||||
@@ -120,50 +121,53 @@ export class AccountComponent {
|
|||||||
removeArea() {
|
removeArea() {
|
||||||
this.user.areasServed.splice(this.user.areasServed.length - 1, 1);
|
this.user.areasServed.splice(this.user.areasServed.length - 1, 1);
|
||||||
}
|
}
|
||||||
|
get isProfessional() {
|
||||||
|
return this.user.customerType === 'broker' || this.user.customerType === 'professional';
|
||||||
|
}
|
||||||
select(event: any, type: 'company' | 'profile') {
|
select(event: any, type: 'company' | 'profile') {
|
||||||
const imageUrl = URL.createObjectURL(event.files[0]);
|
const imageUrl = URL.createObjectURL(event.files[0]);
|
||||||
this.type = type;
|
this.type = type;
|
||||||
const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value };
|
const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value };
|
||||||
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
|
getImageDimensions(imageUrl).then(dimensions => {
|
||||||
data: {
|
const dialogWidth = getDialogWidth(dimensions);
|
||||||
imageUrl: imageUrl,
|
|
||||||
fileUpload: type === 'company' ? this.companyUpload : this.profileUpload,
|
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
|
||||||
config: config,
|
data: {
|
||||||
ratioVariable: type === 'company' ? true : false,
|
imageUrl: imageUrl,
|
||||||
},
|
fileUpload: type === 'company' ? this.companyUpload : this.profileUpload,
|
||||||
header: 'Edit Image',
|
config: config,
|
||||||
width: '50vw',
|
ratioVariable: type === 'company' ? true : false,
|
||||||
modal: true,
|
},
|
||||||
closeOnEscape: true,
|
header: 'Edit Image',
|
||||||
keepInViewport: true,
|
width: dialogWidth,
|
||||||
closable: false,
|
modal: true,
|
||||||
breakpoints: {
|
closeOnEscape: true,
|
||||||
'960px': '75vw',
|
keepInViewport: true,
|
||||||
'640px': '90vw',
|
closable: false,
|
||||||
},
|
});
|
||||||
});
|
this.dialogRef.onClose.subscribe(cropper => {
|
||||||
this.dialogRef.onClose.subscribe(cropper => {
|
if (cropper) {
|
||||||
if (cropper) {
|
this.loadingService.startLoading('uploadImage');
|
||||||
this.loadingService.startLoading('uploadImage');
|
cropper.getCroppedCanvas().toBlob(async blob => {
|
||||||
cropper.getCroppedCanvas().toBlob(async blob => {
|
this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', emailToDirName(this.user.email)).subscribe(
|
||||||
this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', this.user.id).subscribe(
|
async event => {
|
||||||
async event => {
|
if (event.type === HttpEventType.Response) {
|
||||||
if (event.type === HttpEventType.Response) {
|
this.loadingService.stopLoading('uploadImage');
|
||||||
this.loadingService.stopLoading('uploadImage');
|
if (this.type === 'company') {
|
||||||
if (this.type === 'company') {
|
this.user.hasCompanyLogo = true; //
|
||||||
this.user.hasCompanyLogo = true; //
|
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
|
||||||
this.companyLogoUrl = `${this.env.imageBaseUrl}/pictures/logo/${this.user.id}.avif?_ts=${new Date().getTime()}`;
|
} else {
|
||||||
} else {
|
this.user.hasProfile = true;
|
||||||
this.user.hasProfile = true;
|
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}`;
|
||||||
this.profileUrl = `${this.env.imageBaseUrl}/pictures/profile/${this.user.id}.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') {
|
deleteConfirm(type: 'profile' | 'logo') {
|
||||||
@@ -180,10 +184,10 @@ export class AccountComponent {
|
|||||||
accept: async () => {
|
accept: async () => {
|
||||||
if (type === 'profile') {
|
if (type === 'profile') {
|
||||||
this.user.hasProfile = false;
|
this.user.hasProfile = false;
|
||||||
await Promise.all([this.imageService.deleteProfileImagesById(this.user.id), this.userService.save(this.user)]);
|
await Promise.all([this.imageService.deleteProfileImagesById(this.user.email), this.userService.save(this.user)]);
|
||||||
} else {
|
} else {
|
||||||
this.user.hasCompanyLogo = false;
|
this.user.hasCompanyLogo = false;
|
||||||
await Promise.all([this.imageService.deleteLogoImagesById(this.user.id), this.userService.save(this.user)]);
|
await Promise.all([this.imageService.deleteLogoImagesById(this.user.email), this.userService.save(this.user)]);
|
||||||
}
|
}
|
||||||
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
||||||
this.user = await this.userService.getById(this.user.id);
|
this.user = await this.userService.getById(this.user.id);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dy
|
|||||||
import { EditorModule } from 'primeng/editor';
|
import { EditorModule } from 'primeng/editor';
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
||||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { AutoCompleteCompleteEvent, ImageProperty } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
|
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
|
||||||
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
|
||||||
@@ -60,9 +60,7 @@ export class EditBusinessListingComponent {
|
|||||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||||
user: User;
|
user: User;
|
||||||
maxFileSize = 3000000;
|
maxFileSize = 3000000;
|
||||||
uploadUrl: string;
|
|
||||||
environment = environment;
|
environment = environment;
|
||||||
propertyImages: string[];
|
|
||||||
responsiveOptions = [
|
responsiveOptions = [
|
||||||
{
|
{
|
||||||
breakpoint: '1199px',
|
breakpoint: '1199px',
|
||||||
@@ -123,14 +121,14 @@ export class EditBusinessListingComponent {
|
|||||||
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
|
||||||
} else {
|
} else {
|
||||||
this.listing = createDefaultBusinessListing();
|
this.listing = createDefaultBusinessListing();
|
||||||
this.listing.userId = await this.userService.getId(keycloakUser.email);
|
const listingUser = await this.userService.getByMail(keycloakUser.email);
|
||||||
|
this.listing.userId = listingUser.id;
|
||||||
|
this.listing.imageName = emailToDirName(keycloakUser.email);
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.listing.title = this.data?.title;
|
this.listing.title = this.data?.title;
|
||||||
this.listing.description = this.data?.description;
|
this.listing.description = this.data?.description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
|
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
|
|||||||
@@ -82,58 +82,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p-divider></p-divider>
|
<div class="grid">
|
||||||
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<div class="flex-auto p-fluid">
|
<p-inputSwitch inputId="draft" [(ngModel)]="listing.draft"></p-inputSwitch>
|
||||||
<div class="grid">
|
<span class="ml-2 text-900 absolute translate-y-5">Draft Mode (Will not be shown as public listing)</span>
|
||||||
<div class="mb-4 col-12 md:col-6">
|
</div>
|
||||||
<label for="price" class="block font-medium text-900 mb-2">Price</label>
|
</div>
|
||||||
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
|
<div>
|
||||||
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
|
<p-divider></p-divider>
|
||||||
</div>
|
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
||||||
<div class="mb-4 col-12 md:col-6">
|
<div class="flex-auto p-fluid">
|
||||||
<div class="flex flex-column align-items-center flex-or">
|
<div class="grid">
|
||||||
<span class="font-medium text-900 mb-2">Property Pictures</span>
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
|
<label for="price" class="block font-medium text-900 mb-2">Price</label>
|
||||||
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
|
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
|
||||||
<p-fileUpload
|
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
|
||||||
mode="basic"
|
</div>
|
||||||
chooseLabel="Upload"
|
<div class="mb-4 col-12 md:col-6">
|
||||||
[customUpload]="true"
|
<div class="flex flex-column align-items-center flex-or">
|
||||||
name="file"
|
<span class="font-medium text-900 mb-2">Property Pictures</span>
|
||||||
accept="image/*"
|
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
|
||||||
[maxFileSize]="maxFileSize"
|
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
|
||||||
(onSelect)="select($event)"
|
<p-fileUpload
|
||||||
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
|
mode="basic"
|
||||||
[disabled]="!listing.id"
|
chooseLabel="Upload"
|
||||||
>
|
[customUpload]="true"
|
||||||
</p-fileUpload>
|
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>
|
</div>
|
||||||
</div>
|
@if (listing && listing.imageOrder?.length>0){
|
||||||
@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">
|
||||||
<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) {
|
||||||
@for (image of propertyImages; track image) {
|
<span cdkDropList mixedCdkDropList>
|
||||||
<span cdkDropList mixedCdkDropList>
|
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
|
||||||
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
|
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ image }}?_ts={{ ts }}" [alt]="image" class="shadow-2" cdkDrag />
|
||||||
<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>
|
||||||
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
|
</div>
|
||||||
</div>
|
</span>
|
||||||
</span>
|
}
|
||||||
}
|
</div>
|
||||||
</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>
|
||||||
|
@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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p-toast></p-toast>
|
||||||
|
<p-confirmDialog></p-confirmDialog>
|
||||||
</div>
|
</div>
|
||||||
<p-toast></p-toast>
|
|
||||||
<p-confirmDialog></p-confirmDialog>
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
|||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { ListingsService } from '../../../services/listings.service';
|
import { ListingsService } from '../../../services/listings.service';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { SelectOptionsService } from '../../../services/select-options.service';
|
||||||
import { createDefaultCommercialPropertyListing, map2User, routeListingWithState } from '../../../utils/utils';
|
import { createDefaultCommercialPropertyListing, getDialogWidth, getImageDimensions, map2User, routeListingWithState } from '../../../utils/utils';
|
||||||
|
|
||||||
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
|
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||||
import { HttpEventType } from '@angular/common/http';
|
import { HttpEventType } from '@angular/common/http';
|
||||||
@@ -18,9 +18,8 @@ import { DialogModule } from 'primeng/dialog';
|
|||||||
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
|
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
|
||||||
import { EditorModule } from 'primeng/editor';
|
import { EditorModule } from 'primeng/editor';
|
||||||
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { AutoCompleteCompleteEvent, ImageProperty } from '../../../../../../bizmatch-server/src/models/main.model';
|
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
|
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
|
||||||
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
|
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
|
||||||
@@ -63,9 +62,7 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
|
||||||
user: User;
|
user: User;
|
||||||
maxFileSize = 3000000;
|
maxFileSize = 3000000;
|
||||||
uploadUrl: string;
|
|
||||||
environment = environment;
|
environment = environment;
|
||||||
propertyImages: string[];
|
|
||||||
responsiveOptions = [
|
responsiveOptions = [
|
||||||
{
|
{
|
||||||
breakpoint: '1199px',
|
breakpoint: '1199px',
|
||||||
@@ -131,15 +128,14 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
||||||
} else {
|
} else {
|
||||||
this.listing = createDefaultCommercialPropertyListing();
|
this.listing = createDefaultCommercialPropertyListing();
|
||||||
this.listing.userId = await this.userService.getId(keycloakUser.email);
|
const listingUser = await this.userService.getByMail(keycloakUser.email);
|
||||||
this.listing.imagePath = uuidv4();
|
this.listing.userId = listingUser.id;
|
||||||
|
this.listing.imagePath = `${emailToDirName(keycloakUser.email)}`;
|
||||||
if (this.data) {
|
if (this.data) {
|
||||||
this.listing.title = this.data?.title;
|
this.listing.title = this.data?.title;
|
||||||
this.listing.description = this.data?.description;
|
this.listing.description = this.data?.description;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
|
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
@@ -155,40 +151,44 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
|
|
||||||
select(event: any) {
|
select(event: any) {
|
||||||
const imageUrl = URL.createObjectURL(event.files[0]);
|
const imageUrl = URL.createObjectURL(event.files[0]);
|
||||||
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
|
getImageDimensions(imageUrl).then(dimensions => {
|
||||||
data: {
|
const dialogWidth = getDialogWidth(dimensions);
|
||||||
imageUrl: imageUrl,
|
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
|
||||||
fileUpload: this.fileUpload,
|
data: {
|
||||||
ratioVariable: false,
|
imageUrl: imageUrl,
|
||||||
},
|
fileUpload: this.fileUpload,
|
||||||
header: 'Edit Image',
|
ratioVariable: false,
|
||||||
width: '50vw',
|
},
|
||||||
modal: true,
|
header: 'Edit Image',
|
||||||
closeOnEscape: true,
|
width: dialogWidth,
|
||||||
keepInViewport: true,
|
modal: true,
|
||||||
closable: false,
|
closeOnEscape: true,
|
||||||
breakpoints: {
|
keepInViewport: true,
|
||||||
'960px': '75vw',
|
closable: false,
|
||||||
'640px': '90vw',
|
breakpoints: {
|
||||||
},
|
'960px': '75vw',
|
||||||
});
|
'640px': '90vw',
|
||||||
this.dialogRef.onClose.subscribe(cropper => {
|
},
|
||||||
if (cropper) {
|
});
|
||||||
this.loadingService.startLoading('uploadImage');
|
this.dialogRef.onClose.subscribe(cropper => {
|
||||||
cropper.getCroppedCanvas().toBlob(async blob => {
|
if (cropper) {
|
||||||
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.imagePath).subscribe(
|
this.loadingService.startLoading('uploadImage');
|
||||||
async event => {
|
cropper.getCroppedCanvas().toBlob(async blob => {
|
||||||
if (event.type === HttpEventType.Response) {
|
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
|
||||||
console.log('Upload abgeschlossen', event.body);
|
async event => {
|
||||||
this.loadingService.stopLoading('uploadImage');
|
if (event.type === HttpEventType.Response) {
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
this.ts = new Date().getTime();
|
||||||
}
|
console.log('Upload abgeschlossen', event.body);
|
||||||
},
|
this.loadingService.stopLoading('uploadImage');
|
||||||
error => console.error('Fehler beim Upload:', error),
|
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
|
||||||
);
|
}
|
||||||
}, 'image/jpg');
|
},
|
||||||
cropper.destroy();
|
error => console.error('Fehler beim Upload:', error),
|
||||||
}
|
);
|
||||||
|
}, 'image/jpg');
|
||||||
|
cropper.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,9 +205,8 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
|
|
||||||
accept: async () => {
|
accept: async () => {
|
||||||
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
|
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
|
||||||
await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
|
await Promise.all([this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName), this.listingsService.save(this.listing, 'commercialProperty')]);
|
||||||
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
|
||||||
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
|
|
||||||
},
|
},
|
||||||
reject: () => {
|
reject: () => {
|
||||||
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
|
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
|
||||||
@@ -217,8 +216,7 @@ export class EditCommercialPropertyListingComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDrop(event: { previousIndex: number; currentIndex: number }) {
|
onDrop(event: { previousIndex: number; currentIndex: number }) {
|
||||||
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
|
moveItemInArray(this.listing.imageOrder, event.previousIndex, event.currentIndex);
|
||||||
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages);
|
|
||||||
}
|
}
|
||||||
changeListingCategory(value: 'business' | 'commercialProperty') {
|
changeListingCategory(value: 'business' | 'commercialProperty') {
|
||||||
routeListingWithState(this.router, value, this.listing);
|
routeListingWithState(this.router, value, this.listing);
|
||||||
|
|||||||
@@ -1,35 +1,34 @@
|
|||||||
|
|
||||||
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
|
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
|
||||||
<div class="p-fluid flex flex-column lg:flex-row">
|
<div class="p-fluid flex flex-column lg:flex-row">
|
||||||
<menu-account></menu-account>
|
<menu-account></menu-account>
|
||||||
<div class="surface-card p-5 shadow-2 border-round flex-auto">
|
<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>
|
<div class="text-900 font-semibold text-lg mt-3">Contact Us</div>
|
||||||
<p-divider></p-divider>
|
<p-divider></p-divider>
|
||||||
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
<div class="flex gap-5 flex-column-reverse md:flex-row">
|
||||||
<div class="flex-auto p-fluid">
|
<div class="flex-auto p-fluid formgrid">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="mb-4 col-12 md:col-6">
|
<div class="mb-4 col-12 md:col-6">
|
||||||
<label for="name" class="block font-medium text-900 mb-2">Your name</label>
|
<label for="name" class="block font-medium text-900 mb-2">Your name</label>
|
||||||
<input id="name" type="text" pInputText>
|
<input [ngClass]="{ 'ng-invalid': containsError('name'), 'ng-dirty': containsError('name') }" name="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
|
||||||
</div>
|
|
||||||
<div 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>
|
</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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,28 +1,50 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import { CheckboxModule } from 'primeng/checkbox';
|
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { MessageService } from 'primeng/api';
|
||||||
import { StyleClassModule } from 'primeng/styleclass';
|
import { InputMaskModule } from 'primeng/inputmask';
|
||||||
import { SelectOptionsService } from '../../../services/select-options.service';
|
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { DropdownModule } from 'primeng/dropdown';
|
import { ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { CommonModule } from '@angular/common';
|
import { MailService } from '../../../services/mail.service';
|
||||||
import { ToggleButtonModule } from 'primeng/togglebutton';
|
import { UserService } from '../../../services/user.service';
|
||||||
import { TagModule } from 'primeng/tag';
|
import { SharedModule } from '../../../shared/shared/shared.module';
|
||||||
import data from '../../../../assets/data/user.json';
|
import { map2User } from '../../../utils/utils';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { InputTextareaModule } from 'primeng/inputtextarea';
|
|
||||||
import { ChipModule } from 'primeng/chip';
|
|
||||||
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
|
|
||||||
import { DividerModule } from 'primeng/divider';
|
|
||||||
import { TableModule } from 'primeng/table';
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-email-us',
|
selector: 'app-email-us',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, StyleClassModule, MenuAccountComponent, DividerModule,ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, ChipModule,InputTextareaModule],
|
imports: [SharedModule, InputMaskModule],
|
||||||
|
providers: [MessageService],
|
||||||
templateUrl: './email-us.component.html',
|
templateUrl: './email-us.component.html',
|
||||||
styleUrl: './email-us.component.scss'
|
styleUrl: './email-us.component.scss',
|
||||||
})
|
})
|
||||||
export class EmailUsComponent {
|
export class EmailUsComponent {
|
||||||
|
mailinfo: MailInfo;
|
||||||
|
keycloakUser: KeycloakUser;
|
||||||
|
user: User;
|
||||||
|
errorResponse: ErrorResponse;
|
||||||
|
constructor(private mailService: MailService, private userService: UserService, public keycloakService: KeycloakService, private messageService: MessageService) {
|
||||||
|
this.mailinfo = { sender: {}, userId: '', email: '', url: environment.mailinfoUrl };
|
||||||
|
}
|
||||||
|
async ngOnInit() {
|
||||||
|
const token = await this.keycloakService.getToken();
|
||||||
|
this.keycloakUser = map2User(token);
|
||||||
|
if (this.keycloakUser) {
|
||||||
|
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
||||||
|
this.mailinfo.sender = { name: `${this.user.firstname} ${this.user.lastname}`, email: this.user.email, phoneNumber: this.user.phoneNumber, state: this.user.companyLocation };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async mail() {
|
||||||
|
this.mailinfo.email = 'support@bizmatch.net';
|
||||||
|
const result = await this.mailService.mail(this.mailinfo);
|
||||||
|
if (result) {
|
||||||
|
this.errorResponse = result as ErrorResponse;
|
||||||
|
} else {
|
||||||
|
this.errorResponse = null;
|
||||||
|
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Your request has been forwarded to the support team of bizmatch.', life: 3000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containsError(fieldname: string) {
|
||||||
|
return this.errorResponse?.fields.map(f => f.fieldname).includes(fieldname);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class MyListingComponent {
|
|||||||
const keycloakUser = map2User(token);
|
const keycloakUser = map2User(token);
|
||||||
const email = keycloakUser.email;
|
const email = keycloakUser.email;
|
||||||
this.user = await this.userService.getByMail(email);
|
this.user = await this.userService.getByMail(email);
|
||||||
const result = await Promise.all([await this.listingsService.getListingByUserId(this.user.id, 'business'), await this.listingsService.getListingByUserId(this.user.id, 'commercialProperty')]);
|
const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
||||||
this.myListings = [...result[0], ...result[1]];
|
this.myListings = [...result[0], ...result[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export class MyListingComponent {
|
|||||||
} else {
|
} else {
|
||||||
await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath);
|
await this.listingsService.deleteCommercialPropertyListing(listing.id, (<CommercialPropertyListing>listing).imagePath);
|
||||||
}
|
}
|
||||||
const result = await Promise.all([await this.listingsService.getListingByUserId(this.user.id, 'business'), await this.listingsService.getListingByUserId(this.user.id, 'commercialProperty')]);
|
const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
|
||||||
this.myListings = [...result[0], ...result[1]];
|
this.myListings = [...result[0], ...result[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { ImageType } from '../../../../bizmatch-server/src/models/main.model';
|
import { emailToDirName } from '../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -12,33 +12,29 @@ export class ImageService {
|
|||||||
|
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string) {
|
uploadImage(imageBlob: Blob, type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile', imagePath: string, serialId?: number) {
|
||||||
const uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
|
let uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}`;
|
||||||
|
if (type === 'uploadPropertyPicture') {
|
||||||
|
uploadUrl = `${this.apiBaseUrl}/bizmatch/image/${type}/${imagePath}/${serialId}`;
|
||||||
|
}
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', imageBlob, 'image.png');
|
formData.append('file', imageBlob, 'image.png');
|
||||||
|
|
||||||
return this.http.post(uploadUrl, formData, {
|
return this.http.post(uploadUrl, formData, {
|
||||||
// headers: this.headers,
|
|
||||||
//reportProgress: true,
|
|
||||||
observe: 'events',
|
observe: 'events',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async deleteUserImage(userid: string, type: ImageType, name?: string) {
|
|
||||||
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/${type.delete}${userid}`));
|
async deleteListingImage(imagePath: string, serial: number, name?: string) {
|
||||||
|
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/propertyPicture/${imagePath}/${serial}/${name}`));
|
||||||
}
|
}
|
||||||
async deleteListingImage(imagePath: string, name?: string) {
|
|
||||||
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/propertyPicture/${imagePath}/${name}`));
|
async deleteLogoImagesById(email: string) {
|
||||||
|
const adjustedEmail = emailToDirName(email);
|
||||||
|
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/logo/${adjustedEmail}`));
|
||||||
}
|
}
|
||||||
async getProfileImagesForUsers(userids: string[]) {
|
async deleteProfileImagesById(email: string) {
|
||||||
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/profileImages/${userids.join(',')}`));
|
const adjustedEmail = emailToDirName(email);
|
||||||
}
|
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/profile/${adjustedEmail}`));
|
||||||
async getCompanyLogosForUsers(userids: string[]) {
|
|
||||||
return await lastValueFrom(this.http.get<[]>(`${this.apiBaseUrl}/bizmatch/image/companyLogos/${userids.join(',')}`));
|
|
||||||
}
|
|
||||||
async deleteLogoImagesById(userid: string) {
|
|
||||||
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/logo/${userid}`));
|
|
||||||
}
|
|
||||||
async deleteProfileImagesById(userid: string) {
|
|
||||||
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/profile/${userid}`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export class ListingsService {
|
|||||||
const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`);
|
const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
getListingByUserId(userid: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
getListingsByEmail(email: string, listingsCategory: 'business' | 'commercialProperty'): Promise<ListingType[]> {
|
||||||
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${userid}`));
|
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/user/${email}`));
|
||||||
}
|
}
|
||||||
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
|
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
|
||||||
if (listing.id) {
|
if (listing.id) {
|
||||||
@@ -40,11 +40,4 @@ export class ListingsService {
|
|||||||
async deleteCommercialPropertyListing(id: string, imagePath: string) {
|
async deleteCommercialPropertyListing(id: string, imagePath: string) {
|
||||||
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/${id}/${imagePath}`));
|
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/${id}/${imagePath}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPropertyImages(id: string): Promise<string[]> {
|
|
||||||
return await lastValueFrom(this.http.get<string[]>(`${this.apiBaseUrl}/bizmatch/image/${id}`));
|
|
||||||
}
|
|
||||||
async changeImageOrder(id: string, propertyImages: string[]): Promise<string[]> {
|
|
||||||
return await lastValueFrom(this.http.put<string[]>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/imageOrder/${id}`, propertyImages));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { InitEditableRow } from 'primeng/table';
|
import { Injectable } from '@angular/core';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { environment } from '../../environments/environment';
|
|
||||||
import { KeyValue, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model';
|
import { KeyValue, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -13,15 +12,14 @@ export class SelectOptionsService {
|
|||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const allSelectOptions = await lastValueFrom(
|
const allSelectOptions = await lastValueFrom(this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`));
|
||||||
this.http.get<any>(`${this.apiBaseUrl}/bizmatch/select-options`)
|
|
||||||
);
|
|
||||||
this.typesOfBusiness = allSelectOptions.typesOfBusiness;
|
this.typesOfBusiness = allSelectOptions.typesOfBusiness;
|
||||||
this.prices = allSelectOptions.prices;
|
this.prices = allSelectOptions.prices;
|
||||||
this.listingCategories = allSelectOptions.listingCategories;
|
this.listingCategories = allSelectOptions.listingCategories;
|
||||||
this.categories = allSelectOptions.categories;
|
this.customerTypes = allSelectOptions.customerTypes;
|
||||||
this.states = allSelectOptions.locations;
|
this.states = allSelectOptions.locations;
|
||||||
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty
|
this.gender = allSelectOptions.gender;
|
||||||
|
this.typesOfCommercialProperty = allSelectOptions.typesOfCommercialProperty;
|
||||||
}
|
}
|
||||||
public typesOfBusiness: Array<KeyValueStyle>;
|
public typesOfBusiness: Array<KeyValueStyle>;
|
||||||
|
|
||||||
@@ -31,49 +29,41 @@ export class SelectOptionsService {
|
|||||||
|
|
||||||
public listingCategories: Array<KeyValue>;
|
public listingCategories: Array<KeyValue>;
|
||||||
|
|
||||||
public categories: Array<KeyValueStyle>;
|
public customerTypes: Array<KeyValue>;
|
||||||
|
|
||||||
|
public gender: Array<KeyValue>;
|
||||||
|
|
||||||
public states: Array<any>;
|
public states: Array<any>;
|
||||||
|
|
||||||
getState(value:string):string{
|
getState(value: string): string {
|
||||||
return this.states.find(l=>l.value===value)?.name
|
return this.states.find(l => l.value === value)?.name;
|
||||||
}
|
}
|
||||||
getBusiness(value:number):string{
|
getBusiness(value: number): string {
|
||||||
return this.typesOfBusiness.find(t=>t.value===String(value))?.name
|
return this.typesOfBusiness.find(t => t.value === String(value))?.name;
|
||||||
}
|
}
|
||||||
getCommercialProperty(value:number):string{
|
getCommercialProperty(value: number): string {
|
||||||
return this.typesOfCommercialProperty.find(t=>t.value===String(value))?.name
|
return this.typesOfCommercialProperty.find(t => t.value === String(value))?.name;
|
||||||
}
|
}
|
||||||
getListingsCategory(value:string):string{
|
getListingsCategory(value: string): string {
|
||||||
return this.listingCategories.find(l=>l.value===value)?.name
|
return this.listingCategories.find(l => l.value === value)?.name;
|
||||||
}
|
}
|
||||||
getCategory(value:string):string{
|
getCustomerType(value: string): string {
|
||||||
return this.categories.find(c=>c.value===value)?.name
|
return this.customerTypes.find(c => c.value === value)?.name;
|
||||||
}
|
}
|
||||||
getIcon(value:string):string{
|
getGender(value: string): string {
|
||||||
return this.categories.find(c=>c.value===value)?.icon
|
return this.gender.find(c => c.value === value)?.name;
|
||||||
}
|
}
|
||||||
getTextColor(value:string):string{
|
getIconType(value: string): string {
|
||||||
return this.categories.find(c=>c.value===value)?.textColorClass
|
return this.typesOfBusiness.find(c => c.value === value)?.icon;
|
||||||
}
|
}
|
||||||
getBgColor(value:string):string{
|
getTextColorType(value: string): string {
|
||||||
return this.categories.find(c=>c.value===value)?.bgColorClass
|
return this.typesOfBusiness.find(c => c.value === value)?.textColorClass;
|
||||||
}
|
}
|
||||||
getIconAndTextColor(value:string):string{
|
getBgColorType(value: number): string {
|
||||||
const category = this.categories.find(c=>c.value===value)
|
return this.typesOfBusiness.find(c => c.value === String(value))?.bgColorClass;
|
||||||
return `${category?.icon} ${category?.textColorClass}`
|
|
||||||
}
|
}
|
||||||
getIconType(value:string):string{
|
getIconAndTextColorType(value: number): string {
|
||||||
return this.typesOfBusiness.find(c=>c.value===value)?.icon
|
const category = this.typesOfBusiness.find(c => c.value === String(value));
|
||||||
}
|
return `${category?.icon} ${category?.textColorClass}`;
|
||||||
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}`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,13 +33,13 @@ export class UserService {
|
|||||||
async getAllStates(): Promise<any> {
|
async getAllStates(): Promise<any> {
|
||||||
return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
|
return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
|
||||||
}
|
}
|
||||||
async getId(email: string): Promise<string> {
|
// async getId(email: string): Promise<string> {
|
||||||
if (sessionStorage.getItem('USERID')) {
|
// if (sessionStorage.getItem('USERID')) {
|
||||||
return sessionStorage.getItem('USERID');
|
// return sessionStorage.getItem('USERID');
|
||||||
} else {
|
// } else {
|
||||||
const user = await this.getByMail(email);
|
// const user = await this.getByMail(email);
|
||||||
sessionStorage.setItem('USERID', user.id);
|
// sessionStorage.setItem('USERID', user.id);
|
||||||
return user.id;
|
// return user.id;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,14 @@ export function createLogger(name: string, level: number = INFO, options: any =
|
|||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
export function formatPhoneNumber(phone: string): string {
|
||||||
|
const cleaned = ('' + phone).replace(/\D/g, '');
|
||||||
|
const match = cleaned.match(/^(\d{3})(\d{3})(\d{4})$/);
|
||||||
|
if (match) {
|
||||||
|
return '(' + match[1] + ') ' + match[2] + '-' + match[3];
|
||||||
|
}
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
export const getSessionStorageHandler = function (path, value, previous, applyData) {
|
export const getSessionStorageHandler = function (path, value, previous, applyData) {
|
||||||
sessionStorage.setItem('criteria', JSON.stringify(this));
|
sessionStorage.setItem('criteria', JSON.stringify(this));
|
||||||
};
|
};
|
||||||
@@ -130,3 +137,25 @@ export function map2User(jwt: string): KeycloakUser {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export function getImageDimensions(imageUrl: string): Promise<{ width: number; height: number }> {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
resolve({ width: img.width, height: img.height });
|
||||||
|
};
|
||||||
|
img.src = imageUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDialogWidth(dimensions): string {
|
||||||
|
const aspectRatio = dimensions.width / dimensions.height;
|
||||||
|
let dialogWidth = '50vw'; // Standardbreite
|
||||||
|
|
||||||
|
// Passen Sie die Breite basierend auf dem Seitenverhältnis an
|
||||||
|
if (aspectRatio < 1) {
|
||||||
|
dialogWidth = '30vw'; // Hochformat
|
||||||
|
} else if (aspectRatio > 1) {
|
||||||
|
dialogWidth = '50vw'; // Querformat
|
||||||
|
}
|
||||||
|
return dialogWidth;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user