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