Compare commits

...

11 Commits

126 changed files with 47861 additions and 4279 deletions

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@ -0,0 +1,45 @@
{
"env": {
"es2021": true,
"browser": true
},
"extends": [
"airbnb-base",
"airbnb-typescript",
"plugin:@typescript-eslint/recommended",
"eslint-config-prettier",
"plugin:cypress/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module",
"project": ["./tsconfig.json"]
},
"plugins": ["@typescript-eslint"],
"rules": {
"import/no-unresolved": ["off"],
"import/prefer-default-export": ["off"],
"no-useless-constructor": "off",
"@typescript-eslint/no-useless-constructor": ["error"],
"@typescript-eslint/lines-between-class-members": ["off"],
"no-param-reassign": ["off"],
"max-classes-per-file": ["off"],
"no-shadow": ["off"],
"class-methods-use-this": ["off"],
"react/jsx-filename-extension": ["off"],
"import/no-cycle": ["off"],
"radix": ["off"],
"no-promise-executor-return": ["off"],
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "enumMember",
"format": ["UPPER_CASE", "PascalCase"]
}
],
"no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
"spaced-comment": ["off"],
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
}
}

View File

@ -1,4 +1,18 @@
{ {
"arrowParens": "avoid",
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 220,
"proseWrap": "always",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true, "singleQuote": true,
"trailingComma": "all" "tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false
} }

View File

@ -17,6 +17,49 @@
"sourceMaps": true, "sourceMaps": true,
"stopOnEntry": false, "stopOnEntry": false,
"console": "integratedTerminal", "console": "integratedTerminal",
} },
{
"type": "node",
"request": "launch",
"name": "Debug Current TS File",
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"sourceMaps": true,
"smartStep": true,
"internalConsoleOptions": "openOnSessionStart"
},
{
"type": "node",
"request": "launch",
"name": "generateDefs",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
"outFiles": [
"${workspaceFolder}/dist/src/drizzle/**/*.js"
],
"sourceMaps": true,
"smartStep": true,
},
{
"type": "node",
"request": "launch",
"name": "generateTypes",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
"outFiles": [
"${workspaceFolder}/dist/src/drizzle/**/*.js"
],
"sourceMaps": true,
"smartStep": true,
},
] ]
} }

28
bizmatch-server/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,28 @@
{
"editor.suggestSelection": "first",
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
"explorer.confirmDelete": false,
"typescript.updateImportsOnFileMove.enabled": "always",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"prettier.printWidth": 240,
"git.autofetch": false,
"git.autorefresh": true
}

View File

@ -1 +0,0 @@
FT.CREATE listingsIndex ON JSON PREFIX 1 listings: SCHEMA $.location AS location TAG SORTABLE $.price AS price NUMERIC SORTABLE $.listingsCategory AS listingsCategory TAG SORTABLE $.type AS type TAG SORTABLE

1326
bizmatch-server/broker.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: "./src/drizzle/schema.ts",
out: "./src/drizzle/migrations",
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL,
},
verbose: true,
strict: true,
})

View File

@ -4,7 +4,10 @@
"sourceRoot": "src", "sourceRoot": "src",
"compilerOptions": { "compilerOptions": {
"deleteOutDir": true, "deleteOutDir": true,
"assets": ["assets/**/*","**/*.hbs"], "assets": [
"assets/**/*",
"**/*.hbs"
],
"watchAssets": true "watchAssets": true
} }
} }

View File

@ -18,7 +18,12 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json",
"generate": "drizzle-kit generate:pg",
"drop": "drizzle-kit drop",
"migrate": "tsx src/drizzle/migrate.ts",
"import": "tsx src/drizzle/import.ts",
"generateTypes": "tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts"
}, },
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^1.10.3", "@nestjs-modules/mailer": "^1.10.3",
@ -29,6 +34,9 @@
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@nestjs/serve-static": "^4.0.1", "@nestjs/serve-static": "^4.0.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.30.8",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"ky": "^1.2.0", "ky": "^1.2.0",
"nest-winston": "^1.9.4", "nest-winston": "^1.9.4",
@ -38,15 +46,19 @@
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pg": "^8.11.5",
"redis": "^4.6.13", "redis": "^4.6.13",
"redis-om": "^0.4.3", "redis-om": "^0.4.3",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"sharp": "^0.33.2", "sharp": "^0.33.2",
"tsx": "^4.7.2",
"urlcat": "^3.1.0", "urlcat": "^3.1.0",
"winston": "^3.11.0" "winston": "^3.11.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/parser": "^7.24.4",
"@babel/traverse": "^7.24.1",
"@nestjs/cli": "^10.0.0", "@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0", "@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0", "@nestjs/testing": "^10.0.0",
@ -58,14 +70,20 @@
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/pg": "^8.11.5",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",
"commander": "^12.0.0",
"drizzle-kit": "^0.20.16",
"eslint": "^8.42.0", "eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0", "jest": "^29.5.0",
"kysely-codegen": "^0.15.0",
"pg-to-ts": "^4.1.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"rimraf": "^5.0.5",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",

View File

@ -1,36 +1,30 @@
import { Module } from '@nestjs/common'; import { MiddlewareConsumer, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
import path from 'path';
import { fileURLToPath } from 'url';
import * as winston from 'winston';
import { AppController } from './app.controller.js'; import { AppController } from './app.controller.js';
import { AppService } from './app.service.js'; import { AppService } from './app.service.js';
import { FileService } from './file/file.service.js';
import { AuthService } from './auth/auth.service.js';
import { AuthController } from './auth/auth.controller.js';
import { ConfigModule } from '@nestjs/config';
import { SelectOptionsController } from './select-options/select-options.controller.js';
import { SelectOptionsService } from './select-options/select-options.service.js';
import { SubscriptionsController } from './subscriptions/subscriptions.controller.js';
import { RedisModule } from './redis/redis.module.js';
import { ListingsService } from './listings/listings.service.js';
import { ServeStaticModule } from '@nestjs/serve-static';
import path, { join } from 'path';
import { fileURLToPath } from 'url';
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
import * as winston from 'winston';
import { MailModule } from './mail/mail.module.js';
import { AuthModule } from './auth/auth.module.js'; import { AuthModule } from './auth/auth.module.js';
import { FileService } from './file/file.service.js';
import { GeoModule } from './geo/geo.module.js'; import { GeoModule } from './geo/geo.module.js';
import { UserModule } from './user/user.module.js';
import { ListingsModule } from './listings/listings.module.js';
import { SelectOptionsModule } from './select-options/select-options.module.js';
import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js';
import { ImageModule } from './image/image.module.js'; import { ImageModule } from './image/image.module.js';
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 { SubscriptionsController } from './subscriptions/subscriptions.controller.js';
import { UserModule } from './user/user.module.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@Module({ @Module({
imports: [ConfigModule.forRoot(), MailModule, AuthModule, imports: [
ServeStaticModule.forRoot({ ConfigModule.forRoot({ isGlobal: true }),
rootPath: join(__dirname, '..', 'pictures'), // `public` ist das Verzeichnis, wo Ihre statischen Dateien liegen MailModule,
}), AuthModule,
WinstonModule.forRoot({ WinstonModule.forRoot({
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
@ -51,10 +45,13 @@ const __dirname = path.dirname(__filename);
UserModule, UserModule,
ListingsModule, ListingsModule,
SelectOptionsModule, SelectOptionsModule,
RedisModule, ImageModule,
ImageModule
], ],
controllers: [AppController, SubscriptionsController], controllers: [AppController, SubscriptionsController],
providers: [AppService, FileService], providers: [AppService, FileService],
}) })
export class AppModule {} export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestDurationMiddleware).forRoutes('*');
}
}

View File

@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { drizzle } from 'drizzle-orm/node-postgres';
import pkg from 'pg';
const { Pool } = pkg;
import * as schema from './schema.js';
import { ConfigService } from '@nestjs/config';
import { jsonb, varchar } from 'drizzle-orm/pg-core';
import { PG_CONNECTION } from './schema.js';
@Module({
providers: [
{
provide: PG_CONNECTION,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const connectionString = configService.get<string>('DATABASE_URL');
const pool = new Pool({
connectionString,
// ssl: true,
});
return drizzle(pool, { schema, logger:true });
},
},
],
exports: [PG_CONNECTION],
})
export class DrizzleModule {}

View File

@ -0,0 +1,143 @@
import fs from 'fs';
import ts from 'typescript';
export const TABLE_BO_MAPPING = {
'users':'User',
'businesses':'BusinessListing',
'commercials':'CommercialPropertyListing'
}
function generateInterfaceDefinitions(inputFile: string, outputFile: string): void {
const sourceFile = ts.createSourceFile(
inputFile,
fs.readFileSync(inputFile, 'utf8'),
ts.ScriptTarget.Latest,
true
);
let interfaceDefinitions = '';
ts.forEachChild(sourceFile, (node) => {
if (ts.isVariableStatement(node)) {
const variableDeclaration = node.declarationList.declarations[0];
if (variableDeclaration.initializer && ts.isCallExpression(variableDeclaration.initializer)) {
const initializer = variableDeclaration.initializer;
if (initializer.expression.getText(sourceFile) === 'pgTable') {
const tableName = initializer.arguments[0].getText(sourceFile).slice(1, -1); // Entfernen von zusätzlichen Anführungszeichen
const tableDefinition = initializer.arguments[1];
const interfaceName = TABLE_BO_MAPPING[tableName] ? TABLE_BO_MAPPING[tableName] : tableName.charAt(0).toUpperCase() + tableName.slice(1);
let interfaceDefinition = `export interface ${interfaceName} {\n`;
ts.forEachChild(tableDefinition, (propertyNode) => {
if (ts.isPropertyAssignment(propertyNode)) {
const propertyName = propertyNode.name.getText(sourceFile);
const propertyDefinition = propertyNode.initializer;
if (ts.isCallExpression(propertyDefinition)) {
const propertyType = getPropertyType(propertyDefinition, sourceFile);
const isOptional = !hasNonOptionalModifier(propertyDefinition.expression);
interfaceDefinition += ` ${propertyName}${isOptional ? '?' : ''}: ${propertyType};\n`;
}
}
});
interfaceDefinition += '}\n\n';
interfaceDefinitions += interfaceDefinition;
}
}
}
});
fs.writeFileSync(outputFile, interfaceDefinitions);
console.log(`Interface definitions generated successfully. Output file: ${outputFile}`);
}
function getPropertyType(propertyDefinition: ts.CallExpression, sourceFile: ts.SourceFile): string {
const typeFunction = getTypeFunctionName(propertyDefinition.expression, sourceFile);
let propertyType = '';
switch (typeFunction) {
case 'varchar':
case 'char':
case 'text':
case 'uuid':
propertyType = 'string';
break;
case 'integer':
case 'numeric':
case 'real':
case 'doublePrecision':
propertyType = 'number';
break;
case 'boolean':
propertyType = 'boolean';
break;
case 'timestamp':
propertyType = 'Date';
break;
default:
propertyType = 'any';
}
const isArray = hasArrayModifier(propertyDefinition.expression);
return isArray ? `${propertyType}[]` : propertyType;
}
function getTypeFunctionName(expression: ts.Expression, sourceFile: ts.SourceFile): string {
// Prüfen, ob die aktuelle Expression ein Identifier ist (SyntaxKind.Identifier hat den Wert 80)
if (expression.kind === ts.SyntaxKind.Identifier) {
return expression.getText(sourceFile);
}
// Wenn die Expression eine CallExpression oder eine PropertyAccessExpression ist,
// gehe zur nächsten Expression-Ebene
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
return getTypeFunctionName(expression.expression, sourceFile);
}
// Falls ein nicht unterstützter Expression-Typ vorliegt, gibt 'unknown' zurück
return 'unknown';
}
function hasArrayModifier(expression: ts.Expression): boolean {
// Prüfe, ob die aktuelle Expression eine CallExpression ist und der Funktionsname 'array' ist
if (ts.isPropertyAccessExpression(expression) && expression.name.getText() === 'array') {
return true;
}
// Wenn die Expression eine weitere CallExpression oder PropertyAccessExpression ist,
// prüfe rekursiv die nächste Ebene
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
return hasArrayModifier(expression.expression);
}
// Wenn keine weitere Ebene oder kein Array-Modifier gefunden wurde, gib false zurück
return false;
}
function hasNonOptionalModifier(expression: ts.Expression): boolean {
// Prüfe, ob die aktuelle Expression eine CallExpression ist und der Funktionsname 'notNull' ist
if (ts.isPropertyAccessExpression(expression) && (expression.name.getText() === 'notNull' || expression.name.getText() === 'primaryKey')) {
return true;
}
// Wenn die Expression eine weitere CallExpression oder PropertyAccessExpression ist,
// prüfe rekursiv die nächste Ebene
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
return hasNonOptionalModifier(expression.expression);
}
// Wenn keine weitere Ebene oder kein NotNull-Modifier gefunden wurde, gib false zurück
return false;
}
// Hauptprogramm
const inputFile = process.argv[2]|| './src/drizzle/schema.ts';
const outputFile = process.argv[3] || './model.ts';
if (!inputFile) {
console.error('Please provide an input file.');
process.exit(1);
}
generateInterfaceDefinitions(inputFile, outputFile);

View File

@ -0,0 +1,153 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
import { join } from 'path';
import pkg from 'pg';
import { rimraf } from 'rimraf';
import sharp from 'sharp';
import { BusinessListing, CommercialPropertyListing, User } from 'src/models/db.model.js';
import * as schema from './schema.js';
const { Pool } = pkg;
const connectionString = process.env.DATABASE_URL;
// const pool = new Pool({connectionString})
const client = new Pool({ connectionString });
const db = drizzle(client, { schema, logger: true });
//Delete Content
await db.delete(schema.commercials);
await db.delete(schema.businesses);
await db.delete(schema.users);
//Broker
let filePath = `./data/broker.json`;
let data: string = readFileSync(filePath, 'utf8');
const userData: User[] = JSON.parse(data); // Erwartet ein Array von Objekten
const generatedUserData = [];
console.log(userData.length);
let i = 0,
male = 0,
female = 0;
const targetPathProfile = `./pictures/profile`;
deleteFilesOfDir(targetPathProfile);
const targetPathLogo = `./pictures/logo`;
deleteFilesOfDir(targetPathLogo);
for (const user of userData) {
delete user.id;
user.licensedIn = user.licensedIn.map(l => `${l['name']}|${l['value']}`);
user.hasCompanyLogo = true;
user.hasProfile = true;
const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender });
generatedUserData.push(u[0].insertedId);
i++;
if (u[0].gender === 'male') {
male++;
const data = readFileSync(`./pictures/profile_base/Mann_${male}.jpg`);
await storeProfilePicture(data, u[0].insertedId);
} else {
female++;
const data = readFileSync(`./pictures/profile_base/Frau_${male}.jpg`);
await storeProfilePicture(data, u[0].insertedId);
}
const data = readFileSync(`./pictures/logos_base/${i}.jpg`);
await storeCompanyLogo(data, u[0].insertedId);
}
//Business Listings
filePath = `./data/businesses.json`;
data = readFileSync(filePath, 'utf8');
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
for (const business of businessJsonData) {
delete business.id;
business.created = new Date(business.created);
business.userId = getRandomItem(generatedUserData);
await db.insert(schema.businesses).values(business);
}
//Corporate Listings
filePath = `./data/commercials.json`;
data = readFileSync(filePath, 'utf8');
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
for (const commercial of commercialJsonData) {
const id = commercial.id;
delete commercial.id;
commercial.imageOrder = getFilenames(id);
commercial.imagePath = id;
commercial.created = getRandomDateWithinLastYear();
commercial.userId = getRandomItem(generatedUserData);
await db.insert(schema.commercials).values(commercial);
}
//End
await client.end();
function getRandomItem<T>(arr: T[]): T {
if (arr.length === 0) {
throw new Error('The array is empty.');
}
const randomIndex = Math.floor(Math.random() * arr.length);
return arr[randomIndex];
}
function getFilenames(id: string): string[] {
try {
let filePath = `./pictures/property/${id}`;
return readdirSync(filePath);
} catch (e) {
return null;
}
}
function getRandomDateWithinLastYear(): Date {
const currentDate = new Date();
const lastYear = new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), currentDate.getDate());
const timeDiff = currentDate.getTime() - lastYear.getTime();
const randomTimeDiff = Math.random() * timeDiff;
const randomDate = new Date(lastYear.getTime() + randomTimeDiff);
return randomDate;
}
async function storeProfilePicture(buffer: Buffer, userId: 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/profile/${userId}.avif`);
}
async function storeCompanyLogo(buffer: Buffer, userId: 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 fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
}
function deleteFilesOfDir(directoryPath) {
// Überprüfen, ob das Verzeichnis existiert
if (existsSync(directoryPath)) {
// Den Inhalt des Verzeichnisses synchron löschen
try {
readdirSync(directoryPath).forEach(file => {
const filePath = join(directoryPath, file);
// Wenn es sich um ein Verzeichnis handelt, rekursiv löschen
if (statSync(filePath).isDirectory()) {
rimraf.sync(filePath);
} else {
// Wenn es sich um eine Datei handelt, direkt löschen
unlinkSync(filePath);
}
});
console.log('Der Inhalt des Verzeichnisses wurde erfolgreich gelöscht.');
} catch (err) {
console.error('Fehler beim Löschen des Verzeichnisses:', err);
}
} else {
console.log('Das Verzeichnis existiert nicht.');
}
}

View File

@ -0,0 +1,13 @@
import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import pkg from 'pg';
const { Pool } = pkg;
import * as schema from './schema.js';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
const connectionString = process.env.DATABASE_URL
const pool = new Pool({connectionString})
const db = drizzle(pool, { schema });
// This will run migrations on the database, skipping the ones already applied
await migrate(db, { migrationsFolder: './src/drizzle/migrations' });
// Don't forget to close the connection, otherwise the script will hang
await pool.end();

View File

@ -0,0 +1,84 @@
CREATE TABLE IF NOT EXISTS "businesses" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"userId" uuid,
"type" integer,
"title" varchar(255),
"description" text,
"city" varchar(255),
"state" char(2),
"price" double precision,
"favoritesForUser" varchar(30)[],
"draft" boolean,
"listingsCategory" varchar(255),
"realEstateIncluded" boolean,
"leasedLocation" boolean,
"franchiseResale" boolean,
"salesRevenue" double precision,
"cashFlow" double precision,
"supportAndTraining" text,
"employees" integer,
"established" integer,
"internalListingNumber" integer,
"reasonForSale" varchar(255),
"brokerLicencing" varchar(255),
"internals" text,
"created" timestamp,
"updated" timestamp,
"visits" integer,
"lastVisit" timestamp
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "commercials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"userId" uuid,
"type" integer,
"title" varchar(255),
"description" text,
"city" varchar(255),
"state" char(2),
"price" double precision,
"favoritesForUser" varchar(30)[],
"hideImage" boolean,
"draft" boolean,
"zipCode" integer,
"county" varchar(255),
"email" varchar(255),
"website" varchar(255),
"phoneNumber" varchar(255),
"imageOrder" varchar(30)[],
"imagePath" varchar(50),
"created" timestamp,
"updated" timestamp,
"visits" integer,
"lastVisit" timestamp
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"firstname" varchar(255) NOT NULL,
"lastname" varchar(255) NOT NULL,
"email" varchar(255) NOT NULL,
"phoneNumber" varchar(255),
"description" text,
"companyName" varchar(255),
"companyOverview" text,
"companyWebsite" varchar(255),
"companyLocation" varchar(255),
"offeredServices" text,
"areasServed" varchar(100)[],
"hasProfile" boolean,
"hasCompanyLogo" boolean,
"licensedIn" varchar(50)[]
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "businesses" ADD CONSTRAINT "businesses_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "commercials" ADD CONSTRAINT "commercials_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,460 @@
{
"id": "f6d421f9-2394-4a1c-9268-9e46285f0a41",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "5",
"dialect": "pg",
"tables": {
"businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_userId_users_id_fk": {
"name": "businesses_userId_users_id_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"website": {
"name": "website",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_userId_users_id_fk": {
"name": "commercials_userId_users_id_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "varchar(100)[]",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "varchar(50)[]",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,460 @@
{
"id": "3e4b8c5f-4474-4877-abec-38283408ee34",
"prevId": "f6d421f9-2394-4a1c-9268-9e46285f0a41",
"version": "5",
"dialect": "pg",
"tables": {
"businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_userId_users_id_fk": {
"name": "businesses_userId_users_id_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"website": {
"name": "website",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_userId_users_id_fk": {
"name": "commercials_userId_users_id_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "varchar(100)[]",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "varchar(50)[]",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,474 @@
{
"id": "ad48c6eb-2d04-442f-9242-b6765553c7c4",
"prevId": "3e4b8c5f-4474-4877-abec-38283408ee34",
"version": "5",
"dialect": "pg",
"tables": {
"businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_userId_users_id_fk": {
"name": "businesses_userId_users_id_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"website": {
"name": "website",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_userId_users_id_fk": {
"name": "commercials_userId_users_id_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "varchar(100)[]",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "varchar(50)[]",
"primaryKey": false,
"notNull": false
},
"gender": {
"name": "gender",
"type": "gender",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"gender": {
"name": "gender",
"values": {
"male": "male",
"female": "female"
}
}
},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,480 @@
{
"id": "da786c6a-fd5f-4629-bd5e-3ecd42ab1f2c",
"prevId": "ad48c6eb-2d04-442f-9242-b6765553c7c4",
"version": "5",
"dialect": "pg",
"tables": {
"businesses": {
"name": "businesses",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"realEstateIncluded": {
"name": "realEstateIncluded",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"leasedLocation": {
"name": "leasedLocation",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"franchiseResale": {
"name": "franchiseResale",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"salesRevenue": {
"name": "salesRevenue",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"supportAndTraining": {
"name": "supportAndTraining",
"type": "text",
"primaryKey": false,
"notNull": false
},
"employees": {
"name": "employees",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"established": {
"name": "established",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"internalListingNumber": {
"name": "internalListingNumber",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"reasonForSale": {
"name": "reasonForSale",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"brokerLicencing": {
"name": "brokerLicencing",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"internals": {
"name": "internals",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"businesses_userId_users_id_fk": {
"name": "businesses_userId_users_id_fk",
"tableFrom": "businesses",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"commercials": {
"name": "commercials",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"title": {
"name": "title",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"city": {
"name": "city",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"state": {
"name": "state",
"type": "char(2)",
"primaryKey": false,
"notNull": false
},
"price": {
"name": "price",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"favoritesForUser": {
"name": "favoritesForUser",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"listingsCategory": {
"name": "listingsCategory",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"hideImage": {
"name": "hideImage",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"draft": {
"name": "draft",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"zipCode": {
"name": "zipCode",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"county": {
"name": "county",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"website": {
"name": "website",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"imageOrder": {
"name": "imageOrder",
"type": "varchar(200)[]",
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(50)",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated": {
"name": "updated",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"visits": {
"name": "visits",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"lastVisit": {
"name": "lastVisit",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"commercials_userId_users_id_fk": {
"name": "commercials_userId_users_id_fk",
"tableFrom": "commercials",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"firstname": {
"name": "firstname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"lastname": {
"name": "lastname",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"email": {
"name": "email",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"phoneNumber": {
"name": "phoneNumber",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyName": {
"name": "companyName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyOverview": {
"name": "companyOverview",
"type": "text",
"primaryKey": false,
"notNull": false
},
"companyWebsite": {
"name": "companyWebsite",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"companyLocation": {
"name": "companyLocation",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"offeredServices": {
"name": "offeredServices",
"type": "text",
"primaryKey": false,
"notNull": false
},
"areasServed": {
"name": "areasServed",
"type": "varchar(100)[]",
"primaryKey": false,
"notNull": false
},
"hasProfile": {
"name": "hasProfile",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"hasCompanyLogo": {
"name": "hasCompanyLogo",
"type": "boolean",
"primaryKey": false,
"notNull": false
},
"licensedIn": {
"name": "licensedIn",
"type": "varchar(50)[]",
"primaryKey": false,
"notNull": false
},
"gender": {
"name": "gender",
"type": "gender",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"gender": {
"name": "gender",
"values": {
"male": "male",
"female": "female"
}
}
},
"schemas": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -0,0 +1,34 @@
{
"version": "5",
"dialect": "pg",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1714913766996,
"tag": "0000_third_spacker_dave",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1714981666488,
"tag": "0001_rapid_daimon_hellstrom",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1714982539265,
"tag": "0002_black_zaladane",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1715254754561,
"tag": "0003_tough_hobgoblin",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,78 @@
import { boolean, char, doublePrecision, integer, pgEnum, pgTable, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
export const PG_CONNECTION = 'PG_CONNECTION';
export const genderEnum = pgEnum('gender', ['male', 'female']);
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
firstname: varchar('firstname', { length: 255 }).notNull(),
lastname: varchar('lastname', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull(),
phoneNumber: varchar('phoneNumber', { length: 255 }),
description: text('description'),
companyName: varchar('companyName', { length: 255 }),
companyOverview: text('companyOverview'),
companyWebsite: varchar('companyWebsite', { length: 255 }),
companyLocation: varchar('companyLocation', { length: 255 }),
offeredServices: text('offeredServices'),
areasServed: varchar('areasServed', { length: 100 }).array(),
hasProfile: boolean('hasProfile'),
hasCompanyLogo: boolean('hasCompanyLogo'),
licensedIn: varchar('licensedIn', { length: 50 }).array(),
gender: genderEnum('gender'),
});
export const businesses = pgTable('businesses', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('userId').references(() => users.id),
type: integer('type'),
title: varchar('title', { length: 255 }),
description: text('description'),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
price: doublePrecision('price'),
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
draft: boolean('draft'),
listingsCategory: varchar('listingsCategory', { length: 255 }),
realEstateIncluded: boolean('realEstateIncluded'),
leasedLocation: boolean('leasedLocation'),
franchiseResale: boolean('franchiseResale'),
salesRevenue: doublePrecision('salesRevenue'),
cashFlow: doublePrecision('cashFlow'),
supportAndTraining: text('supportAndTraining'),
employees: integer('employees'),
established: integer('established'),
internalListingNumber: integer('internalListingNumber'),
reasonForSale: varchar('reasonForSale', { length: 255 }),
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
internals: text('internals'),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
});
export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('userId').references(() => users.id),
type: integer('type'),
title: varchar('title', { length: 255 }),
description: text('description'),
city: varchar('city', { length: 255 }),
state: char('state', { length: 2 }),
price: doublePrecision('price'),
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
listingsCategory: varchar('listingsCategory', { length: 255 }),
hideImage: boolean('hideImage'),
draft: boolean('draft'),
zipCode: integer('zipCode'),
county: varchar('county', { length: 255 }),
email: varchar('email', { length: 255 }),
website: varchar('website', { length: 255 }),
phoneNumber: varchar('phoneNumber', { length: 255 }),
imageOrder: varchar('imageOrder', { length: 200 }).array(),
imagePath: varchar('imagePath', { length: 50 }),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
});

View File

@ -22,7 +22,7 @@ export class FileService {
fs.ensureDirSync(`./pictures/property`); fs.ensureDirSync(`./pictures/property`);
} }
private loadSubscriptions(): void { private loadSubscriptions(): void {
const filePath = join(__dirname, '..', 'assets', 'subscriptions.json'); const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
const rawData = readFileSync(filePath, 'utf8'); const rawData = readFileSync(filePath, 'utf8');
this.subscriptions = JSON.parse(rawData); this.subscriptions = JSON.parse(rawData);
} }
@ -53,17 +53,16 @@ export class FileService {
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer); // await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
} }
hasCompanyLogo(userId: string){ hasCompanyLogo(userId: string){
return fs.existsSync(`./pictures/logo/${userId}.avif`) return fs.existsSync(`./pictures/logo/${userId}.avif`)?true:false
} }
async getPropertyImages(listingId: string): Promise<ImageProperty[]> { async getPropertyImages(listingId: string): Promise<string[]> {
const result: ImageProperty[] = [] const result: string[] = []
const directory = `./pictures/property/${listingId}` const directory = `./pictures/property/${listingId}`
if (fs.existsSync(directory)) { if (fs.existsSync(directory)) {
const files = await fs.readdir(directory); const files = await fs.readdir(directory);
files.forEach(f => { files.forEach(f => {
const image: ImageProperty = { name: f, id: '', code: '' }; result.push(f)
result.push(image)
}) })
return result; return result;
} else { } else {

View File

@ -14,7 +14,7 @@ export class GeoService {
this.loadGeo(); this.loadGeo();
} }
private loadGeo(): void { private loadGeo(): void {
const filePath = join(__dirname,'..', 'assets', 'geo.json'); const filePath = join(__dirname,'../..', 'assets', 'geo.json');
const rawData = readFileSync(filePath, 'utf8'); const rawData = readFileSync(filePath, 'utf8');
this.geo = JSON.parse(rawData); this.geo = JSON.parse(rawData);
} }

View File

@ -1,51 +1,52 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; import { Controller, Delete, Get, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { SelectOptionsService } from '../select-options/select-options.service.js';
import { ListingsService } from '../listings/listings.service.js'; import { ListingsService } from '../listings/listings.service.js';
import { CommercialPropertyListing } from 'src/models/main.model.js'; import { SelectOptionsService } from '../select-options/select-options.service.js';
import { Entity, EntityData } from 'redis-om';
import { commercials } from 'src/drizzle/schema.js';
import { CommercialPropertyListing } from 'src/models/db.model.js';
@Controller('image') @Controller('image')
export class ImageController { export class ImageController {
constructor(
constructor(private fileService:FileService, private fileService: FileService,
private listingService: ListingsService, private listingService: ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
private selectOptions:SelectOptionsService) { private selectOptions: SelectOptionsService,
} ) {}
@Post('uploadPropertyPicture/:id') @Post('uploadPropertyPicture/:id')
@UseInterceptors(FileInterceptor('file'),) @UseInterceptors(FileInterceptor('file'))
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) { async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
const imagename = await this.fileService.storePropertyPicture(file, id); const imagename = await this.fileService.storePropertyPicture(file, id);
await this.listingService.addImage(id, imagename); await this.listingService.addImage(id, imagename);
} }
@Post('uploadProfile/:id') @Post('uploadProfile/:id')
@UseInterceptors(FileInterceptor('file'),) @UseInterceptors(FileInterceptor('file'))
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) { async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
await this.fileService.storeProfilePicture(file, id); await this.fileService.storeProfilePicture(file, id);
} }
@Post('uploadCompanyLogo/:id') @Post('uploadCompanyLogo/:id')
@UseInterceptors(FileInterceptor('file'),) @UseInterceptors(FileInterceptor('file'))
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) { async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('id') id: string) {
await this.fileService.storeCompanyLogo(file, id); await this.fileService.storeCompanyLogo(file, id);
} }
@Get(':id') @Get(':id')
async getPropertyImagesById(@Param('id') id: string): Promise<any> { async getPropertyImagesById(@Param('id') id: string): Promise<any> {
const result = await this.listingService.getCommercialPropertyListingById(id); const result = await this.listingService.findById(id, commercials);
const listing = result as CommercialPropertyListing; const listing = result as CommercialPropertyListing;
if (listing.imageOrder) { if (listing.imageOrder) {
return listing.imageOrder return listing.imageOrder;
} else { } else {
const imageOrder = await this.fileService.getPropertyImages(id); const imageOrder = await this.fileService.getPropertyImages(id);
listing.imageOrder = imageOrder; listing.imageOrder = imageOrder;
this.listingService.saveListing(listing); this.listingService.updateListing(listing.id, listing, commercials);
return imageOrder; return imageOrder;
} }
} }
@ -61,14 +62,13 @@ export class ImageController {
@Delete('propertyPicture/:listingid/:imagename') @Delete('propertyPicture/:listingid/:imagename')
async deletePropertyImagesById(@Param('listingid') listingid: string, @Param('imagename') imagename: string): Promise<any> { async deletePropertyImagesById(@Param('listingid') listingid: string, @Param('imagename') imagename: string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${listingid}/${imagename}`); this.fileService.deleteImage(`pictures/property/${listingid}/${imagename}`);
await this.listingService.deleteImage(listingid,imagename);
} }
@Delete('logo/:userid/') @Delete('logo/:userid/')
async deleteLogoImagesById(@Param('id') id: string): Promise<any> { async deleteLogoImagesById(@Param('id') id: string): Promise<any> {
this.fileService.deleteImage(`pictures/property//${id}`) this.fileService.deleteImage(`pictures/property//${id}`);
} }
@Delete('profile/:userid/') @Delete('profile/:userid/')
async deleteProfileImagesById(@Param('id') id: string): Promise<any> { async deleteProfileImagesById(@Param('id') id: string): Promise<any> {
this.fileService.deleteImage(`pictures/property//${id}`) this.fileService.deleteImage(`pictures/property//${id}`);
} }
} }

View File

@ -1,54 +1,47 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
import { FileService } from '../file/file.service.js';
import { convertStringToNullUndefined } from '../utils.js';
import { ListingsService } from './listings.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { businesses } from '../drizzle/schema.js';
import { ListingCriteria } from '../models/main.model.js';
import { ListingsService } from './listings.service.js';
@Controller('listings/business') @Controller('listings/business')
export class BusinessListingsController { export class BusinessListingsController {
constructor(
private readonly listingsService: ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
}
@Get()
findAll(): any {
return this.listingsService.getAllBusinessListings();
}
@Get(':id') @Get(':id')
findById(@Param('id') id: string): any { findById(@Param('id') id: string): any {
return this.listingsService.getBusinessListingById(id); return this.listingsService.findById(id, businesses);
} }
@Get('user/:userid') @Get('user/:userid')
findByUserId(@Param('userid') userid: string): any { findByUserId(@Param('userid') userid: string): any {
return this.listingsService.getBusinessListingByUserId(userid); return this.listingsService.findByUserId(userid, businesses);
} }
@Post('search') @Post('search')
find(@Body() criteria: any): any { find(@Body() criteria: ListingCriteria): any {
return this.listingsService.findBusinessListings(criteria); return this.listingsService.findListingsByCriteria(criteria, businesses);
} }
/**
* @param listing creates a new listing
*/
@Post() @Post()
save(@Body() listing: any){ create(@Body() listing: any) {
this.logger.info(`Save Listing`); this.logger.info(`Save Listing`);
this.listingsService.saveListing(listing) return this.listingsService.createListing(listing, businesses);
}
@Put()
update(@Body() listing: any) {
this.logger.info(`Save Listing`);
return this.listingsService.updateListing(listing.id, listing, businesses);
} }
/**
* @param id deletes a listing
*/
@Delete(':id') @Delete(':id')
deleteById(@Param('id') id: string) { deleteById(@Param('id') id: string) {
this.listingsService.deleteBusinessListing(id) this.listingsService.deleteListing(id, businesses);
} }
@Get('states/all')
@Delete('deleteAll') getStates(): any {
deleteAll(){ return this.listingsService.getStates(businesses);
this.listingsService.deleteAllBusinessListings()
} }
} }

View File

@ -1,51 +1,52 @@
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common'; import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
import { ListingsService } from './listings.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { FileInterceptor } from '@nestjs/platform-express'; import { commercials } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { CommercialPropertyListing, ImageProperty } from 'src/models/main.model.js'; import { ListingCriteria } from '../models/main.model.js';
import { ListingsService } from './listings.service.js';
@Controller('listings/commercialProperty') @Controller('listings/commercialProperty')
export class CommercialPropertyListingsController { export class CommercialPropertyListingsController {
constructor(
constructor(private readonly listingsService:ListingsService,private fileService:FileService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { private readonly listingsService: ListingsService,
} private fileService: FileService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
) {}
@Get(':id') @Get(':id')
findById(@Param('id') id: string): any { findById(@Param('id') id: string): any {
return this.listingsService.getCommercialPropertyListingById(id); return this.listingsService.findById(id, commercials);
}
@Get('user/:userid')
findByUserId(@Param('userid') userid: string): any {
return this.listingsService.findByUserId(userid, commercials);
} }
@Post('search') @Post('search')
find(@Body() criteria: any): any { async find(@Body() criteria: ListingCriteria): Promise<any> {
return this.listingsService.findCommercialPropertyListings(criteria); return await this.listingsService.findListingsByCriteria(criteria, commercials);
}
@Get('states/all')
getStates(): any {
return this.listingsService.getStates(commercials);
}
@Post()
async create(@Body() listing: any) {
this.logger.info(`Save Listing`);
return await this.listingsService.createListing(listing, commercials);
}
@Put()
async update(@Body() listing: any) {
this.logger.info(`Save Listing`);
return await this.listingsService.updateListing(listing.id, listing, commercials);
}
@Delete(':id')
deleteById(@Param('id') id: string) {
this.listingsService.deleteListing(id, commercials);
} }
@Put('imageOrder/:id') @Put('imageOrder/:id')
async changeImageOrder(@Param('id') id:string,@Body() imageOrder: ImageProperty[]) { async changeImageOrder(@Param('id') id: string, @Body() imageOrder: string[]) {
this.listingsService.updateImageOrder(id, imageOrder) this.listingsService.updateImageOrder(id, imageOrder);
} }
/**
* @param listing creates a new listing
*/
@Post()
save(@Body() listing: any){
this.logger.info(`Save Listing`);
this.listingsService.saveListing(listing)
}
/**
* @param id deletes a listing
*/
@Delete(':id')
deleteById(@Param('id') id:string){
this.listingsService.deleteCommercialPropertyListing(id)
}
@Delete('deleteAll')
deleteAll(){
this.listingsService.deleteAllcommercialListings()
}
} }

View File

@ -1,16 +1,15 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { BusinessListingsController } from './business-listings.controller.js'; import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { ListingsService } from './listings.service.js';
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
import { RedisModule } from '../redis/redis.module.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { UnknownListingsController } from './unknown-listings.controller.js';
import { UserModule } from '../user/user.module.js';
import { BrokerListingsController } from './broker-listings.controller.js';
import { UserService } from '../user/user.service.js'; import { UserService } from '../user/user.service.js';
import { BrokerListingsController } from './broker-listings.controller.js';
import { BusinessListingsController } from './business-listings.controller.js';
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
import { ListingsService } from './listings.service.js';
import { UnknownListingsController } from './unknown-listings.controller.js';
@Module({ @Module({
imports: [RedisModule], imports: [DrizzleModule],
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController], controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
providers: [ListingsService, FileService, UserService], providers: [ListingsService, FileService, UserService],
exports: [ListingsService], exports: [ListingsService],

View File

@ -1,186 +1,125 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { import { and, eq, gte, ilike, lte, sql } from 'drizzle-orm';
BusinessListing, import { NodePgDatabase } from 'drizzle-orm/node-postgres';
CommercialPropertyListing,
ListingCriteria,
ListingType,
ImageProperty,
ListingCategory
} from '../models/main.model.js';
import { convertStringToNullUndefined } from '../utils.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { EntityData, EntityId, Repository, Schema, SchemaDefinition } from 'redis-om'; import * as schema from '../drizzle/schema.js';
import { REDIS_CLIENT } from '../redis/redis.module.js'; import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
import { ListingCriteria } from '../models/main.model.js';
@Injectable() @Injectable()
export class ListingsService { export class ListingsService {
schemaNameBusiness:ListingCategory={name:'business'} constructor(
schemaNameCommercial:ListingCategory={name:'commercialProperty'} @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
businessListingRepository:Repository; @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
commercialPropertyListingRepository:Repository; ) {}
baseListingSchemaDef : SchemaDefinition = { private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials): any[] {
id: { type: 'string' }, const conditions = [];
userId: { type: 'string' }, if (criteria.type) {
listingsCategory: { type: 'string' }, conditions.push(eq(table.type, criteria.type));
title: { type: 'string' },
description: { type: 'string' },
country: { type: 'string' },
state:{ type: 'string' },
city:{ type: 'string' },
zipCode: { type: 'number' },
type: { type: 'string' },
price: { type: 'number' },
favoritesForUser:{ type: 'string[]' },
hideImage:{ type: 'boolean' },
draft:{ type: 'boolean' },
created:{ type: 'date' },
updated:{ type: 'date' }
} }
businessListingSchemaDef : SchemaDefinition = { if (criteria.state) {
...this.baseListingSchemaDef, conditions.push(eq(table.state, criteria.state));
salesRevenue: { type: 'number' },
cashFlow: { type: 'number' },
employees: { type: 'number' },
established: { type: 'number' },
internalListingNumber: { type: 'number' },
realEstateIncluded:{ type: 'boolean' },
leasedLocation:{ type: 'boolean' },
franchiseResale:{ type: 'boolean' },
supportAndTraining: { type: 'string' },
reasonForSale: { type: 'string' },
brokerLicencing: { type: 'string' },
internals: { type: 'string' },
} }
commercialPropertyListingSchemaDef : SchemaDefinition = { if (criteria.minPrice) {
...this.baseListingSchemaDef, conditions.push(gte(table.price, criteria.minPrice));
imageNames:{ type: 'string[]' },
} }
businessListingSchema = new Schema(this.schemaNameBusiness.name,this.businessListingSchemaDef, { if (criteria.maxPrice) {
dataStructure: 'JSON' conditions.push(lte(table.price, criteria.maxPrice));
})
commercialPropertyListingSchema = new Schema(this.schemaNameCommercial.name,this.commercialPropertyListingSchemaDef, {
dataStructure: 'JSON'
})
constructor(@Inject(REDIS_CLIENT) private readonly redis: any, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger){
this.businessListingRepository = new Repository(this.businessListingSchema, redis);
this.commercialPropertyListingRepository = new Repository(this.commercialPropertyListingSchema, redis)
this.businessListingRepository.createIndex();
this.commercialPropertyListingRepository.createIndex();
} }
async saveListing(listing: BusinessListing | CommercialPropertyListing) { if (criteria.realEstateChecked) {
const repo=listing.listingsCategory==='business'?this.businessListingRepository:this.commercialPropertyListingRepository; conditions.push(eq(businesses.realEstateIncluded, true));
let result
if (listing.id){
result = await repo.save(listing.id,listing as any)
} else {
result = await repo.save(listing as any)
listing.id=result[EntityId];
result = await repo.save(listing.id,listing as any)
} }
return result; if (criteria.title) {
conditions.push(ilike(table.title, `%${criteria.title}%`));
} }
async getCommercialPropertyListingById(id: string): Promise<CommercialPropertyListing>{ return conditions;
return await this.commercialPropertyListingRepository.fetch(id) as unknown as CommercialPropertyListing;
} }
async getBusinessListingById(id: string) { // ##############################################################
return await this.businessListingRepository.fetch(id) // Listings general
// ##############################################################
async findListingsByCriteria(criteria: ListingCriteria, table: typeof businesses | typeof commercials): Promise<{ data: Record<string, any>[]; total: number }> {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
return await this.findListings(table, criteria, start, length);
} }
async getBusinessListingByUserId(userid:string){ private async findListings(table: typeof businesses | typeof commercials, criteria: ListingCriteria, start = 0, length = 12): Promise<any> {
return await this.businessListingRepository.search().where('userId').equals(userid).return.all() const conditions = this.getConditions(criteria, table);
const [data, total] = await Promise.all([
this.conn
.select()
.from(table)
.where(and(...conditions))
.offset(start)
.limit(length),
this.conn
.select({ count: sql`count(*)` })
.from(table)
.where(and(...conditions))
.then(result => Number(result[0].count)),
]);
return { total, data };
} }
async deleteBusinessListing(id: string){ async findById(id: string, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
return await this.businessListingRepository.remove(id); const result = await this.conn
} .select()
async deleteCommercialPropertyListing(id: string){ .from(table)
return await this.commercialPropertyListingRepository.remove(id); .where(sql`${table.id} = ${id}`);
} return result[0] as BusinessListing | CommercialPropertyListing;
async getAllBusinessListings(start?: number, end?: number) {
return await this.businessListingRepository.search().return.all()
}
async getAllCommercialListings(start?: number, end?: number) {
return await this.commercialPropertyListingRepository.search().return.all()
}
async findBusinessListings(criteria:ListingCriteria): Promise<any> {
// let listings = await this.getAllBusinessListings();
// return this.find(criteria,listings);
this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`);
const result = await this.redis.ft.search('business:index','*',{LIMIT:{from:0,size:50}});
this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`);
return result.documents;
}
async findCommercialPropertyListings(criteria:ListingCriteria): Promise<any> {
let listings = await this.getAllCommercialListings();
return this.find(criteria,listings);
}
async deleteAllBusinessListings(){
const ids = await this.getIdsForRepo(this.schemaNameBusiness.name);
this.businessListingRepository.remove(ids);
}
async deleteAllcommercialListings(){
const ids = await this.getIdsForRepo(this.schemaNameCommercial.name);
this.commercialPropertyListingRepository.remove(ids);
}
async getIdsForRepo(repoName:string, maxcount=100000){
let cursor = 0;
let ids = [];
do {
const reply = await this.redis.scan(cursor, {
MATCH: `${repoName}:*`,
COUNT: maxcount
});
cursor = reply.cursor;
// Extrahiere die ID aus jedem Schlüssel und füge sie zur Liste hinzu
ids = ids.concat(reply.keys.map(key => key.split(':')[1]).filter(id=>id!='index'));
} while (cursor !== 0);
return ids;
} }
async find(criteria:ListingCriteria, listings: any[]): Promise<any> { async findByUserId(userId: string, table: typeof businesses | typeof commercials): Promise<BusinessListing[] | CommercialPropertyListing[]> {
listings=listings.filter(l=>l.listingsCategory===criteria.listingsCategory); return (await this.conn.select().from(table).where(eq(table.userId, userId))) as BusinessListing[] | CommercialPropertyListing[];
if (convertStringToNullUndefined(criteria.type)){
console.log(criteria.type);
listings=listings.filter(l=>l.type===criteria.type);
}
if (convertStringToNullUndefined(criteria.state)){
console.log(criteria.state);
listings=listings.filter(l=>l.state===criteria.state);
}
if (convertStringToNullUndefined(criteria.minPrice)){
console.log(criteria.minPrice);
listings=listings.filter(l=>l.price>=Number(criteria.minPrice));
}
if (convertStringToNullUndefined(criteria.maxPrice)){
console.log(criteria.maxPrice);
listings=listings.filter(l=>l.price<=Number(criteria.maxPrice));
}
if (convertStringToNullUndefined(criteria.realEstateChecked)){
console.log(criteria.realEstateChecked);
listings=listings.filter(l=>l.realEstateIncluded);
}
if (convertStringToNullUndefined(criteria.category)){
console.log(criteria.category);
listings=listings.filter(l=>l.category===criteria.category);
}
return listings
} }
async updateImageOrder(id:string,imageOrder: ImageProperty[]){ async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing data.created = new Date();
data.updated = new Date();
data.visits = 0;
data.lastVisit = null;
const [createdListing] = await this.conn.insert(table).values(data).returning();
return createdListing as BusinessListing | CommercialPropertyListing;
}
async updateListing(id: string, data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
data.updated = new Date();
data.created = new Date(data.created);
const [updateListing] = await this.conn.update(table).set(data).where(eq(table.id, id)).returning();
return updateListing as BusinessListing | CommercialPropertyListing;
}
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
await this.conn.delete(table).where(eq(table.id, id));
}
async getStates(table: typeof businesses | typeof commercials): Promise<any[]> {
return await this.conn
.select({ state: table.state, count: sql<number>`count(${table.id})`.mapWith(Number) })
.from(table)
.groupBy(sql`${table.state}`)
.orderBy(sql`count desc`);
}
// ##############################################################
// Images for commercial Properties
// ##############################################################
async updateImageOrder(id: string, imageOrder: string[]) {
const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
listing.imageOrder = imageOrder; listing.imageOrder = imageOrder;
this.saveListing(listing); await this.updateListing(listing.id, listing, commercials);
} }
async deleteImage(listingid:string,name:string,){ async deleteImage(id: string, name: string) {
const listing = await this.getCommercialPropertyListingById(listingid) as unknown as CommercialPropertyListing const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
const index = listing.imageOrder.findIndex(im=>im.name===name); const index = listing.imageOrder.findIndex(im => im === name);
if (index > -1) { if (index > -1) {
listing.imageOrder.splice(index, 1); listing.imageOrder.splice(index, 1);
this.saveListing(listing); await this.updateListing(listing.id, listing, commercials);
} }
} }
async addImage(id: string, imagename: string) { async addImage(id: string, imagename: string) {
const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing const listing = (await this.findById(id, commercials)) as unknown as CommercialPropertyListing;
listing.imageOrder.push({name:imagename,code:'',id:''}); listing.imageOrder.push(imagename);
this.saveListing(listing); listing.imagePath = listing.id;
await this.updateListing(listing.id, listing, commercials);
} }
} }

View File

@ -4,6 +4,8 @@ import { convertStringToNullUndefined } from '../utils.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { businesses, commercials } from 'src/drizzle/schema.js';
@Controller('listings/undefined') @Controller('listings/undefined')
export class UnknownListingsController { export class UnknownListingsController {
@ -11,19 +13,15 @@ export class UnknownListingsController {
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
} }
@Get(':id') @Get(':id')
async findById(@Param('id') id:string): Promise<any> { async findById(@Param('id') id:string): Promise<any> {
const result = await this.listingsService.getBusinessListingById(id); const result = await this.listingsService.findById(id,businesses);
if (result.id){ if (result){
return result return result
} else { } else {
return await this.listingsService.getCommercialPropertyListingById(id); return await this.listingsService.findById(id,commercials);
}
}
@Get('repo/:repo')
async getAllByRepo(@Param('repo') repo:string): Promise<any> {
return await this.listingsService.getIdsForRepo(repo);
} }
} }
}

View File

@ -1,15 +1,13 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'; import { Body, Controller, Post } from '@nestjs/common';
import { User } from 'src/models/db.model.js';
import { MailInfo } from 'src/models/main.model.js';
import { MailService } from './mail.service.js'; import { MailService } from './mail.service.js';
import { KeycloakUser, MailInfo } from 'src/models/main.model.js';
@Controller('mail') @Controller('mail')
export class MailController { export class MailController {
constructor(private mailService:MailService){ constructor(private mailService: MailService) {}
}
@Post() @Post()
sendEMail(@Body() mailInfo: MailInfo): Promise< KeycloakUser> { sendEMail(@Body() mailInfo: MailInfo): Promise<User> {
return this.mailService.sendInquiry(mailInfo); return this.mailService.sendInquiry(mailInfo);
} }
} }

View File

@ -1,26 +1,27 @@
import { Module } from '@nestjs/common';
import { MailService } from './mail.service.js';
import { MailController } from './mail.controller.js';
import { MailerModule } from '@nestjs-modules/mailer'; import { MailerModule } from '@nestjs-modules/mailer';
import path, { join } from 'path';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js'; import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
import { Module } from '@nestjs/common';
import path, { join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { AuthModule } from '../auth/auth.module.js'; import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { FileService } from '../file/file.service.js';
import { UserModule } from '../user/user.module.js';
import { UserService } from '../user/user.service.js';
import { MailController } from './mail.controller.js';
import { MailService } from './mail.service.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const user = process.env.amazon_user;
const password = process.env.amazon_password;
@Module({ @Module({
imports: [AuthModule, imports: [
DrizzleModule,
UserModule,
MailerModule.forRoot({ MailerModule.forRoot({
// transport: 'smtps://user@example.com:topsecret@smtp.example.com',
// or
transport: { transport: {
host: 'email-smtp.us-east-2.amazonaws.com', host: 'email-smtp.us-east-2.amazonaws.com',
secure: false, secure: false,
port: 587, port: 587,
// auth: {
// user: 'andreas.knuth@gmail.com',
// pass: 'ksnh xjae dqbv xana',
// },
auth: { auth: {
user: 'AKIAU6GDWVAQ2QNFLNWN', user: 'AKIAU6GDWVAQ2QNFLNWN',
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7', pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
@ -38,7 +39,7 @@ const __dirname = path.dirname(__filename);
}, },
}), }),
], ],
providers: [MailService], providers: [MailService, UserService, FileService],
controllers: [MailController] controllers: [MailController],
}) })
export class MailModule {} export class MailModule {}

View File

@ -1,26 +1,41 @@
import { MailerService } from '@nestjs-modules/mailer'; import { MailerService } from '@nestjs-modules/mailer';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { AuthService } from '../auth/auth.service.js'; import path, { join } from 'path';
import { KeycloakUser, MailInfo, User } from '../models/main.model.js'; import { fileURLToPath } from 'url';
import { User } from '../models/db.model.js';
import { MailInfo } from '../models/main.model.js';
import { UserService } from '../user/user.service.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@Injectable() @Injectable()
export class MailService { export class MailService {
constructor(private mailerService: MailerService, private authService:AuthService) {} constructor(
private mailerService: MailerService,
private userService: UserService,
) {}
async sendInquiry(mailInfo: MailInfo):Promise<KeycloakUser> { async sendInquiry(mailInfo: MailInfo): Promise<User> {
const user = await this.authService.getUser(mailInfo.userId) as KeycloakUser; //const user = await this.authService.getUser(mailInfo.userId) as KeycloakUser;
const user = await this.userService.getUserByMail(mailInfo.email);
console.log(JSON.stringify(user)); console.log(JSON.stringify(user));
await this.mailerService.sendMail({ await this.mailerService.sendMail({
to: user.email, to: user.email,
from: '"Bizmatch Team" <info@bizmatch.net>', // override default from from: '"Bizmatch Team" <info@bizmatch.net>', // override default from
subject: `Inquiry from ${mailInfo.sender.name}`, subject: `Inquiry from ${mailInfo.sender.name}`,
template: './inquiry', // `.hbs` extension is appended automatically //template: './inquiry', // `.hbs` extension is appended automatically
context: { // ✏️ filling curly brackets with content template: join(__dirname, '../..', 'mail/templates/inquiry.hbs'),
name: user.firstName, context: {
inquiry:mailInfo.sender.comments // ✏️ filling curly brackets with content
name: user.firstname,
inquiry: mailInfo.sender.comments,
internalListingNumber: mailInfo.listing.internalListingNumber,
title: mailInfo.listing.title,
iname: mailInfo.sender.name,
phone: mailInfo.sender.phoneNumber,
email: mailInfo.sender.email,
}, },
}); });
return user return user;
} }
} }

View File

@ -1,6 +1,10 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js'; import { AppModule } from './app.module.js';
import * as express from 'express';
import path, { join } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('bizmatch'); app.setGlobalPrefix('bizmatch');

View File

@ -0,0 +1,73 @@
export interface User {
id?: string;
firstname: string;
lastname: string;
email: string;
phoneNumber?: string;
description?: string;
companyName?: string;
companyOverview?: string;
companyWebsite?: string;
companyLocation?: string;
offeredServices?: string;
areasServed?: string[];
hasProfile?: boolean;
hasCompanyLogo?: boolean;
licensedIn?: string[];
}
export interface BusinessListing {
id: string;
userId?: string;
type?: number;
title?: string;
description?: string;
city?: string;
state?: string;
price?: number;
favoritesForUser?: string[];
draft?: boolean;
realEstateIncluded?: boolean;
leasedLocation?: boolean;
franchiseResale?: boolean;
salesRevenue?: number;
cashFlow?: number;
supportAndTraining?: string;
employees?: number;
established?: number;
internalListingNumber?: number;
reasonForSale?: string;
brokerLicencing?: string;
internals?: string;
created?: Date;
updated?: Date;
visits?: number;
lastVisit?: Date;
listingsCategory?: string;
}
export interface CommercialPropertyListing {
id: string;
userId?: string;
type?: number;
title?: string;
description?: string;
city?: string;
state?: string;
price?: number;
favoritesForUser?: string[];
hideImage?: boolean;
draft?: boolean;
zipCode?: number;
county?: string;
email?: string;
website?: string;
phoneNumber?: string;
imageOrder?: string[];
imagePath?: string;
created?: Date;
updated?: Date;
visits?: number;
lastVisit?: Date;
listingsCategory?: string;
}

View File

@ -1 +0,0 @@
../../../common-models/src/main.model.ts

View File

@ -0,0 +1,166 @@
import { BusinessListing, CommercialPropertyListing, User } from './db.model';
export interface StatesResult {
state: string;
count: number;
}
export interface KeyValue {
name: string;
value: string;
}
export interface KeyValueRatio {
label: string;
value: number;
}
export interface KeyValueStyle {
name: string;
value: string;
icon: string;
bgColorClass: string;
textColorClass: string;
}
export type SelectOption<T = number> = {
value: T;
label: string;
};
export type ImageType = {
name: 'propertyPicture' | 'companyLogo' | 'profile';
upload: string;
delete: string;
};
export type ListingCategory = {
name: 'business' | 'commercialProperty';
};
export type ListingType = BusinessListing | CommercialPropertyListing;
export type ResponseBusinessListingArray = {
data: BusinessListing[];
total: number;
};
export type ResponseBusinessListing = {
data: BusinessListing;
};
export type ResponseCommercialPropertyListingArray = {
data: CommercialPropertyListing[];
total: number;
};
export type ResponseCommercialPropertyListing = {
data: CommercialPropertyListing;
};
export type ResponseUsersArray = {
data: User[];
total: number;
};
export interface ListingCriteria {
start: number;
length: number;
page: number;
pageCount: number;
type: number;
state: string;
minPrice: number;
maxPrice: number;
realEstateChecked: boolean;
title: string;
category: 'professional|broker';
name: string;
}
export interface KeycloakUser {
id: string;
createdTimestamp: number;
username: string;
enabled: boolean;
totp: boolean;
emailVerified: boolean;
firstName: string;
lastName: string;
email: string;
disableableCredentialTypes: any[];
requiredActions: any[];
notBefore: number;
access: Access;
}
export interface Access {
manageGroupMembership: boolean;
view: boolean;
mapRoles: boolean;
impersonate: boolean;
manage: boolean;
}
export interface Subscription {
id: string;
userId: string;
level: string;
start: Date;
modified: Date;
end: Date;
status: string;
invoices: Array<Invoice>;
}
export interface Invoice {
id: string;
date: Date;
price: number;
}
export interface JwtToken {
exp: number;
iat: number;
auth_time: number;
jti: string;
iss: string;
aud: string;
sub: string;
typ: string;
azp: string;
nonce: string;
session_state: string;
acr: string;
realm_access: Realmaccess;
resource_access: Resourceaccess;
scope: string;
sid: string;
email_verified: boolean;
name: string;
preferred_username: string;
given_name: string;
family_name: string;
email: string;
user_id: string;
}
interface Resourceaccess {
account: Realmaccess;
}
interface Realmaccess {
roles: string[];
}
export interface PageEvent {
first: number;
rows: number;
page: number;
pageCount: number;
}
export interface AutoCompleteCompleteEvent {
originalEvent: Event;
query: string;
}
export interface MailInfo {
sender: Sender;
userId: string;
email: string;
listing?: BusinessListing;
}
export interface Sender {
name?: string;
email?: string;
phoneNumber?: string;
state?: string;
comments?: string;
}
export interface ImageProperty {
id: string;
code: string;
name: string;
}

View File

@ -1,7 +1,4 @@
import { Entity } from "redis-om"; import { Entity } from "redis-om";
import { UserBase } from "./main.model.js";
export interface Geo { export interface Geo {
id: number; id: number;
name: string; name: string;
@ -65,6 +62,3 @@ export interface Timezone {
abbreviation: string; abbreviation: string;
tzName: string; tzName: string;
} }
export interface UserEntity extends UserBase, Entity {
licensedIn?: string[];
}

View File

@ -1,29 +0,0 @@
// redis.module.ts
import { Module } from '@nestjs/common';
@Module({
providers: [
{
provide: 'REDIS_OPTIONS',
useValue: {
url: 'redis://localhost:6379'
}
},
{
inject: ['REDIS_OPTIONS'],
provide: 'REDIS_CLIENT',
useFactory: async (options: { url: string }) => {
const client = createClient(options);
await client.connect();
return client;
}
}],
exports:['REDIS_CLIENT']
})
export class RedisModule {}
export const REDIS_CLIENT = "REDIS_CLIENT";
// redis.service.ts
import { Injectable } from '@nestjs/common';
import { createClient } from 'redis';

View File

@ -0,0 +1,16 @@
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RequestDurationMiddleware implements NestMiddleware {
private readonly logger = new Logger(RequestDurationMiddleware.name);
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`);
});
next();
}
}

View File

@ -3,7 +3,6 @@ import { ImageType, KeyValue, KeyValueStyle } from '../models/main.model.js';
@Injectable() @Injectable()
export class SelectOptionsService { export class SelectOptionsService {
constructor() {} constructor() {}
public typesOfBusiness: Array<KeyValueStyle> = [ public typesOfBusiness: Array<KeyValueStyle> = [
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' }, { name: 'Automotive', value: '1', icon: 'fa-solid fa-car', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' },
@ -39,20 +38,19 @@ export class SelectOptionsService {
public listingCategories: Array<KeyValue> = [ public listingCategories: Array<KeyValue> = [
{ name: 'Business', value: 'business' }, { name: 'Business', value: 'business' },
{ name: 'Commercial Property', value: 'commercialProperty' }, { name: 'Commercial Property', value: 'commercialProperty' },
] ];
public categories: Array<KeyValueStyle> = [ public categories: Array<KeyValueStyle> = [
{ name: 'Broker', value: 'broker', icon: 'pi-image', bgColorClass: 'bg-green-100', textColorClass: 'text-green-600' }, { 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' }, { name: 'Professional', value: 'professional', icon: 'pi-globe', bgColorClass: 'bg-yellow-100', textColorClass: 'text-yellow-600' },
] ];
public imageTypes: ImageType[] = [ public imageTypes: ImageType[] = [
{ name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' }, { name: 'propertyPicture', upload: 'uploadPropertyPicture', delete: 'propertyPicture' },
{ name: 'companyLogo', upload: 'uploadCompanyLogo', delete: 'logo' }, { name: 'companyLogo', upload: 'uploadCompanyLogo', delete: 'logo' },
{ name: 'profile', upload: 'uploadProfile', delete: 'profile' }, { name: 'profile', upload: 'uploadProfile', delete: 'profile' },
] ];
private usStates = [ private usStates = [
{ name: 'ALABAMA', abbreviation: 'AL' }, { name: 'ALABAMA', abbreviation: 'AL' },
{ name: 'ALASKA', abbreviation: 'AK' }, { name: 'ALASKA', abbreviation: 'AK' },
{ name: 'AMERICAN SAMOA', abbreviation: 'AS'},
{ name: 'ARIZONA', abbreviation: 'AZ' }, { name: 'ARIZONA', abbreviation: 'AZ' },
{ name: 'ARKANSAS', abbreviation: 'AR' }, { name: 'ARKANSAS', abbreviation: 'AR' },
{ name: 'CALIFORNIA', abbreviation: 'CA' }, { name: 'CALIFORNIA', abbreviation: 'CA' },
@ -60,7 +58,6 @@ export class SelectOptionsService {
{ name: 'CONNECTICUT', abbreviation: 'CT' }, { name: 'CONNECTICUT', abbreviation: 'CT' },
{ name: 'DELAWARE', abbreviation: 'DE' }, { name: 'DELAWARE', abbreviation: 'DE' },
{ name: 'DISTRICT OF COLUMBIA', abbreviation: 'DC' }, { name: 'DISTRICT OF COLUMBIA', abbreviation: 'DC' },
{ name: 'FEDERATED STATES OF MICRONESIA', abbreviation: 'FM'},
{ name: 'FLORIDA', abbreviation: 'FL' }, { name: 'FLORIDA', abbreviation: 'FL' },
{ name: 'GEORGIA', abbreviation: 'GA' }, { name: 'GEORGIA', abbreviation: 'GA' },
{ name: 'GUAM', abbreviation: 'GU' }, { name: 'GUAM', abbreviation: 'GU' },
@ -73,7 +70,6 @@ export class SelectOptionsService {
{ name: 'KENTUCKY', abbreviation: 'KY' }, { name: 'KENTUCKY', abbreviation: 'KY' },
{ name: 'LOUISIANA', abbreviation: 'LA' }, { name: 'LOUISIANA', abbreviation: 'LA' },
{ name: 'MAINE', abbreviation: 'ME' }, { name: 'MAINE', abbreviation: 'ME' },
{ name: 'MARSHALL ISLANDS', abbreviation: 'MH'},
{ name: 'MARYLAND', abbreviation: 'MD' }, { name: 'MARYLAND', abbreviation: 'MD' },
{ name: 'MASSACHUSETTS', abbreviation: 'MA' }, { name: 'MASSACHUSETTS', abbreviation: 'MA' },
{ name: 'MICHIGAN', abbreviation: 'MI' }, { name: 'MICHIGAN', abbreviation: 'MI' },
@ -89,13 +85,11 @@ export class SelectOptionsService {
{ name: 'NEW YORK', abbreviation: 'NY' }, { name: 'NEW YORK', abbreviation: 'NY' },
{ name: 'NORTH CAROLINA', abbreviation: 'NC' }, { name: 'NORTH CAROLINA', abbreviation: 'NC' },
{ name: 'NORTH DAKOTA', abbreviation: 'ND' }, { name: 'NORTH DAKOTA', abbreviation: 'ND' },
{ name: 'NORTHERN MARIANA ISLANDS', abbreviation: 'MP'},
{ name: 'OHIO', abbreviation: 'OH' }, { name: 'OHIO', abbreviation: 'OH' },
{ name: 'OKLAHOMA', abbreviation: 'OK' }, { name: 'OKLAHOMA', abbreviation: 'OK' },
{ name: 'OREGON', abbreviation: 'OR' }, { name: 'OREGON', abbreviation: 'OR' },
{ name: 'PALAU', abbreviation: 'PW' }, { name: 'PALAU', abbreviation: 'PW' },
{ name: 'PENNSYLVANIA', abbreviation: 'PA' }, { name: 'PENNSYLVANIA', abbreviation: 'PA' },
{ name: 'PUERTO RICO', abbreviation: 'PR'},
{ name: 'RHODE ISLAND', abbreviation: 'RI' }, { name: 'RHODE ISLAND', abbreviation: 'RI' },
{ name: 'SOUTH CAROLINA', abbreviation: 'SC' }, { name: 'SOUTH CAROLINA', abbreviation: 'SC' },
{ name: 'SOUTH DAKOTA', abbreviation: 'SD' }, { name: 'SOUTH DAKOTA', abbreviation: 'SD' },
@ -103,13 +97,11 @@ export class SelectOptionsService {
{ name: 'TEXAS', abbreviation: 'TX' }, { name: 'TEXAS', abbreviation: 'TX' },
{ name: 'UTAH', abbreviation: 'UT' }, { name: 'UTAH', abbreviation: 'UT' },
{ name: 'VERMONT', abbreviation: 'VT' }, { name: 'VERMONT', abbreviation: 'VT' },
{ name: 'VIRGIN ISLANDS', abbreviation: 'VI'},
{ name: 'VIRGINIA', abbreviation: 'VA' }, { name: 'VIRGINIA', abbreviation: 'VA' },
{ name: 'WASHINGTON', abbreviation: 'WA' }, { name: 'WASHINGTON', abbreviation: 'WA' },
{ name: 'WEST VIRGINIA', abbreviation: 'WV' }, { name: 'WEST VIRGINIA', abbreviation: 'WV' },
{ name: 'WISCONSIN', abbreviation: 'WI' }, { name: 'WISCONSIN', abbreviation: 'WI' },
{ name: 'WYOMING', abbreviation: 'WY' } { name: 'WYOMING', abbreviation: 'WY' },
] ];
public locations:Array<any> = [...this.usStates.map(state=>({name:state.name, value:state.abbreviation}))].concat({name:'CANADA',value:"CA"}); public locations: Array<any> = [...this.usStates.map(state => ({ name: state.name, value: state.abbreviation }))].concat({ name: 'CANADA', value: 'CA' });
} }

View File

@ -1,14 +1,21 @@
import { Body, Controller, Get, Inject, Param, Post, Put } from '@nestjs/common'; import { Body, Controller, Get, Inject, Param, Post, Put, Query, Req } from '@nestjs/common';
import { UserService } from './user.service.js'; import { UserService } from './user.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { UserEntity } from 'src/models/server.model.js'; import { User } from 'src/models/db.model.js';
@Controller('user') @Controller('user')
export class UserController { export class UserController {
constructor(private userService: UserService, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {} constructor(private userService: UserService, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
@Get()
findByMail(@Query('mail') mail: string): any {
this.logger.info(`Searching for user with EMail: ${mail}`);
const user = this.userService.getUserByMail(mail);
this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user;
}
@Get(':id') @Get(':id')
findById(@Param('id') id: string): any { findById(@Param('id') id: string): any {
this.logger.info(`Searching for user with ID: ${id}`); this.logger.info(`Searching for user with ID: ${id}`);
@ -16,9 +23,8 @@ export class UserController {
this.logger.info(`Found user: ${JSON.stringify(user)}`); this.logger.info(`Found user: ${JSON.stringify(user)}`);
return user; return user;
} }
@Post() @Post()
save(@Body() user: any): Promise<UserEntity> { save(@Body() user: any): Promise<User> {
this.logger.info(`Saving user: ${JSON.stringify(user)}`); this.logger.info(`Saving user: ${JSON.stringify(user)}`);
const savedUser = this.userService.saveUser(user); const savedUser = this.userService.saveUser(user);
this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`); this.logger.info(`User persisted: ${JSON.stringify(savedUser)}`);

View File

@ -1,13 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DrizzleModule } from '../drizzle/drizzle.module.js';
import { FileService } from '../file/file.service.js';
import { UserController } from './user.controller.js'; import { UserController } from './user.controller.js';
import { UserService } from './user.service.js'; import { UserService } from './user.service.js';
import { RedisModule } from '../redis/redis.module.js';
import { FileService } from '../file/file.service.js';
@Module({ @Module({
imports: [RedisModule], imports: [DrizzleModule],
controllers: [UserController], controllers: [UserController],
providers: [UserService,FileService] providers: [UserService, FileService],
}) })
export class UserModule { export class UserModule {}
}

View File

@ -1,45 +1,77 @@
import { Get, Inject, Injectable, Param } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { createClient } from 'redis'; import { and, eq, ilike, or, sql } from 'drizzle-orm';
import { Entity, Repository, Schema } from 'redis-om'; import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
import { ListingCriteria, User } from '../models/main.model.js'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { REDIS_CLIENT } from '../redis/redis.module.js'; import { PG_CONNECTION } from 'src/drizzle/schema.js';
import { UserEntity } from '../models/server.model.js'; import { User } from 'src/models/db.model.js';
import { Logger } from 'winston';
import * as schema from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js'; import { FileService } from '../file/file.service.js';
import { ListingCriteria } from '../models/main.model.js';
@Injectable() @Injectable()
export class UserService { export class UserService {
userRepository:Repository; constructor(
userSchema = new Schema('user',{ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
id: { type: 'string' }, @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
firstname: { type: 'string' }, private fileService: FileService,
lastname: { type: 'string' }, ) {}
email: { type: 'string' }, private getConditions(criteria: ListingCriteria): any[] {
phoneNumber: { type: 'string' }, const conditions = [];
companyOverview:{ type: 'string' }, if (criteria.state) {
companyWebsite:{ type: 'string' }, conditions.push(sql`EXISTS (SELECT 1 FROM unnest(users."areasServed") AS area WHERE area LIKE '%' || ${criteria.state} || '%')`);
companyLocation:{ type: 'string' }, }
offeredServices:{ type: 'string' }, if (criteria.name) {
areasServed:{ type: 'string[]' }, conditions.push(or(ilike(schema.users.firstname, `%${criteria.name}%`), ilike(schema.users.lastname, `%${criteria.name}%`)));
names:{ type: 'string[]', path:'$.licensedIn.name' }, }
values:{ type: 'string[]', path:'$.licensedIn.value' } return conditions;
}, { }
dataStructure: 'JSON' async getUserByMail(email: string) {
}) const users = (await this.conn
constructor(@Inject(REDIS_CLIENT) private readonly redis: any,private fileService:FileService){ .select()
this.userRepository = new Repository(this.userSchema, redis) .from(schema.users)
this.userRepository.createIndex(); .where(sql`email = ${email}`)) as User[];
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(user.id);
user.hasProfile = this.fileService.hasProfile(user.id);
return user;
} }
async getUserById(id: string) { async getUserById(id: string) {
const user = await this.userRepository.fetch(id) as UserEntity; const users = (await this.conn
.select()
.from(schema.users)
.where(sql`id = ${id}`)) as User[];
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(id); user.hasCompanyLogo = this.fileService.hasCompanyLogo(id);
user.hasProfile = this.fileService.hasProfile(id); user.hasProfile = this.fileService.hasProfile(id);
return user; return user;
} }
async saveUser(user:any):Promise<UserEntity>{ async saveUser(user: any): Promise<User> {
return await this.userRepository.save(user.id,user) as UserEntity if (user.id) {
const [updateUser] = await this.conn.update(schema.users).set(user).where(eq(schema.users.id, user.id)).returning();
return updateUser as User;
} else {
const [newUser] = await this.conn.insert(schema.users).values(user).returning();
return newUser as User;
}
} }
async findUser(criteria: ListingCriteria) { async findUser(criteria: ListingCriteria) {
return await this.userRepository.search().return.all(); const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const conditions = this.getConditions(criteria);
const [data, total] = await Promise.all([
this.conn
.select()
.from(schema.users)
.where(and(...conditions))
.offset(start)
.limit(length),
this.conn
.select({ count: sql`count(*)` })
.from(schema.users)
.where(and(...conditions))
.then(result => Number(result[0].count)),
]);
return { total, data };
} }
} }

View File

@ -1,4 +1,4 @@
{ {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"] "exclude": ["node_modules", "test", "dist", "**/*spec.ts"],
} }

View File

@ -1,8 +1,8 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2021", "target": "ES2021",
"module": "Node16", "module": "ESNext",
"moduleResolution": "Node16", "moduleResolution": "Node",
"declaration": true, "declaration": true,
"removeComments": true, "removeComments": true,
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
@ -16,7 +16,8 @@
"strictNullChecks": false, "strictNullChecks": false,
"noImplicitAny": false, "noImplicitAny": false,
"strictBindCallApply": false, "strictBindCallApply": false,
"forceConsistentCasingInFileNames": false, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": false, "noFallthroughCasesInSwitch": false,
"esModuleInterop":true
} }
} }

18
bizmatch/.prettierrc.json Normal file
View File

@ -0,0 +1,18 @@
{
"arrowParens": "avoid",
"embeddedLanguageFormatting": "auto",
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"jsxSingleQuote": false,
"printWidth": 220,
"proseWrap": "always",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

28
bizmatch/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,28 @@
{
"editor.suggestSelection": "first",
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
"explorer.confirmDelete": false,
"typescript.updateImportsOnFileMove.enabled": "always",
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
},
"prettier.printWidth": 240,
"git.autofetch": false,
"git.autorefresh": true
}

32
bizmatch/certs/cert.pem Normal file
View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFgTCCA2mgAwIBAgIUa+QJdPmuRDNbuf/nzb2J+6ii5nwwDQYJKoZIhvcNAQEL
BQAwUDELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05SVzEQMA4GA1UEBwwHRVJLUkFU
SDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTI0MDUxMDEy
NTQxNVoXDTI1MDUxMDEyNTQxNVowUDELMAkGA1UEBhMCREUxDDAKBgNVBAgMA05S
VzEQMA4GA1UEBwwHRVJLUkFUSDEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
dHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxiTSQDCC/i3n
X6bMKpl0baUgjbzYDc7ZrvIYfj/t25sdv0E/07ysbXNuldzCX6Rnva/1wVZS30zy
vQm8cVM074oP9qy7wKeIU15nEwRe03P5zipix1WXGWXHY+ShZ2MHy/iDQ1XzpO3p
xXs2vxuJZSUoz1M0c+/pTWBx2D790l5qNkt2sbk5NaHfPQDuw+y2cXDqmJqcCi9I
rYbaQGhXeb/IRu8pW4UwasVsq7DxGlDX8k8Dva5O0Ixf+muqQELuMdeTtR/PoFxw
2F+qOUlS2ujuyQkLOvVZOTalxRfWMuexaQzLlQO91MDehTrOFuMUBCKhYztgZKe2
k9z0fTJmtLyxMPTQuZCv1Gnrw6hcVxjiFQ8YP2ni+ekb86dIA3llH8r+4xEGygfB
QxHiBH9uO8Q9MFpfU2CPE7GxQoB17fu4KqaK0ucVnNM+rJcsNom9svixb5C4CkS/
S1/KQVDi8mrYwQIOP+Y4YLuNvSvUlitZXq8h0ogVqNMl2+R0CYX4lk/mkOEeCeGW
yG4ek2GQxZNLAnoMoLb+kHnVhPaV0SWW052wvXZzOrIMrlkSZK6yYim3JPsD8hc/
284lNEFL3DknICPsVFd64LjwPxA0J5AqyhQAvpXyFVHUUA5+h2EATrBh/Fp9cw84
AkEeVArMWOlx5cg7nAdgQaD5XUaBp7kCAwEAAaNTMFEwHQYDVR0OBBYEFMSO9FoT
nqjHpniyExGf53tV/TAhMB8GA1UdIwQYMBaAFMSO9FoTnqjHpniyExGf53tV/TAh
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBADbsIroXoCIe8adn
zh/WNZUUXLDW3JU2QyyYKhnZgqE0Wqh5QLgNwd5ZfH3Iaqhf0xFH9jUeEAWWA17d
hVy4rWsC200DRZ5BYOaffqdflpDE3yAk+8p3kAWjogaCX1wvBuLZ9BHWpuqQ72ec
zYM2ag2wWfBpicXl4BaSnsx1xErxKvxgixgGy6BhcErmfnYtJDnU2Cl+MSMXb758
7hAl9JiJAH8OuaAjvbkhSVTZQFjDgCGHHQ8mR8IksWdGGe/LN/yoWc4lk7lv7vmu
YBfP/SNZvxBzZlw1fXcdz1Wirljy7yz3+s59Knkc2jysysFC4LkZlUy0unmGoPy0
D1XdXyDMy05eoUaeRnM0rfzUxYfXMA3sQlsWw7he6fD8YylVedXxd3mcfK0jle0y
VkDyreZ9+mc/4vmjW0KpOfFGvhhAS9L1D8K3bKpky3HoHSqK1Nb8Ymh/WkhOpHwg
unUyIKdRHvGeWkUXQaLbRKI6w2BQwT7oKDOD60cJG26U3XcYarevz9qHsZX865tj
4xZrp+IUr8OkYBnRrmx2TZ70goRXI77nHVzHmY+xHhjvPJOZOcUAvEHU+5VY3ucN
0noEqiYzb77LcqVbbL3cywDLiyfdx9/x8TU1iYPA+IMwhYb/tLBFzFWmR7znw6On
D775XK/EVryozX/6GmtG+XGZs+57
-----END CERTIFICATE-----

52
bizmatch/certs/key.pem Normal file
View File

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDGJNJAMIL+Ledf
pswqmXRtpSCNvNgNztmu8hh+P+3bmx2/QT/TvKxtc26V3MJfpGe9r/XBVlLfTPK9
CbxxUzTvig/2rLvAp4hTXmcTBF7Tc/nOKmLHVZcZZcdj5KFnYwfL+INDVfOk7enF
eza/G4llJSjPUzRz7+lNYHHYPv3SXmo2S3axuTk1od89AO7D7LZxcOqYmpwKL0it
htpAaFd5v8hG7ylbhTBqxWyrsPEaUNfyTwO9rk7QjF/6a6pAQu4x15O1H8+gXHDY
X6o5SVLa6O7JCQs69Vk5NqXFF9Yy57FpDMuVA73UwN6FOs4W4xQEIqFjO2Bkp7aT
3PR9Mma0vLEw9NC5kK/UaevDqFxXGOIVDxg/aeL56Rvzp0gDeWUfyv7jEQbKB8FD
EeIEf247xD0wWl9TYI8TsbFCgHXt+7gqporS5xWc0z6slyw2ib2y+LFvkLgKRL9L
X8pBUOLyatjBAg4/5jhgu429K9SWK1leryHSiBWo0yXb5HQJhfiWT+aQ4R4J4ZbI
bh6TYZDFk0sCegygtv6QedWE9pXRJZbTnbC9dnM6sgyuWRJkrrJiKbck+wPyFz/b
ziU0QUvcOScgI+xUV3rguPA/EDQnkCrKFAC+lfIVUdRQDn6HYQBOsGH8Wn1zDzgC
QR5UCsxY6XHlyDucB2BBoPldRoGnuQIDAQABAoICAAazixrZqSyAj/E3unL8Yqgs
rAevKd15r/oPPQ3UCq7hNaXYxphaKri+7TALWdWTQWD0eQrTaRUdTJ5hHGr2xfUO
BdExcV4oLF+pczH89VoQc5Pp8hJMzkHxI8e4nU7aVhKrcoEOAKIE2+Guc6EOBN0T
Xyh352++XvUbfG40XzBEujHg5oBHQ+yQ73RoOisNL/RxPbXwkLN1eu9Hfs0r2j2H
Y3Yms47hV8xcpfq+jsD1mAAddQJuyUKbZMma55SpztWHtXqsO0Dwr25Z+e9bD/7Q
XvcUo7kYQC7DruKWFkv9cw4a/S2qhTqTVVNLNFoozu3+39dz1CRDWdTxZaFwWXHX
KWc/YRmi5gxjadF3F87Ur+DYBLUJT/Iz5iUfUOQ/OON4ARDUM0UNqC1DemNxxI53
eiCw21GYwIA/59w7cWHOHJZImXKBMVJ3ko7hjI1eo4LWPvDfQHlG+5Fo9vUNPk9c
GiIreWMIycc5yUm9y1zxTa5zJdOBS+dQD0T230++o9OPytAiz2wT4ChXBfm02WUs
0UaU3uGSDmsvt4WI9SjlvSQi12EVekYc86lmlpgtMxc6zBpwJaFSrZa+N4ax9A5P
hOOeO6jy5XjEHTcIW+Yhrb0PCEr/U2xSn+zfhclHGTuwQLTdB30SB1pPECtA1j2+
rdcFx9+1asoIlQxO+k2pAoIBAQDsQQesFELq96/+7rTSgN1lm19AqKvm102WiOHT
CgJhltH19PyOnO23MKQKoYabTsSH2kOpwaA3CKPTk1ecU0P6mptN1xxEN7NgkRKC
Wr/pilXKTJg98A/o0zbxZkeYmyT3x6XwWVU1w4msR5lQ7bH4eR6MaQDghAKTRrAS
XMRFM4WQBfWr6DLTtz6Am5o+dA5qr8iIdjqGn8CgzPPqFQ/1ItWRkR2eiaGBUUhY
2Z8sL+3WW8h0u5qULqIrpY4EjLfl68FFbYgoxSLskQMpX4H/fCc8s9XPn527Ckiv
UuoRP7wsdcpt3H3SGDB8ZePH2JAbeu7nauAYeoSuzmqLwsF9AoIBAQDWtF96PpHK
FGQ8Epy79FDuJ95qaMFrfHRhx60erb2hHdwUYpBgGXHzN2icdP6Jg90ZyLdU97xH
QSNrTLxeUYcCZf0pucPYMFvNdMhTXiYLVpfwkho7Wl4YmuXEOG5ZW4pp0+sgFF/X
V8p2hu5QART6JwVsyBrO2T7EoGVDppbbhzF6tXnuLDV/JiP6/QcEYMxitQ06s6B2
8MXIqqNbidaCoALmeDgzderSKmiSGHWcAO6mef+xh0qZMfpOjwVLRTQyheiHJaub
DkBbtNu91kPJoyyn5+dCbuuK+tOii3FSANBAFH19esZJfcuZWo9x9dqZsT0HZ0lE
tlUDXhGBrVPtAoIBAHfD86a5Ur8YtyCOVB5Oc23Z2OzHVPWd+dgxJgG9Fj3wnhmI
iyukxCFUyCQXhExhHuIbtKdu39BmUd6k2AoIb/Kvw8EvJkYy0n1GrdJlPNqgZSM7
twXXF8mYoUa46dyj8ZamoCl6r+akbLtoRIGxLcJfbCwT4vzuDvwoHoQAgQLvvmqn
isYN3Q5U25uIxiWY4eIVoJwFC2BJxfX+UDw/VyqW8RttLE29SaFr2jgogjd9SJ2d
Q75hiFhMV6u2rosB5wvoer6+awL4BN9WF/s2Tol8n8t3AxHQwb4a1YQDjWMXI0aK
pAcTerkxyAqYAGPEFjHIHSo1lMrz+SVAwOR+42UCggEAUbVxRId9WidqggYfSdRP
3GKl3V8ihPJnJDMmai96pE9Fyyg7g6cLW6ExmaFYoSLiyQY+5wIk0AU1IoeghFCI
jdwcfX2pz6OPvF/+QOPqnJQG3NHtU7svZjPEz2kebblNsrqol5vJYZ2SeosdNKtE
vXKOOPjqYuAAaDoWb6l9bexEY0ufLIn8jfgI52LWAc+I2OPINhfYMIuu6ZAu/Q42
6Z1VnToRQVxV0ke7ZiYS1Bzytb5mFbzEIgsIFE+PlzauB7A4bv5iEW9aBMyOd++L
+rezre6ubvThhRGx6wEgTjHrDwf9Pfy0a5GJI0J4pskGuUjfTer70j+FmPN6vBwn
fQKCAQANeREfOILt9Unwpbo9Vj48BMfVYvJN7Gk4K6LGWN0rE0jxtpAzBiI3BqI0
+oj1gy+6Nn9n4hbeqDSyVB5uivCxmFIXpPO1s8Xu+EpEm90Po/551wWBvePOe8YK
vJK+UqUXDDcG+CUKsY8quOrNFIbSu4vOB81lgELh/cfhF/C5yOCsJx5pk5TJFwl8
3mAlV6KKTcacqEB/kKg+3sY1sv31EdsvpwOcEmXRXhI6hv4yENk0+cEFpEgJ7gkH
gzJ5IYYSEAhfy9lPDOhwTG3VC8Fr/z6gld6V6hym9cv2emd6ifjnP4rivsGg8d77
qs7lw2IbVhzRkVryySXsCXn2O1iu
-----END PRIVATE KEY-----

95
bizmatch/dbschema.ts Normal file
View File

@ -0,0 +1,95 @@
/* tslint:disable */
/* eslint-disable */
/**
* AUTO-GENERATED FILE - DO NOT EDIT!
*
* This file was automatically generated by pg-to-ts v.4.1.1
* $ pg-to-ts generate -c postgresql://username:password@localhost:5432/bizmatch -t businesses -s public
*
*/
export type Json = unknown;
// Table businesses
export interface Businesses {
id: string;
userId: string | null;
type: number | null;
title: string | null;
description: string | null;
city: string | null;
state: string | null;
price: number | null;
favoritesForUser: string[] | null;
draft: boolean | null;
listingsCategory: string | null;
realEstateIncluded: boolean | null;
leasedLocation: boolean | null;
franchiseResale: boolean | null;
salesRevenue: number | null;
cashFlow: number | null;
supportAndTraining: string | null;
employees: number | null;
established: number | null;
internalListingNumber: number | null;
reasonForSale: string | null;
brokerLicencing: string | null;
internals: string | null;
created: Date | null;
updated: Date | null;
visits: number | null;
lastVisit: Date | null;
}
export interface BusinessesInput {
id?: string;
userId?: string | null;
type?: number | null;
title?: string | null;
description?: string | null;
city?: string | null;
state?: string | null;
price?: number | null;
favoritesForUser?: string[] | null;
draft?: boolean | null;
listingsCategory?: string | null;
realEstateIncluded?: boolean | null;
leasedLocation?: boolean | null;
franchiseResale?: boolean | null;
salesRevenue?: number | null;
cashFlow?: number | null;
supportAndTraining?: string | null;
employees?: number | null;
established?: number | null;
internalListingNumber?: number | null;
reasonForSale?: string | null;
brokerLicencing?: string | null;
internals?: string | null;
created?: Date | null;
updated?: Date | null;
visits?: number | null;
lastVisit?: Date | null;
}
const businesses = {
tableName: 'businesses',
columns: ['id', 'userId', 'type', 'title', 'description', 'city', 'state', 'price', 'favoritesForUser', 'draft', 'listingsCategory', 'realEstateIncluded', 'leasedLocation', 'franchiseResale', 'salesRevenue', 'cashFlow', 'supportAndTraining', 'employees', 'established', 'internalListingNumber', 'reasonForSale', 'brokerLicencing', 'internals', 'created', 'updated', 'visits', 'lastVisit'],
requiredForInsert: [],
primaryKey: 'id',
foreignKeys: { userId: { table: 'users', column: 'id', $type: null as unknown /* users */ }, },
$type: null as unknown as Businesses,
$input: null as unknown as BusinessesInput
} as const;
export interface TableTypes {
businesses: {
select: Businesses;
input: BusinessesInput;
};
}
export const tables = {
businesses,
}

View File

@ -3,7 +3,7 @@
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve & http-server ../bizmatch-server",
"build": "ng build", "build": "ng build",
"build.dev": "ng build --configuration dev", "build.dev": "ng build --configuration dev",
"watch": "ng build --watch --configuration development", "watch": "ng build --watch --configuration development",
@ -55,6 +55,7 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/jasmine": "~5.1.4", "@types/jasmine": "~5.1.4",
"@types/node": "^20.11.20", "@types/node": "^20.11.20",
"http-server": "^14.1.1",
"jasmine-core": "~5.1.2", "jasmine-core": "~5.1.2",
"karma": "~6.4.2", "karma": "~6.4.2",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",

View File

@ -2,5 +2,9 @@
"/api": { "/api": {
"target": "http://localhost:3000", "target": "http://localhost:3000",
"secure": false "secure": false
},
"/pictures": {
"target": "http://localhost:8080",
"secure": false
} }
} }

View File

@ -13,7 +13,7 @@ import { KeycloakEventType } from './models/keycloak-event';
import { createGenericObject } from './utils/utils'; import { createGenericObject } from './utils/utils';
import onChange from 'on-change'; import onChange from 'on-change';
import { UserService } from './services/user.service'; import { UserService } from './services/user.service';
import {ListingCriteria, User} from '../../../common-models/src/main.model' import {ListingCriteria} from '../../../bizmatch-server/src/models/main.model'
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
@ -24,7 +24,6 @@ import {ListingCriteria, User} from '../../../common-models/src/main.model'
export class AppComponent { export class AppComponent {
title = 'bizmatch'; title = 'bizmatch';
actualRoute =''; actualRoute ='';
user:User;
listingCriteria:ListingCriteria = onChange(createGenericObject<ListingCriteria>(),(path, value, previousValue, applyData)=>{ listingCriteria:ListingCriteria = onChange(createGenericObject<ListingCriteria>(),(path, value, previousValue, applyData)=>{
sessionStorage.setItem('criteria',JSON.stringify(value)); sessionStorage.setItem('criteria',JSON.stringify(value));
}); });
@ -41,7 +40,6 @@ export class AppComponent {
}); });
} }
ngOnInit(){ ngOnInit(){
this.user = this.userService.getUser();
} }

View File

@ -1,84 +1,119 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { ListingsComponent } from './pages/listings/listings.component';
import { HomeComponent } from './pages/home/home.component';
import { DetailsListingComponent } from './pages/details/details-listing/details-listing.component';
import { AccountComponent } from './pages/subscription/account/account.component';
import { EditListingComponent } from './pages/subscription/edit-listing/edit-listing.component';
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
import { authGuard } from './guards/auth.guard';
import { PricingComponent } from './pages/pricing/pricing.component';
import { LogoutComponent } from './components/logout/logout.component'; import { LogoutComponent } from './components/logout/logout.component';
import { authGuard } from './guards/auth.guard';
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
import { DetailsUserComponent } from './pages/details/details-user/details-user.component'; import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
import { HomeComponent } from './pages/home/home.component';
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.component';
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
import { PricingComponent } from './pages/pricing/pricing.component';
import { AccountComponent } from './pages/subscription/account/account.component';
import { EditBusinessListingComponent } from './pages/subscription/edit-business-listing/edit-business-listing.component';
import { EditCommercialPropertyListingComponent } from './pages/subscription/edit-commercial-property-listing/edit-commercial-property-listing.component';
import { EmailUsComponent } from './pages/subscription/email-us/email-us.component';
import { FavoritesComponent } from './pages/subscription/favorites/favorites.component';
import { MyListingComponent } from './pages/subscription/my-listing/my-listing.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
path: 'listings/:type', path: 'businessListings',
component: ListingsComponent, component: BusinessListingsComponent,
runGuardsAndResolvers: 'always',
}, },
// Umleitung von /listing zu /listing/business
{ {
path: 'listings', path: 'commercialPropertyListings',
pathMatch: 'full', component: CommercialPropertyListingsComponent,
redirectTo: 'listings/business', runGuardsAndResolvers: 'always',
runGuardsAndResolvers:'always' },
{
path: 'brokerListings',
component: BrokerListingsComponent,
runGuardsAndResolvers: 'always',
}, },
{ {
path: 'home', path: 'home',
component: HomeComponent, component: HomeComponent,
}, },
// #########
// Listings Details
{ {
path: 'details-listing/:type/:id', path: 'details-business-listing/:id',
component: DetailsListingComponent, component: DetailsBusinessListingComponent,
}, },
{ {
path: 'details-listing/:type/:id', path: 'details-commercial-property-listing/:id',
component: DetailsListingComponent, component: DetailsCommercialPropertyListingComponent,
}, },
// #########
// User Details
{ {
path: 'details-user/:id', path: 'details-user/:id',
component: DetailsUserComponent, component: DetailsUserComponent,
}, },
// #########
// User edit
{ {
path: 'account/:id', path: 'account',
component: AccountComponent, component: AccountComponent,
canActivate: [authGuard], canActivate: [authGuard],
}, },
// #########
// Create, Update Listings
{ {
path: 'editListing/:id', path: 'editBusinessListing/:id',
component: EditListingComponent, component: EditBusinessListingComponent,
canActivate: [authGuard], canActivate: [authGuard],
}, },
{ {
path: 'createListing', path: 'createBusinessListing',
component: EditListingComponent, component: EditBusinessListingComponent,
canActivate: [authGuard], canActivate: [authGuard],
}, },
{
path: 'editCommercialPropertyListing/:id',
component: EditCommercialPropertyListingComponent,
canActivate: [authGuard],
},
{
path: 'createCommercialPropertyListing',
component: EditCommercialPropertyListingComponent,
canActivate: [authGuard],
},
// #########
// My Listings
{ {
path: 'myListings', path: 'myListings',
component: MyListingComponent, component: MyListingComponent,
canActivate: [authGuard], canActivate: [authGuard],
}, },
// #########
// My Favorites
{ {
path: 'myFavorites', path: 'myFavorites',
component: FavoritesComponent, component: FavoritesComponent,
canActivate: [authGuard], canActivate: [authGuard],
}, },
// #########
// EMAil Us
{ {
path: 'emailUs', path: 'emailUs',
component: EmailUsComponent, component: EmailUsComponent,
canActivate: [authGuard], canActivate: [authGuard],
}, },
// #########
// Logout
{ {
path: 'logout', path: 'logout',
component: LogoutComponent, component: LogoutComponent,
canActivate: [authGuard], canActivate: [authGuard],
}, },
// #########
// Pricing
{ {
path: 'pricing', path: 'pricing',
component: PricingComponent component: PricingComponent,
}, },
{ path: '**', redirectTo: 'home' }, { path: '**', redirectTo: 'home' },
]; ];

View File

@ -2,7 +2,7 @@
<div class="surface-0"> <div class="surface-0">
<div class="grid"> <div class="grid">
<div class="col-12 md:col-3 md:mb-0 mb-3"> <div class="col-12 md:col-3 md:mb-0 mb-3">
<img src="assets/images/header-logo.png" alt="footer sections" height="30" class="mr-3"> <img src="assets/images/header-logo.png" alt="footer sections" height="30" class="mr-3" />
<div class="text-500">© 2024 Bizmatch All rights reserved.</div> <div class="text-500">© 2024 Bizmatch All rights reserved.</div>
</div> </div>
<div class="col-12 md:col-3"> <div class="col-12 md:col-3">
@ -18,7 +18,7 @@
<div class="col-12 md:col-3 text-500"> <div class="col-12 md:col-3 text-500">
<div class="text-black font-bold line-height-3 mb-3">Actions</div> <div class="text-black font-bold line-height-3 mb-3">Actions</div>
<a *ngIf="!userService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a> <a *ngIf="!userService.isLoggedIn()" (click)="login()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Login</a>
<a *ngIf="userService.isLoggedIn()" [routerLink]="['/account',userService.getUser()?.id]" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a> <a *ngIf="userService.isLoggedIn()" [routerLink]="['/account']" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline">Account</a>
<a *ngIf="userService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="userService.logout()">Log Out</a> <a *ngIf="userService.isLoggedIn()" class="text-500 line-height-3 block cursor-pointer mb-2 no-underline" (click)="userService.logout()">Log Out</a>
</div> </div>
</div> </div>

View File

@ -1,35 +1,32 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { MenuItem } from 'primeng/api'; import { MenuItem } from 'primeng/api';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
import { MenubarModule } from 'primeng/menubar'; import { MenubarModule } from 'primeng/menubar';
import { OverlayPanelModule } from 'primeng/overlaypanel'; import { OverlayPanelModule } from 'primeng/overlaypanel';
import { environment } from '../../../environments/environment';
import { UserService } from '../../services/user.service';
import { TabMenuModule } from 'primeng/tabmenu'; import { TabMenuModule } from 'primeng/tabmenu';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { faUserGear } from '@fortawesome/free-solid-svg-icons'; import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { Router } from '@angular/router'; import { environment } from '../../../environments/environment';
import { User } from '../../../../../common-models/src/main.model'; import { UserService } from '../../services/user.service';
@Component({ @Component({
selector: 'header', selector: 'header',
standalone: true, standalone: true,
imports: [CommonModule, MenubarModule, ButtonModule, OverlayPanelModule, TabMenuModule], imports: [CommonModule, MenubarModule, ButtonModule, OverlayPanelModule, TabMenuModule],
templateUrl: './header.component.html', templateUrl: './header.component.html',
styleUrl: './header.component.scss' styleUrl: './header.component.scss',
}) })
export class HeaderComponent { export class HeaderComponent {
public buildVersion = environment.buildVersion; public buildVersion = environment.buildVersion;
user$:Observable<User> user$: Observable<User>;
user: User; user: User;
public tabItems: MenuItem[]; public tabItems: MenuItem[];
public menuItems: MenuItem[]; public menuItems: MenuItem[];
activeItem activeItem;
faUserGear=faUserGear faUserGear = faUserGear;
constructor(public userService: UserService,private router: Router) { constructor(public userService: UserService, private router: Router) {}
}
ngOnInit() { ngOnInit() {
this.user$ = this.userService.getUserObservable(); this.user$ = this.userService.getUserObservable();
@ -43,66 +40,65 @@ export class HeaderComponent {
{ {
label: 'Account', label: 'Account',
icon: 'pi pi-user', icon: 'pi pi-user',
routerLink: `/account/${this.user.id}`, routerLink: `/account`,
visible: this.isUserLogedIn() visible: this.isUserLogedIn(),
}, },
{ {
label: 'Create Listing', label: 'Create Listing',
icon: 'pi pi-plus-circle', icon: 'pi pi-plus-circle',
routerLink: "/createListing", routerLink: '/createBusinessListing',
visible: this.isUserLogedIn() visible: this.isUserLogedIn(),
}, },
{ {
label: 'My Listings', label: 'My Listings',
icon: 'pi pi-list', icon: 'pi pi-list',
routerLink:"/myListings", routerLink: '/myListings',
visible: this.isUserLogedIn() visible: this.isUserLogedIn(),
}, },
{ {
label: 'My Favorites', label: 'My Favorites',
icon: 'pi pi-star', icon: 'pi pi-star',
routerLink:"/myFavorites", routerLink: '/myFavorites',
visible: this.isUserLogedIn() visible: this.isUserLogedIn(),
}, },
{ {
label: 'EMail Us', label: 'EMail Us',
icon: 'fa-regular fa-envelope', icon: 'fa-regular fa-envelope',
routerLink:"/emailUs", routerLink: '/emailUs',
visible: this.isUserLogedIn() visible: this.isUserLogedIn(),
}, },
{ {
label: 'Logout', label: 'Logout',
icon: 'fa-solid fa-right-from-bracket', icon: 'fa-solid fa-right-from-bracket',
routerLink:"/logout", routerLink: '/logout',
visible: this.isUserLogedIn() visible: this.isUserLogedIn(),
}, },
{ {
label: 'Login', label: 'Login',
icon: 'fa-solid fa-right-from-bracket', icon: 'fa-solid fa-right-from-bracket',
//routerLink:"/account",
command: () => this.login(), command: () => this.login(),
visible: !this.isUserLogedIn() visible: !this.isUserLogedIn(),
}, },
] ],
} },
] ];
}); });
this.tabItems = [ this.tabItems = [
{ {
label: 'Businesses for Sale', label: 'Businesses for Sale',
routerLink: '/listings/business', routerLink: '/businessListings',
fragment:'' state: {},
},
{
label: 'Professionals/Brokers Directory',
routerLink: '/listings/professionals_brokers',
fragment:''
}, },
{ {
label: 'Commercial Property', label: 'Commercial Property',
routerLink: '/listings/commercialProperty', routerLink: '/commercialPropertyListings',
fragment:'' state: {},
} },
{
label: 'Professionals/Brokers Directory',
routerLink: '/brokerListings',
state: {},
},
]; ];
this.activeItem = this.tabItems[0]; this.activeItem = this.tabItems[0];

View File

@ -6,9 +6,10 @@ import { HttpEventType } from '@angular/common/http';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FileUpload, FileUploadModule } from 'primeng/fileupload'; import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { KeyValueRatio, User } from '../../../../../common-models/src/main.model'; import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { SharedModule } from '../../shared/shared/shared.module'; import { SharedModule } from '../../shared/shared/shared.module';
import { SelectButtonModule } from 'primeng/selectbutton'; import { SelectButtonModule } from 'primeng/selectbutton';
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
export const stateOptions:KeyValueRatio[]=[ export const stateOptions:KeyValueRatio[]=[
{label:'16/9',value:16/9}, {label:'16/9',value:16/9},
{label:'1/1',value:1}, {label:'1/1',value:1},

View File

@ -0,0 +1,92 @@
<div class="surface-ground h-full">
<div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round">
<div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{ listing?.title }}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
@if(listing){
<div class="grid">
<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 align-self-start">Description</div>
<div class="text-900 w-full md:w-10 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">Category</div>
<div class="text-900 w-full md:w-10">
<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>
</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>
</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>
</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>
</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>
</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>
</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>
</li>
</ul>
@if(listing && user && (user.id===listing?.userId || isAdmin())){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/editBusinessListing', listing.id]"></button>
}
</div>
<div class="col-12 md:col-6">
<div class="surface-card p-4 border-round p-fluid">
<div class="font-medium text-xl text-primary text-900 mb-3">Contact The Author of This Listing</div>
<div class="font-italic text-sm text-900 mb-5">Please Include your contact info below:</div>
<div class="grid formgrid p-fluid">
<div class="field mb-4 col-12 md:col-6">
<label for="name" class="font-medium text-900">Your Name</label>
<input id="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="email" class="font-medium text-900">Your Email</label>
<input id="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email" />
</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" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
<input id="state" type="text" pInputText [(ngModel)]="mailinfo.sender.state" />
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
<div class="field mb-4 col-12">
<label for="notes" class="font-medium text-900">Questions/Comments</label>
<textarea id="notes" pInputTextarea [autoResize]="true" [rows]="5" [(ngModel)]="mailinfo.sender.comments"></textarea>
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
</div>
<button pButton pRipple label="Submit" icon="pi pi-file" class="w-auto" (click)="mail()"></button>
</div>
</div>
</div>
}
</div>
</div>
</div>

View File

@ -0,0 +1,88 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
import { lastValueFrom } from 'rxjs';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ListingsService } from '../../../services/listings.service';
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';
@Component({
selector: 'app-details-business-listing',
standalone: true,
imports: [SharedModule, GalleriaModule],
providers: [MessageService],
templateUrl: './details-business-listing.component.html',
styleUrl: './details-business-listing.component.scss',
})
export class DetailsBusinessListingComponent {
// listings: Array<BusinessListing>;
responsiveOptions = [
{
breakpoint: '1199px',
numVisible: 1,
numScroll: 1,
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1,
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1,
},
];
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
listing: BusinessListing;
criteria: ListingCriteria;
mailinfo: MailInfo;
environment = environment;
user: User;
description: SafeHtml;
constructor(
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
private router: Router,
private userService: UserService,
public selectOptions: SelectOptionsService,
private mailService: MailService,
private messageService: MessageService,
private sanitizer: DomSanitizer,
private location: Location,
) {
this.mailinfo = { sender: {}, userId: '', email: '' };
this.userService.getUserObservable().subscribe(user => {
this.user = user;
});
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
}
async ngOnInit() {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
}
back() {
this.location.back();
}
isAdmin() {
return this.userService.hasAdminRole();
}
async mail() {
this.mailinfo.email = this.user.email;
this.mailinfo.userId = this.listing.userId;
this.mailinfo.listing = this.listing;
await this.mailService.mail(this.mailinfo);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Your message has been sent to the creator of the listing', life: 3000 });
}
}

View File

@ -0,0 +1,89 @@
<div class="surface-ground h-full">
<div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round">
<div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{ listing?.title }}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
@if(listing){
<div class="grid">
<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 align-self-start">Description</div>
<div class="text-900 w-full md:w-10 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>
</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>
</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>
</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>
</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>
</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>
</li>
</ul>
<p-galleria [value]="propertyImages" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
<ng-template pTemplate="item" let-item>
<img src="pictures/property/{{ listing.imagePath }}/{{ item }}" style="width: 100%" />
</ng-template>
</p-galleria>
@if(listing && user && (user.id===listing?.userId || isAdmin())){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/editCommercialPropertyListing', listing.id]"></button>
}
</div>
@if (mailinfo){
<div class="col-12 md:col-6">
<div class="surface-card p-4 border-round p-fluid">
<div class="font-medium text-xl text-primary text-900 mb-3">Contact The Author of This Listing</div>
<div class="font-italic text-sm text-900 mb-5">Please Include your contact info below:</div>
<div class="grid formgrid p-fluid">
<div class="field mb-4 col-12 md:col-6">
<label for="name" class="font-medium text-900">Your Name</label>
<input id="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="email" class="font-medium text-900">Your Email</label>
<input id="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email" />
</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" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
<input id="state" type="text" pInputText [(ngModel)]="mailinfo.sender.state" />
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
<div class="field mb-4 col-12">
<label for="notes" class="font-medium text-900">Questions/Comments</label>
<textarea id="notes" pInputTextarea [autoResize]="true" [rows]="5" [(ngModel)]="mailinfo.sender.comments"></textarea>
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
</div>
<button pButton pRipple label="Submit" icon="pi pi-file" class="w-auto" (click)="mail()"></button>
</div>
</div>
}
</div>
}
</div>
</div>
</div>

View File

@ -1,94 +1,89 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { 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/listings.json';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea';
import { ChipModule } from 'primeng/chip';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service';
import { UserService } from '../../../services/user.service';
import onChange from 'on-change';
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
import { ImageProperty, ListingCriteria, ListingType, MailInfo, User } from '../../../../../../common-models/src/main.model';
import { MailService } from '../../../services/mail.service';
import { MessageService } from 'primeng/api';
import { SharedModule } from '../../../shared/shared/shared.module';
import { GalleriaModule } from 'primeng/galleria';
import { environment } from '../../../../environments/environment';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
import { lastValueFrom } from 'rxjs';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ListingsService } from '../../../services/listings.service';
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';
@Component({ @Component({
selector: 'app-details-listing', selector: 'app-details-commercial-property-listing',
standalone: true, standalone: true,
imports: [SharedModule, GalleriaModule], imports: [SharedModule, GalleriaModule],
providers: [MessageService], providers: [MessageService],
templateUrl: './details-listing.component.html', templateUrl: './details-commercial-property-listing.component.html',
styleUrl: './details-listing.component.scss' styleUrl: './details-commercial-property-listing.component.scss',
}) })
export class DetailsListingComponent { export class DetailsCommercialPropertyListingComponent {
// listings: Array<BusinessListing>; // listings: Array<BusinessListing>;
responsiveOptions = [ responsiveOptions = [
{ {
breakpoint: '1199px', breakpoint: '1199px',
numVisible: 1, numVisible: 1,
numScroll: 1 numScroll: 1,
}, },
{ {
breakpoint: '991px', breakpoint: '991px',
numVisible: 2, numVisible: 2,
numScroll: 1 numScroll: 1,
}, },
{ {
breakpoint: '767px', breakpoint: '767px',
numVisible: 1, numVisible: 1,
numScroll: 1 numScroll: 1,
} },
]; ];
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
private type: 'business'|'commercialProperty' | undefined = this.activatedRoute.snapshot.params['type'] as 'business'|'commercialProperty' | undefined; listing: CommercialPropertyListing;
listing: ListingType; criteria: ListingCriteria;
criteria: ListingCriteria
mailinfo: MailInfo; mailinfo: MailInfo;
propertyImages: ImageProperty[] = [] propertyImages: string[] = [];
environment = environment; environment = environment;
user:User user: User;
description: SafeHtml; description: SafeHtml;
constructor(private activatedRoute: ActivatedRoute, constructor(
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService, private listingsService: ListingsService,
private router: Router, private router: Router,
private userService: UserService, private userService: UserService,
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private mailService: MailService, private mailService: MailService,
private messageService: MessageService, private messageService: MessageService,
private sanitizer: DomSanitizer) { private sanitizer: DomSanitizer,
private location: Location,
) {
this.mailinfo = { sender: {}, userId: '', email: '' };
this.userService.getUserObservable().subscribe(user => { this.userService.getUserObservable().subscribe(user => {
this.user = user this.user = user;
}); });
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler); this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.mailinfo = { sender: {}, userId: '' }
} }
async ngOnInit() { async ngOnInit() {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, this.type)); this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id) this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description); this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
} }
back() { back() {
this.router.navigate(['listings', this.criteria.listingsCategory]) this.location.back();
} }
isAdmin() { isAdmin() {
return this.userService.hasAdminRole(); return this.userService.hasAdminRole();
} }
async mail() { async mail() {
this.mailinfo.email = this.user.email;
this.mailinfo.userId = this.listing.userId; this.mailinfo.userId = this.listing.userId;
this.mailinfo.listing = this.listing;
await this.mailService.mail(this.mailinfo); await this.mailService.mail(this.mailinfo);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Your message has been sent to the creator of the listing', life: 3000 }); this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Your message has been sent to the creator of the listing', life: 3000 });
} }

View File

@ -1,141 +0,0 @@
<div class="surface-ground h-full">
<div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round">
<div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{listing?.title}}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
<div class="grid">
<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">Description</div>
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div>
</li>
@if (listing && (listing.listingsCategory==='business')){
<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">
<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>
</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>
</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>
</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>
</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>
</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>
</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>
</li>
}
@if (listing && (listing.listingsCategory==='commercialProperty')){
<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>
</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>
</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>
</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>
</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>
</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>
</li>
}
</ul>
<p-galleria [value]="propertyImages" [showIndicators]="true" [showThumbnails]="false"
[responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }"
[numVisible]="5">
<ng-template pTemplate="item" let-item>
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{item.name}}"
style="width: 100%;" />
</ng-template>
<!-- <ng-template pTemplate="thumbnail" let-item>
<div class="grid grid-nogutter justify-content-center">
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{item.name}}" />
</div>
</ng-template> -->
</p-galleria>
@if(listing && user && (user.id===listing?.userId || isAdmin())){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto"
[routerLink]="['/editListing',listing.id]"></button>
}
</div>
<div class="col-12 md:col-6">
<div class="surface-card p-4 border-round p-fluid">
<div class="font-medium text-xl text-primary text-900 mb-3">Contact The Author of This Listing
</div>
<div class="font-italic text-sm text-900 mb-5">Please Include your contact info below:</div>
<div class="grid formgrid p-fluid">
<div class="field mb-4 col-12 md:col-6">
<label for="name" class="font-medium text-900">Your Name</label>
<input id="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name">
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="email" class="font-medium text-900">Your Email</label>
<input id="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email">
</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">
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
<input id="state" type="text" pInputText [(ngModel)]="mailinfo.sender.state">
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
<div class="field mb-4 col-12">
<label for="notes" class="font-medium text-900">Questions/Comments</label>
<textarea id="notes" pInputTextarea [autoResize]="true" [rows]="5"
[(ngModel)]="mailinfo.sender.comments"></textarea>
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
</div>
<button pButton pRipple label="Submit" icon="pi pi-file" class="w-auto"
(click)="mail()"></button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
<div class="surface-ground h-full"> <div class="surface-ground h-full">
<div class="px-6 py-5"> <div class="px-6 py-5">
@if (user){
<div class="surface-card p-4 shadow-2 border-round"> <div class="surface-card p-4 shadow-2 border-round">
<!-- <div class="flex justify-content-between align-items-center align-content-center"> <!-- <div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{listing?.title}}</div> <div class="font-medium text-3xl text-900 mb-3">{{listing?.title}}</div>
@ -9,8 +10,7 @@
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between"> <div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between">
<div class="flex align-items-start flex-column md:flex-row"> <div class="flex align-items-start flex-column md:flex-row">
@if(user.hasProfile){ @if(user.hasProfile){
<img src="{{environment.apiBaseUrl}}/profile/{{user.id}}.avif" class="mr-5 mb-3 lg:mb-0" <img src="pictures//profile/{{ user.id }}.avif" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
style="width:90px" />
} @else { } @else {
<img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" /> <img src="assets/images/person_placeholder.jpg" class="mr-5 mb-3 lg:mb-0" style="width: 90px" />
} }
@ -34,8 +34,7 @@
<!-- <span class="font-medium text-500">Logo</span> --> <!-- <span class="font-medium text-500">Logo</span> -->
<div> <div>
@if(user.hasCompanyLogo){ @if(user.hasCompanyLogo){
<img src="{{environment.apiBaseUrl}}/logo/{{user.id}}.avif" <img src="pictures/logo/{{ user.id }}.avif" class="mr-5 lg:mb-0" style="height: 60px; max-width: 100px" />
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" />
} }
<!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png" <!-- <img *ngIf="!user.hasCompanyLogo" src="assets/images/placeholder.png"
class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> --> class="mr-5 lg:mb-0" style="height:60px;max-width:100px" /> -->
@ -44,9 +43,8 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div> </div>
<p class="mt-2 text-700 line-height-3 text-l font-semibold">{{ user.description }}</p> <p class="mt-2 text-700 line-height-3 text-l font-semibold">{{ user.description }}</p>
</div> </div>
@ -86,31 +84,27 @@
<p-tag severity="warning" value="SQL" [rounded]="true"></p-tag> --> <p-tag severity="warning" value="SQL" [rounded]="true"></p-tag> -->
</div> </div>
</li> </li>
<li class="flex align-items-center py-3 px-2 flex-wrap <li class="flex align-items-center py-3 px-2 flex-wrap">
">
<div class="text-500 w-full md:w-2 font-medium">Licensed In</div> <div class="text-500 w-full md:w-2 font-medium">Licensed In</div>
<div class="text-900 w-full md:w-10"> <div class="text-900 w-full md:w-10">
@for (license of user.licensedIn; track license) { @for (license of userLicensedIn; track license) {
<div>{{ license.name }} : {{ license.value }}</div> <div>{{ license.name }} : {{ license.value }}</div>
} }
</div> </div>
</li> </li>
@if(businessListings?.length>0){
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground"> <li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">My Listings For Sale</div> <div class="text-500 w-full md:w-2 font-medium">My Business Listings For Sale</div>
<div class="text-900 w-full md:w-10"> <div class="text-900 w-full md:w-10">
<div class="grid mt-0 mr-0"> <div class="grid mt-0 mr-0">
@for (listing of userListings; track listing) { @for (listing of businessListings; track listing) {
<div class="col-12 md:col-6 cursor-pointer" [routerLink]="['/details-listing/business',listing.id]"> <div class="col-12 md:col-6 cursor-pointer" [routerLink]="['/details-business-listing', listing.id]">
<div class="p-3 border-1 surface-border border-round surface-card"> <div class="p-3 border-1 surface-border border-round surface-card">
<div class="text-900 mb-2"> <div class="text-900 mb-2">
<span [class]="selectOptions.getBgColorType(listing.type)" <span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px">
class="inline-flex border-circle align-items-center justify-content-center mr-3" <i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="pi text-xl"></i>
style="width:38px;height:38px">
<i [class]="selectOptions.getIconAndTextColorType(listing.type)"
class="pi text-xl"></i>
</span> </span>
<span <span class="font-medium">{{ selectOptions.getBusiness(listing.type) }}</span>
class="font-medium">{{selectOptions.getBusiness(listing.type)}}</span>
</div> </div>
<div class="text-700">{{ listing.title }}</div> <div class="text-700">{{ listing.title }}</div>
</div> </div>
@ -119,14 +113,37 @@
</div> </div>
</div> </div>
</li> </li>
} @if(commercialPropListings?.length>0){
<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">My Commercial Property Listings For Sale</div>
<div class="text-900 w-full md:w-10">
<div class="grid mt-0 mr-0">
@for (listing of commercialPropListings; track listing) {
<div class="col-12 md:col-6 cursor-pointer" [routerLink]="['/details-commercial-property-listing', listing.id]">
<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="pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}" class="mr-3" style="width: 45px; height: 45px" />
} @else {
<img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" />
}
<span class="font-medium">{{ selectOptions.getCommercialProperty(listing.type) }}</span>
</div>
<div class="text-700">{{ listing.title }}</div>
</div>
</div>
}
</div>
</div>
</li>
}
</ul> </ul>
</div> </div>
</div> </div>
@if( user?.id===(user$| async)?.id || isAdmin()){ @if( user?.id===(user$| async)?.id || isAdmin()){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" <button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/account']"></button>
[routerLink]="['/account',user.id]"></button>
} }
</div> </div>
}
</div> </div>
</div> </div>

View File

@ -1,16 +1,18 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { SharedModule } from '../../../shared/shared/shared.module'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { GalleriaModule } from 'primeng/galleria';
import { MessageService } from 'primeng/api';
import { BusinessListing, ListingCriteria, ListingType, User } from '../../../../../../common-models/src/main.model';
import { environment } from '../../../../environments/environment';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { UserService } from '../../../services/user.service'; import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeyValue, ListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { UserService } from '../../../services/user.service';
import { ImageService } from '../../../services/image.service'; import { SharedModule } from '../../../shared/shared/shared.module';
@Component({ @Component({
selector: 'app-details-user', selector: 'app-details-user',
@ -18,37 +20,46 @@ import { ImageService } from '../../../services/image.service';
imports: [SharedModule, GalleriaModule], imports: [SharedModule, GalleriaModule],
providers: [MessageService], providers: [MessageService],
templateUrl: './details-user.component.html', templateUrl: './details-user.component.html',
styleUrl: './details-user.component.scss' styleUrl: './details-user.component.scss',
}) })
export class DetailsUserComponent { export class DetailsUserComponent {
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User; user: User;
user$:Observable<User> user$: Observable<User>;
environment = environment; environment = environment;
criteria: ListingCriteria; criteria: ListingCriteria;
userListings:BusinessListing[] businessListings: BusinessListing[];
commercialPropListings: CommercialPropertyListing[];
companyOverview: SafeHtml; companyOverview: SafeHtml;
offeredServices: SafeHtml; offeredServices: SafeHtml;
constructor(private activatedRoute: ActivatedRoute, userLicensedIn: KeyValue[];
constructor(
private activatedRoute: ActivatedRoute,
private router: Router, private router: Router,
private userService: UserService, private userService: UserService,
private listingsService: ListingsService, private listingsService: ListingsService,
private messageService: MessageService, private messageService: MessageService,
public selectOptions: SelectOptionsService, public selectOptions: SelectOptionsService,
private sanitizer: DomSanitizer, private sanitizer: DomSanitizer,
private imageService:ImageService) { private imageService: ImageService,
} private location: Location,
) {}
async ngOnInit() { async ngOnInit() {
this.user = await this.userService.getById(this.id); this.user = await this.userService.getById(this.id);
this.userLicensedIn = this.user.licensedIn.map(l => {
this.userListings = await this.listingsService.getListingByUserId(this.id); return { name: l.split('|')[0], value: l.split('|')[1] };
});
const results = await Promise.all([await this.listingsService.getListingByUserId(this.id, 'business'), await this.listingsService.getListingByUserId(this.id, 'commercialProperty')]);
// Zuweisen der Ergebnisse zu den Member-Variablen der Klasse
this.businessListings = results[0];
this.commercialPropListings = results[1];
this.user$ = this.userService.getUserObservable(); this.user$ = this.userService.getUserObservable();
this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview); this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview);
this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices); this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices);
} }
back() { back() {
this.router.navigate(['listings', this.criteria.listingsCategory]) this.location.back();
} }
isAdmin() { isAdmin() {
return this.userService.hasAdminRole(); return this.userService.hasAdminRole();

View File

@ -1,14 +1,12 @@
<div class="container"> <div class="container">
<div class="wrapper"> <div class="wrapper">
<div class="py-3 px-6 flex align-items-center justify-content-between relative"> <div class="py-3 px-6 flex align-items-center justify-content-between relative">
<a routerLink="/home"><img src="../../../assets/images/header-logo.png" alt="Image" height="50" ></a> <a routerLink="/home"><img src="../../../assets/images/header-logo.png" alt="Image" height="50" /></a>
<div <div class="align-items-center flex-grow-1 justify-content-between hidden lg:flex absolute lg:static w-full left-0 top-100 px-6 lg:px-0 shadow-2 lg:shadow-none z-2">
class="align-items-center flex-grow-1 justify-content-between hidden lg:flex absolute lg:static w-full left-0 top-100 px-6 lg:px-0 shadow-2 lg:shadow-none z-2">
<section></section> <section></section>
<div <div class="flex justify-content-between lg:block border-top-1 lg:border-top-none border-gray-800 py-3 lg:py-0 mt-3 lg:mt-0">
class="flex justify-content-between lg:block border-top-1 lg:border-top-none border-gray-800 py-3 lg:py-0 mt-3 lg:mt-0">
@if(userService.isLoggedIn()){ @if(userService.isLoggedIn()){
<p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" [routerLink]="['/account',(user$|async)?.id]"></p-button> <p-button label="Account" class="ml-3 font-bold" [outlined]="true" severity="secondary" [routerLink]="['/account']"></p-button>
} @else { } @else {
<p-button label="Log In" class="ml-3 font-bold" [outlined]="true" severity="secondary" (click)="login()"></p-button> <p-button label="Log In" class="ml-3 font-bold" [outlined]="true" severity="secondary" (click)="login()"></p-button>
} }
@ -19,55 +17,64 @@
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="w-12 lg:w-6 p-4"> <div class="w-12 lg:w-6 p-4">
<h1 class="text-6xl font-bold text-blue-900 mt-0 mb-3">Find businesses for sale</h1> <h1 class="text-6xl font-bold text-blue-900 mt-0 mb-3">Find businesses for sale</h1>
<p class="text-3xl text-blue-600 mt-0 mb-5">Arcu cursus euismod quis viverra nibh cras. Amet justo <p class="text-3xl text-blue-600 mt-0 mb-5">Unlocking Exclusive Opportunities, Empowering Entrepreneurial Dreams</p>
donec
enim diam vulputate ut.</p>
<ul class="list-none p-0 m-0"> <ul class="list-none p-0 m-0">
<li class="mb-3 flex align-items-center"><i <li class="mb-3 flex align-items-center"><i class="pi pi-compass text-yellow-500 text-xl mr-2"></i><span class="text-blue-600 line-height-3">Texas expertise and nationwide presence</span></li>
class="pi pi-compass text-yellow-500 text-xl mr-2"></i><span <li class="mb-3 flex align-items-center"><i class="pi pi-map text-yellow-500 text-xl mr-2"></i><span class="text-blue-600 line-height-3">Industry diversity</span></li>
class="text-blue-600 line-height-3">Senectus et netus et malesuada fames.</span></li> <li class="mb-3 flex align-items-center"><i class="pi pi-calendar text-yellow-500 text-xl mr-2"></i><span class="text-blue-600 line-height-3">Support throughout the entire process</span></li>
<li class="mb-3 flex align-items-center"><i
class="pi pi-map text-yellow-500 text-xl mr-2"></i><span
class="text-blue-600 line-height-3">Orci a scelerisque purus semper eget.</span></li>
<li class="mb-3 flex align-items-center"><i
class="pi pi-calendar text-yellow-500 text-xl mr-2"></i><span
class="text-blue-600 line-height-3">Aenean sed adipiscing diam donec adipiscing
tristique.</span></li>
</ul> </ul>
</div> </div>
<div class="w-12 lg:w-6 text-center lg:text-right flex"> <div class="w-12 lg:w-6 text-center lg:text-right flex">
<div class="mt-5"> <div class="mt-5">
<ul class="flex flex-column align-items-left gap-3 px-2 py-3 list-none surface-border"> <ul class="flex flex-column align-items-left gap-3 px-2 py-3 list-none surface-border">
<li><button pButton pRipple icon="pi pi-user" (click)="activeTabAction = 'business'" <li><button pButton pRipple icon="pi pi-user" (click)="changeTab('business')" label="Businesses" [ngClass]="{ 'p-button-text text-700': activeTabAction !== 'business' }"></button></li>
label="Businesses" <li>
[ngClass]="{'p-button-text text-700': activeTabAction !== 'business'}"></button></li> <button
<li><button pButton pRipple icon="pi pi-globe" (click)="activeTabAction = 'professionals_brokers'" pButton
label="Professionals/Brokers Directory" pRipple
[ngClass]="{'p-button-text text-700': activeTabAction != 'professionals_brokers'}"></button></li> icon="pi pi-shield"
<li><button pButton pRipple icon="pi pi-shield" (click)="activeTabAction = 'commercialProperty'" (click)="changeTab('commercialProperty')"
label="Commercial Property" label="Commercial Property"
[ngClass]="{'p-button-text text-700': activeTabAction != 'commercialProperty'}"></button> [ngClass]="{ 'p-button-text text-700': activeTabAction != 'commercialProperty' }"
></button>
</li>
<li>
<button pButton pRipple icon="pi pi-globe" (click)="changeTab('broker')" label="Professionals/Brokers Directory" [ngClass]="{ 'p-button-text text-700': activeTabAction != 'broker' }"></button>
</li> </li>
</ul> </ul>
</div> </div>
<div class="mt-5"> <div [ngClass]="{ 'mt-5': activeTabAction === 'business', 'mt-11': activeTabAction === 'commercialProperty', 'mt-22': activeTabAction === 'broker' }">
<div class="flex flex-column align-items-right gap-3 px-2 py-3 my-3 surface-border"> <div class="flex flex-column align-items-right gap-3 px-2 py-3 my-3 surface-border">
<p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name" <p-dropdown [options]="states" [(ngModel)]="criteria.state" optionLabel="name" optionValue="value" [showClear]="true" placeholder="State" [style]="{ width: '200px' }"></p-dropdown>
optionValue="value" [showClear]="true" placeholder="Category" @if(activeTabAction === 'business'){
[style]="{ width: '200px'}"></p-dropdown> <p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Category" [style]="{ width: '200px' }"></p-dropdown>
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name" optionValue="value" } @if(activeTabAction === 'commercialProperty'){
[showClear]="true" placeholder="Min Price" [style]="{ width: '200px'}"></p-dropdown> <p-dropdown
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name" optionValue="value" [options]="selectOptions.typesOfCommercialProperty"
[showClear]="true" placeholder="Max Price" [style]="{ width: '200px'}"></p-dropdown> [(ngModel)]="criteria.type"
<button pButton pRipple label="Find" class="ml-3 font-bold" optionLabel="name"
[style]="{ width: '170px'}" (click)="search()"></button> optionValue="value"
[showClear]="true"
placeholder="Category"
[style]="{ width: '200px' }"
></p-dropdown>
} @if(activeTabAction === 'business' || activeTabAction === 'commercialProperty'){
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Min Price" [style]="{ width: '200px' }"></p-dropdown>
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Max Price" [style]="{ width: '200px' }"></p-dropdown>
}
<button pButton pRipple label="Find" class="ml-3 font-bold" [style]="{ width: '200px' }" (click)="search()"></button>
</div> </div>
</div> </div>
</div> </div>
<div class="w-12 flex justify-content-center"> <div class="w-12 flex justify-content-center">
<button type="button" pButton pRipple label="Create Your Listing" <button
class="block mt-7 mb-7 lg:mb-0 p-button-rounded p-button-success p-button-lg font-medium" [routerLink]="userService.isLoggedIn()?'/createListing':'/pricing'"></button> type="button"
pButton
pRipple
label="Create Your Listing"
class="block mt-7 mb-7 lg:mb-0 p-button-rounded p-button-success p-button-lg font-medium"
[routerLink]="userService.isLoggedIn() ? '/createBusinessListing' : '/pricing'"
></button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,10 +1,11 @@
:host { :host {
height: 100% height: 100%;
} }
.container { .container {
background-image: url(../../../assets/images/index-bg.webp); background-image: url(../../../assets/images/index-bg.webp);
//background-image: url(../../../assets/images/corpusChristiSkyline.jpg); // background-image: url(../../../assets/images/1_Version.jpg);
//background-image: url(../../../assets/images/2_1_Version.jpg);
background-size: cover; background-size: cover;
background-position: center; background-position: center;
height: 100vh; height: 100vh;
@ -13,5 +14,11 @@
width: 200px; width: 200px;
} }
.p-button-white { .p-button-white {
color:aliceblue color: aliceblue;
}
.mt-11 {
margin-top: 5.9rem !important;
}
.mt-22 {
margin-top: 9.7rem !important;
} }

View File

@ -1,45 +1,62 @@
import { Component } from '@angular/core';
import { DropdownModule } from 'primeng/dropdown';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { BrowserAnimationsModule, NoopAnimationsModule } from '@angular/platform-browser/animations'; import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { StyleClassModule } from 'primeng/styleclass'; import onChange from 'on-change';
import { ButtonModule } from 'primeng/button'; import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox'; import { CheckboxModule } from 'primeng/checkbox';
import { DropdownModule } from 'primeng/dropdown';
import { InputTextModule } from 'primeng/inputtext'; import { InputTextModule } from 'primeng/inputtext';
import { StyleClassModule } from 'primeng/styleclass';
import { Observable } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { ListingsService } from '../../services/listings.service';
import { SelectOptionsService } from '../../services/select-options.service'; import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service'; import { UserService } from '../../services/user.service';
import onChange from 'on-change'; import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../utils/utils';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
import { ListingCriteria, User } from '../../../../../common-models/src/main.model';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, RouterModule], imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, RouterModule],
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.scss' styleUrl: './home.component.scss',
}) })
export class HomeComponent { export class HomeComponent {
activeTabAction = 'business'; activeTabAction: 'business' | 'commercialProperty' | 'broker' = 'business';
type: string; type: string;
maxPrice: string; maxPrice: string;
minPrice: string; minPrice: string;
criteria:ListingCriteria criteria: ListingCriteria;
user$:Observable<User> user$: Observable<User>;
public constructor(private router: Router,private activatedRoute: ActivatedRoute, public selectOptions:SelectOptionsService, public userService:UserService) { states = [];
public constructor(private router: Router, private activatedRoute: ActivatedRoute, public selectOptions: SelectOptionsService, public userService: UserService, private listingsService: ListingsService) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler); this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
resetCriteria(this.criteria);
} }
ngOnInit(){ async ngOnInit() {
this.user$ = this.userService.getUserObservable(); this.user$ = this.userService.getUserObservable();
if (this.activeTabAction === 'business' || this.activeTabAction === 'commercialProperty') {
const statesResult = await this.listingsService.getAllStates(this.activeTabAction);
this.states = statesResult.map(s => s.state).map(ls => ({ name: this.selectOptions.getState(ls as string), value: ls }));
} else {
this.states = [];
}
}
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
this.activeTabAction = tabname;
if (this.activeTabAction === 'business' || this.activeTabAction === 'commercialProperty') {
const statesResult = await this.listingsService.getAllStates(this.activeTabAction);
this.states = statesResult.map(s => s.state).map(ls => ({ name: this.selectOptions.getState(ls as string), value: ls }));
} else {
this.states = this.selectOptions.states;
}
} }
search() { search() {
this.router.navigate([`listings/${this.activeTabAction}`]) const data = { keep: true };
this.router.navigate([`${this.activeTabAction}Listings`]);
} }
login() { login() {
this.userService.login(window.location.href); this.userService.login(window.location.href);
} }

View File

@ -0,0 +1,73 @@
<div id="sky-line" class="hidden-lg-down"></div>
<div class="search">
<div class="wrapper">
<div class="grid p-4 align-items-center">
<div class="col-2">
<p-dropdown
[filter]="true"
filterBy="name"
[options]="selectOptions.states"
[(ngModel)]="criteria.state"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="Location"
[style]="{ width: '100%' }"
>
<ng-template let-state pTemplate="item">
<div class="flex align-items-center gap-2">
<div>{{ state.name }}</div>
</div>
</ng-template>
</p-dropdown>
</div>
<div class="col-2">
<p-inputGroup>
<input id="name" type="text" pInputText [(ngModel)]="criteria.name" placeholder="Name" />
<button type="button" pButton icon="pi pi-times" class="p-button-secondary" (click)="reset()"></button>
</p-inputGroup>
</div>
<div class="col-1 col-offset-7">
<p-button label="Refine" (click)="refine()"></p-button>
</div>
</div>
</div>
</div>
<div class="surface-200 h-full">
<div class="wrapper">
<div class="grid">
@for (user of users; track user) {
<div class="col-12 lg:col-6 xl:col-4 p-4 flex flex-column">
<div class="surface-card shadow-2 p-2 flex flex-column flex-grow-1 justify-content-between" style="border-radius: 10px">
<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="pictures/profile/{{ user.id }}.avif" class="w-5rem" />
} @else {
<img src="assets/images/person_placeholder.jpg" class="w-5rem" />
}
</span>
<div class="flex flex-column align-items-center md:align-items-stretch ml-4 mt-4 md:mt-0">
<p class="mt-0 mb-3 line-height-3 text-center md:text-left">{{ user.description }}</p>
<span class="text-900 font-medium mb-1 mt-auto">{{ user.firstname }} {{ user.lastname }}</span>
<div class="text-600 text-sm">{{ user.companyName }}</div>
</div>
</div>
<div class="px-4 py-3 text-right flex justify-content-between align-items-center">
@if(user.hasCompanyLogo){
<img src="pictures/logo/{{ user.id }}.avif" class="rounded-image" />
} @else {
<img src="assets/images/placeholder.png" class="rounded-image" />
}
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full profile" class="p-button-rounded p-button-success" [routerLink]="['/details-user', user.id]"></button>
</div>
</div>
</div>
}
</div>
<div class="mb-2 surface-200 flex align-items-center justify-content-center">
<div class="mx-1 text-color">Total number of Professionals/Brokers: {{ totalRecords }}</div>
<p-paginator (onPageChange)="onPageChange($event)" [first]="first" [rows]="rows" [totalRecords]="totalRecords" [rowsPerPageOptions]="[12, 24, 48]"></p-paginator>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
#sky-line { #sky-line {
background-image: url(../../../assets/images/bw-sky.jpg); background-image: url(../../../../assets/images/bw-sky.jpg);
height: 204px; height: 204px;
background-position: bottom; background-position: bottom;
background-size: cover; background-size: cover;

View File

@ -0,0 +1,111 @@
import { CommonModule, NgOptimizedImage } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import onChange from 'on-change';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { DropdownModule } from 'primeng/dropdown';
import { InputGroupModule } from 'primeng/inputgroup';
import { InputTextModule } from 'primeng/inputtext';
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 { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../../utils/utils';
@Component({
selector: 'app-broker-listings',
standalone: true,
imports: [
CommonModule,
StyleClassModule,
ButtonModule,
CheckboxModule,
InputTextModule,
DropdownModule,
FormsModule,
StyleClassModule,
ToggleButtonModule,
RouterModule,
PaginatorModule,
InputGroupModule,
NgOptimizedImage,
],
templateUrl: './broker-listings.component.html',
styleUrl: './broker-listings.component.scss',
})
export class BrokerListingsComponent {
environment = environment;
listings: Array<BusinessListing>;
users: Array<User>;
filteredListings: Array<ListingType>;
criteria: ListingCriteria;
realEstateChecked: boolean;
maxPrice: string;
minPrice: string;
type: string;
states = [];
statesSet = new Set();
state: string;
first: number = 0;
rows: number = 12;
totalRecords: number = 0;
ts = new Date().getTime();
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
constructor(
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,
private userService: UserService,
private activatedRoute: ActivatedRoute,
private router: Router,
private cdRef: ChangeDetectorRef,
private imageService: ImageService,
private route: ActivatedRoute,
) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.criteria.type = undefined;
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
resetCriteria(this.criteria);
}
this.init();
});
}
async ngOnInit() {
const statesResult = await this.listingsService.getAllStates('business');
this.states = statesResult.map(ls => ({ name: this.selectOptions.getState(ls.state as string), value: ls.state, count: ls.count }));
}
async init() {
this.search();
}
refine() {
this.criteria.start = 0;
this.criteria.page = 0;
this.search();
}
async search() {
const usersReponse = await this.userService.search(this.criteria);
this.users = usersReponse.data;
this.totalRecords = usersReponse.total;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(event: any) {
this.criteria.start = event.first;
this.criteria.length = event.rows;
this.criteria.page = event.page;
this.criteria.pageCount = event.pageCount;
this.search();
}
reset() {
this.criteria.name = '';
}
}

View File

@ -0,0 +1,95 @@
<div id="sky-line" class="hidden-lg-down"></div>
<div class="search">
<div class="wrapper">
<div class="grid p-4 align-items-center">
<div class="col-2">
<p-dropdown
[filter]="true"
filterBy="name"
[options]="states"
[(ngModel)]="criteria.state"
optionLabel="criteria.location"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="State"
[style]="{ width: '100%' }"
>
<ng-template let-state pTemplate="item">
<div class="flex align-items-center gap-2">
<div>{{ state.name }} ({{ state.count }})</div>
</div>
</ng-template>
</p-dropdown>
</div>
<div class="col-2">
<p-dropdown
[filter]="true"
filterBy="name"
[options]="selectOptions.typesOfBusiness"
[(ngModel)]="criteria.type"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="Categorie of Business"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Min Price" [style]="{ width: '100%' }"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Max Price" [style]="{ width: '100%' }"></p-dropdown>
</div>
<div class="col-2">
<p-inputGroup>
<input id="name" type="text" pInputText [(ngModel)]="criteria.title" placeholder="Title" />
<button type="button" pButton icon="pi pi-times" class="p-button-secondary" (click)="reset()"></button>
</p-inputGroup>
</div>
<div class="col-1" pTooltip="Real Estate excluded/included" tooltipPosition="top">
<!-- <p-toggleButton [(ngModel)]="checked1" onLabel="Sustainable" offLabel="Unsustainable" onIcon="pi pi-check" offIcon="pi pi-times" styleClass="mb-3 lg:mt-0 mr-4 flex-shrink-0 w-12rem"></p-toggleButton> -->
<p-toggleButton [(ngModel)]="criteria.realEstateChecked" onLabel="RE incl." offLabel="RE excl."></p-toggleButton>
</div>
<div class="col-1">
<p-button label="Refine" (click)="refine()"></p-button>
</div>
</div>
</div>
</div>
<div class="surface-200 h-full">
<div class="wrapper">
<div class="grid">
@for (listing of listings; track listing.id) {
<div class="col-12 lg:col-3 p-3">
<div class="shadow-2 border-round surface-card mb-3 h-full flex-column justify-content-between flex">
<div class="p-4 h-full flex flex-column">
<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>
</span>
<span class="text-900 font-medium text-2xl">{{ selectOptions.getBusiness(listing.type) }}</span>
</div>
<div class="text-900 my-3 text-xl font-medium">{{ listing.title }}</div>
<p class="mt-0 mb-1 text-700 line-height-3">Asking price: {{ listing.price | currency }}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Sales revenue: {{ listing.salesRevenue | currency }}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Net profit: {{ listing.cashFlow | currency }}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Location: {{ selectOptions.getState(listing.state) }}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{ listing.established }}</p>
<div class="mt-auto ml-auto">
<img src="pictures/logo/{{ listing.userId }}.avif" (error)="imageErrorHandler(listing)" class="rounded-image" />
</div>
</div>
<div class="px-4 py-3 surface-100 text-left">
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full Listing" class="p-button-rounded p-button-success" [routerLink]="['/details-business-listing', listing.id]"></button>
</div>
</div>
</div>
}
</div>
<div class="mb-2 surface-200 flex align-items-center justify-content-center">
<div class="mx-1 text-color">Total number of Listings: {{ totalRecords }}</div>
<p-paginator (onPageChange)="onPageChange($event)" [first]="first" [rows]="rows" [totalRecords]="totalRecords" [rowsPerPageOptions]="[12, 24, 48]"></p-paginator>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,110 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import onChange from 'on-change';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { DropdownModule } from 'primeng/dropdown';
import { InputGroupModule } from 'primeng/inputgroup';
import { InputTextModule } from 'primeng/inputtext';
import { PaginatorModule } from 'primeng/paginator';
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 { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../../utils/utils';
@Component({
selector: 'app-business-listings',
standalone: true,
imports: [
CommonModule,
StyleClassModule,
ButtonModule,
CheckboxModule,
InputTextModule,
DropdownModule,
FormsModule,
StyleClassModule,
ToggleButtonModule,
RouterModule,
PaginatorModule,
InputGroupModule,
TooltipModule,
],
templateUrl: './business-listings.component.html',
styleUrl: './business-listings.component.scss',
})
export class BusinessListingsComponent {
environment = environment;
listings: Array<BusinessListing>;
filteredListings: Array<BusinessListing>;
criteria: ListingCriteria;
realEstateChecked: boolean;
maxPrice: string;
minPrice: string;
type: string;
states = [];
state: string;
totalRecords: number = 0;
ts = new Date().getTime();
first: number = 0;
rows: number = 12;
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
constructor(
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,
private activatedRoute: ActivatedRoute,
private router: Router,
private cdRef: ChangeDetectorRef,
private imageService: ImageService,
private route: ActivatedRoute,
) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.criteria.type = undefined;
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
resetCriteria(this.criteria);
}
this.init();
});
}
async ngOnInit() {}
async init() {
const statesResult = await this.listingsService.getAllStates('business');
this.states = statesResult.map(ls => ({ name: this.selectOptions.getState(ls.state as string), value: ls.state, count: ls.count }));
this.search();
}
refine() {
this.criteria.start = 0;
this.criteria.page = 0;
this.search();
}
async search() {
const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
this.listings = listingReponse.data;
this.totalRecords = listingReponse.total;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(event: any) {
this.criteria.start = event.first;
this.criteria.length = event.rows;
this.criteria.page = event.page;
this.criteria.pageCount = event.pageCount;
this.search();
}
imageErrorHandler(listing: ListingType) {
// listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann
}
reset() {
this.criteria.title = null;
}
}

View File

@ -0,0 +1,101 @@
<div id="sky-line" class="hidden-lg-down"></div>
<div class="search">
<div class="wrapper">
<div class="grid p-4 align-items-center">
<div class="col-2">
<p-dropdown [filter]="true" filterBy="name" [options]="states" [(ngModel)]="criteria.state" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Location" [style]="{ width: '100%' }">
<ng-template let-state pTemplate="item">
<div class="flex align-items-center gap-2">
<div>{{ state.name }} ({{ state.count }})</div>
</div>
</ng-template>
</p-dropdown>
</div>
<div class="col-2">
<p-dropdown
[filter]="true"
filterBy="name"
[options]="selectOptions.typesOfCommercialProperty"
[(ngModel)]="criteria.type"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="Categorie of Property"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Min Price" [style]="{ width: '100%' }"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Max Price" [style]="{ width: '100%' }"></p-dropdown>
</div>
<div class="col-2">
<p-inputGroup>
<input id="name" type="text" pInputText [(ngModel)]="criteria.title" placeholder="Title" />
<button type="button" pButton icon="pi pi-times" class="p-button-secondary" (click)="reset()"></button>
</p-inputGroup>
</div>
<div class="col-1 col-offset-1">
<p-button label="Refine" (click)="refine()"></p-button>
</div>
</div>
</div>
</div>
<div class="surface-200 h-full">
<div class="wrapper">
<div class="grid">
@for (listing of listings; track listing.id) {
<div class="col-12 xl:col-4 flex">
<div class="surface-card p-2 flex flex-column flex-grow-1 justify-content-between" style="border-radius: 10px">
<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="pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}" 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" />
}
<p class="absolute px-2 py-1 border-round-lg text-sm font-normal text-white mt-0 mb-0" style="background-color: rgba(255, 255, 255, 0.3); backdrop-filter: invert(30%); top: 3%; left: 3%">
{{ selectOptions.getState(listing.state) }}
</p>
</div>
<div class="flex flex-column w-full gap-3">
<div class="flex w-full justify-content-between align-items-center flex-wrap gap-3">
<p class="font-semibold text-lg mt-0 mb-0">{{ listing.title }}</p>
<!-- <p-rating [ngModel]="val1" readonly="true" stars="5" [cancel]="false" ngClass="flex-shrink-0"></p-rating> -->
</div>
<p class="font-normal text-lg text-600 mt-0 mb-0">{{ listing.city }}</p>
<div class="flex flex-wrap justify-content-between xl:h-2rem mt-auto">
<p class="text-base flex align-items-center text-900 mt-0 mb-1">
<i class="pi pi-list mr-2"></i>
<span class="font-medium">{{ selectOptions.getCommercialProperty(listing.type) }}</span>
</p>
</div>
<p class="font-semibold text-3xl text-900 mt-0 mb-2">{{ listing.price | currency }}</p>
</div>
</article>
<div class="px-4 py-3 text-left">
<button
pButton
pRipple
icon="pi pi-arrow-right"
iconPos="right"
label="View Full Listing"
class="p-button-rounded p-button-success"
[routerLink]="['/details-commercial-property-listing', listing.id]"
></button>
</div>
</div>
</div>
}
</div>
<div class="mb-2 surface-200 flex align-items-center justify-content-center">
<!-- @if(listings && listings.length>12){ -->
<div class="mx-1 text-color">Total number of Listings: {{ totalRecords }}</div>
<p-paginator (onPageChange)="onPageChange($event)" [first]="first" [rows]="rows" [totalRecords]="totalRecords" [rowsPerPageOptions]="[12, 24, 48]"></p-paginator>
<!-- } -->
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,92 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import onChange from 'on-change';
import { ButtonModule } from 'primeng/button';
import { CheckboxModule } from 'primeng/checkbox';
import { DropdownModule } from 'primeng/dropdown';
import { InputGroupModule } from 'primeng/inputgroup';
import { InputTextModule } from 'primeng/inputtext';
import { PaginatorModule } from 'primeng/paginator';
import { StyleClassModule } from 'primeng/styleclass';
import { ToggleButtonModule } from 'primeng/togglebutton';
import { CommercialPropertyListing } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { getCriteriaStateObject, getSessionStorageHandler, resetCriteria } from '../../../utils/utils';
@Component({
selector: 'app-commercial-property-listings',
standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, StyleClassModule, ToggleButtonModule, RouterModule, PaginatorModule, InputGroupModule],
templateUrl: './commercial-property-listings.component.html',
styleUrl: './commercial-property-listings.component.scss',
})
export class CommercialPropertyListingsComponent {
environment = environment;
listings: Array<CommercialPropertyListing>;
filteredListings: Array<CommercialPropertyListing>;
criteria: ListingCriteria;
realEstateChecked: boolean;
first: number = 0;
rows: number = 12;
maxPrice: string;
minPrice: string;
type: string;
states = [];
statesSet = new Set();
state: string;
totalRecords: number = 0;
ts = new Date().getTime();
constructor(
public selectOptions: SelectOptionsService,
private listingsService: ListingsService,
private activatedRoute: ActivatedRoute,
private router: Router,
private cdRef: ChangeDetectorRef,
private imageService: ImageService,
private route: ActivatedRoute,
) {
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
this.criteria.type = undefined;
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
resetCriteria(this.criteria);
}
this.init();
});
}
async ngOnInit() {}
async init() {
const statesResult = await this.listingsService.getAllStates('commercialProperty');
this.states = statesResult.map(ls => ({ name: this.selectOptions.getState(ls.state as string), value: ls.state, count: ls.count }));
this.search();
}
refine() {
this.criteria.start = 0;
this.criteria.page = 0;
this.search();
}
async search() {
const listingReponse = await this.listingsService.getListings(this.criteria, 'commercialProperty');
this.listings = listingReponse.data;
this.totalRecords = listingReponse.total;
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(event: any) {
this.criteria.start = event.first;
this.criteria.length = event.rows;
this.criteria.page = event.page;
this.criteria.pageCount = event.pageCount;
this.search();
}
reset() {
this.criteria.title = null;
}
}

View File

@ -1,162 +0,0 @@
<div id="sky-line" class="hidden-lg-down">
</div>
<div class="search">
<div class="wrapper">
<div class="grid p-4 align-items-center">
@if (category==='business'){
<div class="col-2">
<p-dropdown [options]="selectOptions.typesOfBusiness" [(ngModel)]="criteria.type" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Categorie of Business"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="states" [(ngModel)]="state" optionLabel="criteria.location" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="State" [style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.minPrice" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Min Price"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="selectOptions.prices" [(ngModel)]="criteria.maxPrice" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Max Price"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-3">
<!-- <p-toggleButton [(ngModel)]="checked1" onLabel="Sustainable" offLabel="Unsustainable" onIcon="pi pi-check" offIcon="pi pi-times" styleClass="mb-3 lg:mt-0 mr-4 flex-shrink-0 w-12rem"></p-toggleButton> -->
<p-toggleButton [(ngModel)]="criteria.realEstateChecked" onLabel="Real Estate not included"
offLabel="Real Estate included"></p-toggleButton>
</div>
}
@if (category==='commercialProperty'){
<div class="col-2">
<p-dropdown [options]="states" [(ngModel)]="criteria.state" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="Location" [style]="{ width: '100%'}"></p-dropdown>
</div>
}
<!-- @if (listingCategory==='professionals_brokers'){ -->
<!-- <div class="col-2">
<p-dropdown [options]="selectOptions.categories" [(ngModel)]="criteria.category" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Category"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="col-2">
<p-dropdown [options]="states" [(ngModel)]="criteria.state" optionLabel="name" optionValue="value"
[showClear]="true" placeholder="Location" [style]="{ width: '100%'}"></p-dropdown>
</div> -->
<!-- } -->
<div [ngClass]="{'col-offset-9':type==='commercialProperty','col-offset-7':type==='professionals_brokers'}" class="col-1">
<p-button label="Refine" (click)="search()"></p-button>
</div>
</div>
</div>
</div>
<div class="surface-200 h-full">
<div class="wrapper">
<div class="grid">
@for (listing of filteredListings; track listing.id) {
<div *ngIf="listing.listingsCategory==='business'" class="col-12 lg:col-3 p-3">
<div class="shadow-2 border-round surface-card mb-3 h-full flex-column justify-content-between flex">
<div class="p-4 h-full flex flex-column">
<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>
</span>
<span class="text-900 font-medium text-2xl">{{selectOptions.getBusiness(listing.type)}}</span>
</div>
<div class="text-900 my-3 text-xl font-medium">{{listing.title}}</div>
<p class="mt-0 mb-1 text-700 line-height-3">Asking price: {{listing.price | currency}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Sales revenue: {{listing.salesRevenue | currency}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Net profit: {{listing.cashFlow | currency}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Location: {{selectOptions.getState(listing.state)}}</p>
<p class="mt-0 mb-1 text-700 line-height-3">Established: {{listing.established}}</p>
<div class="mt-auto ml-auto">
<img *ngIf="!listing.hideImage" src="{{environment.apiBaseUrl}}/logo/{{listing.userId}}" (error)="imageErrorHandler(listing)" class="rounded-image"/>
</div>
</div>
<div class="px-4 py-3 surface-100 text-left">
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full Listing"
class="p-button-rounded p-button-success" [routerLink]="['/details-listing/business',listing.id]"></button>
</div>
</div>
</div>
}
@for (listing of filteredListings; track listing.id) {
<div *ngIf="listing.listingsCategory==='commercialProperty'" class="col-12 xl:col-4 flex">
<div class="surface-card p-2 flex flex-column flex-grow-1 justify-content-between" style="border-radius: 10px">
<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="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{listing.imageOrder[0].name}}" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem">
} @else {
<!-- <img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{listing.imageOrder[0].name}}" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem"> -->
<img src="assets/images/placeholder_properties.jpg" alt="Image" class="border-round w-full h-full md:w-12rem md:h-9rem" />
}
<p class="absolute px-2 py-1 border-round-lg text-sm font-normal text-white mt-0 mb-0" style="background-color: rgba(255, 255, 255, 0.3); backdrop-filter: invert(30%);; top: 3%; left: 3%;">{{selectOptions.getState(listing.state)}}</p>
</div>
<div class="flex flex-column w-full gap-3">
<div class="flex w-full justify-content-between align-items-center flex-wrap gap-3">
<p class="font-semibold text-lg mt-0 mb-0">{{listing.title}}</p>
<!-- <p-rating [ngModel]="val1" readonly="true" stars="5" [cancel]="false" ngClass="flex-shrink-0"></p-rating> -->
</div>
<p class="font-normal text-lg text-600 mt-0 mb-0">{{listing.city}}</p>
<div class="flex flex-wrap justify-content-between xl:h-2rem mt-auto">
<p class="text-base flex align-items-center text-900 mt-0 mb-1">
<i class="pi pi-list mr-2"></i>
<span class="font-medium">{{selectOptions.getCommercialProperty(listing.type)}}</span>
</p>
</div>
<p class="font-semibold text-3xl text-900 mt-0 mb-2">{{listing.price | currency}}</p>
</div>
</article>
<div class="px-4 py-3 text-left ">
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full Listing"
class="p-button-rounded p-button-success" [routerLink]="['/details-listing/commercialProperty',listing.id]"></button>
</div>
</div>
</div>
}
@for (user of users; track user.id) {
<div class="col-12 lg:col-6 xl:col-4 p-4 flex flex-column flex-grow-1">
<div class="surface-card shadow-2 p-2 flex flex-column flex-grow-1 justify-content-between" style="border-radius: 10px">
<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="{{environment.apiBaseUrl}}/profile/{{user.id}}.avif?_ts={{ts}}" class="w-5rem" />
} @else {
<img src="assets/images/person_placeholder.jpg" class="w-5rem" />
}
</span>
<div class="flex flex-column align-items-center md:align-items-stretch ml-4 mt-4 md:mt-0">
<p class="mt-0 mb-3 line-height-3 text-center md:text-left">{{user.description}}</p>
<span class="text-900 font-medium mb-1 mt-auto">{{user.firstname}} {{user.lastname}}</span>
<div class="text-600 text-sm">{{user.companyName}}</div>
</div>
</div>
<div class="px-4 py-3 text-right flex justify-content-between align-items-center">
@if(user.hasCompanyLogo){
<img src="{{environment.apiBaseUrl}}/logo/{{user.id}}.avif" class="rounded-image"/>
} @else {
<img src="assets/images/placeholder.png" class="rounded-image"/>
}
<button pButton pRipple icon="pi pi-arrow-right" iconPos="right" label="View Full profile"
class="p-button-rounded p-button-success" [routerLink]="['/details-user',user.id]"></button>
</div>
</div>
</div>
}
</div>
<div class="mb-2 surface-200 flex align-items-center justify-content-center">
<div class="mx-1 text-color">Total number of Listings: {{totalRecords}}</div>
<p-paginator (onPageChange)="onPageChange($event)" [first]="first" [rows]="rows" [totalRecords]="totalRecords" [rowsPerPageOptions]="[12, 24, 48]" ></p-paginator>
</div>
</div>
</div>

View File

@ -1,123 +0,0 @@
import { ChangeDetectorRef, 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 { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { ListingsService } from '../../services/listings.service';
import { Observable, lastValueFrom } from 'rxjs';
import { PaginatorModule } from 'primeng/paginator';
import onChange from 'on-change';
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
import { InitEditableRow } from 'primeng/table';
import { environment } from '../../../environments/environment';
import { ListingCriteria, ListingType, User } from '../../../../../common-models/src/main.model';
import { UserService } from '../../services/user.service';
import { ImageService } from '../../services/image.service';
@Component({
selector: 'app-listings',
standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, StyleClassModule, ToggleButtonModule, RouterModule, PaginatorModule],
templateUrl: './listings.component.html',
styleUrls: ['./listings.component.scss', '../pages.scss']
})
export class ListingsComponent {
environment=environment;
listings: Array<ListingType>;
users: Array<User>
filteredListings: Array<ListingType>;
criteria:ListingCriteria;
realEstateChecked: boolean;
// category: string;
maxPrice: string;
minPrice: string;
type:string;
states = [];
statesSet = new Set();
state:string;
first: number = 0;
rows: number = 12;
totalRecords:number = 0;
ts = new Date().getTime()
public category: 'business' | 'commercialProperty' | 'professionals_brokers' | undefined;
constructor(public selectOptions: SelectOptionsService,
private listingsService:ListingsService,
private userService:UserService,
private activatedRoute: ActivatedRoute,
private router:Router,
private cdRef:ChangeDetectorRef,
private imageService:ImageService) {
this.criteria = onChange(getCriteriaStateObject(),getSessionStorageHandler);
this.router.getCurrentNavigation()
this.activatedRoute.snapshot
this.activatedRoute.params.subscribe(params => {
if (this.activatedRoute.snapshot.fragment===''){
this.criteria = onChange(createGenericObject<ListingCriteria>(),getSessionStorageHandler)
this.first=0;
}
this.category = (<any>params).type;
this.criteria.listingsCategory=this.category;
this.init()
})
}
async ngOnInit(){
}
async init(){
if (this.category==='business' || this.category==='commercialProperty'){
this.users=[]
this.listings=await this.listingsService.getListings(this.criteria);
this.setStates();
this.filteredListings=[...this.listings];
this.totalRecords=this.listings.length
this.filteredListings=[...this.listings].splice(this.first,this.rows);
this.cdRef.markForCheck();
this.cdRef.detectChanges();
} else {
this.listings=[]
this.filteredListings=[];
this.users=await this.userService.search(this.criteria);
const profiles = await this.imageService.getProfileImagesForUsers(this.users.map(u=>u.id));
const logos = await this.imageService.getCompanyLogosForUsers(this.users.map(u=>u.id));
this.users.forEach(u=>{
u.hasProfile=profiles[u.id]
u.hasCompanyLogo=logos[u.id]
})
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
}
setStates(){
this.statesSet=new Set();
this.listings.forEach(l=>{
if (l.state){
this.statesSet.add(l.state)
}
})
this.states = [...this.statesSet].map((ls) =>({name:this.selectOptions.getState(ls as string),value:ls}))
}
async search() {
this.listings= await this.listingsService.getListings(this.criteria);
this.setStates();
this.totalRecords=this.listings.length
this.filteredListings =[...this.listings].splice(this.first,this.rows);
this.cdRef.markForCheck();
this.cdRef.detectChanges();
}
onPageChange(event: any) {
this.first = event.first;
this.rows = event.rows;
this.filteredListings=[...this.listings].splice(this.first,this.rows);
}
imageErrorHandler(listing: ListingType) {
listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann
}
}

View File

@ -8,19 +8,34 @@
</a> </a>
</li> </li>
<li> <li>
<a routerLink="/createListing" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"> <a
routerLink="/createBusinessListing"
routerLinkActive="text-blue-500"
pRipple
class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"
>
<i class="pi pi-plus-circle md:mr-2"></i> <i class="pi pi-plus-circle md:mr-2"></i>
<span class="font-medium hidden md:block">Create Listing</span> <span class="font-medium hidden md:block">Create Listing</span>
</a> </a>
</li> </li>
<li> <li>
<a routerLink="/myListings" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"> <a
routerLink="/myListings"
routerLinkActive="text-blue-500"
pRipple
class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"
>
<i class="pi pi-list md:mr-2"></i> <i class="pi pi-list md:mr-2"></i>
<span class="font-medium hidden md:block">My Listings</span> <span class="font-medium hidden md:block">My Listings</span>
</a> </a>
</li> </li>
<li> <li>
<a routerLink="/myFavorites" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"> <a
routerLink="/myFavorites"
routerLinkActive="text-blue-500"
pRipple
class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"
>
<i class="pi pi-star md:mr-2"></i> <i class="pi pi-star md:mr-2"></i>
<span class="font-medium hidden md:block">My Favorites</span> <span class="font-medium hidden md:block">My Favorites</span>
</a> </a>
@ -32,7 +47,12 @@
</a> </a>
</li> </li>
<li> <li>
<a (click)="userService.logout()" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"> <a
(click)="userService.logout()"
routerLinkActive="text-blue-500"
pRipple
class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"
>
<fa-icon [icon]="faRightFromBracket" class="mr-2 flex"></fa-icon> <fa-icon [icon]="faRightFromBracket" class="mr-2 flex"></fa-icon>
<span class="font-medium hidden md:block">Logout</span> <span class="font-medium hidden md:block">Logout</span>
</a> </a>

View File

@ -1,26 +1,24 @@
<div class="container"> <div class="container">
<div class="wrapper"> <div class="wrapper">
<div class="py-3 px-6 flex flex-column align-items-center justify-content-between relative"> <div class="py-3 px-6 flex flex-column align-items-center justify-content-between relative">
<a routerLink="/home"><img src="../../../assets/images/header-logo.png" alt="Image" height="50" ></a> <a routerLink="/home"><img src="../../../assets/images/header-logo.png" alt="Image" height="50" /></a>
<div class="px-4 py-8 md:px-6 lg:px-8 bg-no-repeat bg-cover"> <div class="px-4 py-8 md:px-6 lg:px-8 bg-no-repeat bg-cover">
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<div class="w-full lg:w-6 lg:pr-8"> <div class="w-full lg:w-6 lg:pr-8">
<div class="text-900 font-bold text-6xl text-blue-900 mb-4">Pricing</div> <div class="text-900 font-bold text-6xl text-blue-900 mb-4">Pricing</div>
<div class="text-700 text-xl text-blue-600 line-height-3 mb-4 lg:mb-0">Lorem ipsum dolor sit, amet consectetur adipisicing elit. Velitnumquam eligendi quos.</div> <div class="text-700 text-xl text-blue-600 line-height-3 mb-4 lg:mb-0">
With the "Forever Free" package, you can get started right away. If you need more support or a larger data volume, you can upgrade to the "Monthly" or "Yearly" package at any time.
</div>
</div> </div>
<div class="w-full md:w-6 lg:w-3"> <div class="w-full md:w-6 lg:w-3">
<ul class="list-none p-0 m-0"> <ul class="list-none p-0 m-0">
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max"> <li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i> <i class="pi pi-check text-green-500 mr-3"></i>
<span>Arcu vitae elementum</span> <span>Flexible pricing</span>
</li> </li>
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max"> <li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i> <i class="pi pi-check text-green-500 mr-3"></i>
<span>Dui faucibus in ornare</span> <span>Upgradeable plans</span>
</li>
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i>
<span>Morbi tincidunt augue</span>
</li> </li>
</ul> </ul>
</div> </div>
@ -28,16 +26,20 @@
<ul class="list-none p-0 m-0"> <ul class="list-none p-0 m-0">
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max"> <li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i> <i class="pi pi-check text-green-500 mr-3"></i>
<span>Duis ultricies lacus sed</span> <span>Customizable options</span>
</li> </li>
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max"> <li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i>
<span>Monthly/Yearly package</span>
</li>
<!-- <li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i> <i class="pi pi-check text-green-500 mr-3"></i>
<span>Imperdiet proin</span> <span>Imperdiet proin</span>
</li> </li>
<li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max"> <li class="flex align-items-center my-4 bg-white-alpha-40 shadow-2 py-1 px-2 border-round-lg w-max">
<i class="pi pi-check text-green-500 mr-3"></i> <i class="pi pi-check text-green-500 mr-3"></i>
<span>Nisi scelerisque</span> <span>Nisi scelerisque</span>
</li> </li> -->
</ul> </ul>
</div> </div>
</div> </div>
@ -46,7 +48,13 @@
<div class="shadow-2 p-3 h-full bg-primary" style="border-radius: 6px"> <div class="shadow-2 p-3 h-full bg-primary" style="border-radius: 6px">
<div class="font-medium text-xl mb-5">Free Forever</div> <div class="font-medium text-xl mb-5">Free Forever</div>
<div class="font-bold text-5xl mb-5">Free</div> <div class="font-bold text-5xl mb-5">Free</div>
<button (click)="register()" type="button" pRipple class="font-medium appearance-none border-none p-2 surface-0 text-primary hover:surface-100 p-component lg:w-full border-rounded cursor-pointer transition-colors transition-duration-150" style="border-radius: 6px"> <button
(click)="register()"
type="button"
pRipple
class="font-medium appearance-none border-none p-2 surface-0 text-primary hover:surface-100 p-component lg:w-full border-rounded cursor-pointer transition-colors transition-duration-150"
style="border-radius: 6px"
>
<span>Create Account</span> <span>Create Account</span>
</button> </button>
<p class="text-sm line-height-3 mb-0 mt-5">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p> <p class="text-sm line-height-3 mb-0 mt-5">Lorem ipsum dolor sit, amet consectetur adipisicing elit.</p>
@ -79,7 +87,6 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,51 +7,44 @@
<p-divider></p-divider> <p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row"> <div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid"> <div class="flex-auto p-fluid">
<!-- <div class="mb-4"> @if (user){
<label for="email" class="block font-medium text-900 mb-2">Username</label>
<input id="email" type="text" [disabled]="true" pInputText [(ngModel)]="user.username">
<p class="font-italic text-sm line-height-1">Usernames cannot be changed.</p>
</div> -->
<div class="mb-4"> <div class="mb-4">
<label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label> <label for="state" class="block font-medium text-900 mb-2">E-mail (required)</label>
<input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email"> <input id="state" type="text" [disabled]="true" pInputText [(ngModel)]="user.email" />
<p class="font-italic text-sm line-height-1">You can only modify your email by contacting us at <p class="font-italic text-sm line-height-1">You can only modify your email by contacting us at support&#64;bizmatch.net</p>
emailchange&#64;bizmatch.net</p>
</div> </div>
<div class="grid"> <div class="grid">
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
<label for="firstname" class="block font-medium text-900 mb-2">First Name</label> <label for="firstname" class="block font-medium text-900 mb-2">First Name</label>
<input id="firstname" type="text" pInputText [(ngModel)]="user.firstname"> <input id="firstname" type="text" pInputText [(ngModel)]="user.firstname" />
</div> </div>
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
<label for="lastname" class="block font-medium text-900 mb-2">Last Name</label> <label for="lastname" class="block font-medium text-900 mb-2">Last Name</label>
<input id="lastname" type="text" pInputText [(ngModel)]="user.lastname"> <input id="lastname" type="text" pInputText [(ngModel)]="user.lastname" />
</div> </div>
</div> </div>
<div class="grid"> <div class="grid">
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
<label for="firstname" class="block font-medium text-900 mb-2">Company Name</label> <label for="firstname" class="block font-medium text-900 mb-2">Company Name</label>
<input id="firstname" type="text" pInputText [(ngModel)]="user.companyName"> <input id="firstname" type="text" pInputText [(ngModel)]="user.companyName" />
</div> </div>
<div class="mb-4 col-12 md:col-6"> <div class="mb-4 col-12 md:col-6">
<label for="lastname" class="block font-medium text-900 mb-2">Describe yourself</label> <label for="lastname" class="block font-medium text-900 mb-2">Describe yourself</label>
<input id="lastname" type="text" pInputText [(ngModel)]="user.description"> <input id="lastname" type="text" pInputText [(ngModel)]="user.description" />
</div> </div>
</div> </div>
<div class="grid"> <div class="grid">
<div class="mb-4 col-12 md:col-4"> <div class="mb-4 col-12 md:col-4">
<label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label> <label for="phoneNumber" class="block font-medium text-900 mb-2">Your Phone Number</label>
<input id="phoneNumber" type="text" pInputText [(ngModel)]="user.phoneNumber"> <input id="phoneNumber" type="text" pInputText [(ngModel)]="user.phoneNumber" />
</div> </div>
<div class="mb-4 col-12 md:col-4"> <div class="mb-4 col-12 md:col-4">
<label for="companyWebsite" class="block font-medium text-900 mb-2">Company Website</label> <label for="companyWebsite" class="block font-medium text-900 mb-2">Company Website</label>
<input id="companyWebsite" type="text" pInputText [(ngModel)]="user.companyWebsite"> <input id="companyWebsite" type="text" pInputText [(ngModel)]="user.companyWebsite" />
</div> </div>
<div class="mb-4 col-12 md:col-4"> <div class="mb-4 col-12 md:col-4">
<label for="companyLocation" class="block font-medium text-900 mb-2">Company <label for="companyLocation" class="block font-medium text-900 mb-2">Company Location</label>
Location</label> <p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
<p-autoComplete [(ngModel)]="user.companyLocation" [suggestions]="suggestions"
(completeMethod)="search($event)"></p-autoComplete>
</div> </div>
</div> </div>
<div class="mb-4"> <div class="mb-4">
@ -69,62 +62,82 @@
<div class="mb-4"> <div class="mb-4">
<label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label> <label for="areasServed" class="block font-medium text-900 mb-2">Areas We Serve</label>
<textarea id="areasServed" type="text" pInputTextarea rows="5" [autoResize]="true" <textarea id="areasServed" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="user.areasServed"></textarea>
[(ngModel)]="user.areasServed"></textarea>
</div> </div>
<div> <div>
<label for="companyOverview" class="block font-medium text-900 mb-2">Licensed In</label> <label for="companyOverview" class="block font-medium text-900 mb-2">Licensed In</label>
@for (licensedIn of user.licensedIn; track licensedIn.value){ @for (licensedIn of userLicensedIn; track licensedIn.value){
<div class="grid"> <div class="grid">
<div class="flex col-12 md:col-6"> <div class="flex col-12 md:col-6">
<p-dropdown id="states" [options]="selectOptions?.states" [(ngModel)]="licensedIn.name" <p-dropdown
optionLabel="name" optionValue="value" [showClear]="true" placeholder="State" id="states"
[ngStyle]="{ width: '100%'}"></p-dropdown> [options]="selectOptions?.states"
[(ngModel)]="licensedIn.name"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="State"
[ngStyle]="{ width: '100%' }"
></p-dropdown>
</div> </div>
<div class="flex col-12 md:col-6"> <div class="flex col-12 md:col-6">
<input id="companyWebsite" type="text" pInputText [(ngModel)]="licensedIn.value" <input id="companyWebsite" type="text" pInputText [(ngModel)]="licensedIn.value" placeholder="Licence Number" />
placeholder="Licence Number">
</div> </div>
</div> </div>
} }
</div> </div>
<div class="field mb-5 col-12 md:col-6 flex align-items-center"> <div class="field mb-5 col-12 md:col-6 flex align-items-center">
<p-button class="mr-1" icon="pi pi-plus" severity="success" (click)="addLicence()"></p-button> <p-button class="mr-1" icon="pi pi-plus" severity="success" (click)="addLicence()"></p-button>
<p-button icon="pi pi-minus" severity="danger" (click)="removeLicence()" <p-button icon="pi pi-minus" severity="danger" (click)="removeLicence()" [disabled]="user.licensedIn?.length < 2"></p-button>
[disabled]="user.licensedIn?.length<2"></p-button>
<span class="text-xs">&nbsp;(Add more licenses or remove existing ones.)</span> <span class="text-xs">&nbsp;(Add more licenses or remove existing ones.)</span>
<!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> --> <!-- <button pButton pRipple label="Add Licence" class="w-auto" (click)="addLicence()"></button> -->
</div> </div>
}
<div> <div>
<button pButton pRipple label="Update Profile" class="w-auto" <button pButton pRipple label="Update Profile" class="w-auto" (click)="updateProfile(user)"></button>
(click)="updateProfile(user)"></button>
</div> </div>
</div> </div>
<div> <div>
<div class="flex flex-column align-items-center flex-or mb-8"> <div class="flex flex-column align-items-center flex-or mb-8">
<span class="font-medium text-900 mb-2">Company Logo</span> <span class="font-medium text-900 mb-2">Company Logo</span>
<span class="font-medium text-xs mb-2">(is shown in every offer)</span> <span class="font-medium text-xs mb-2">(is shown in every offer)</span>
@if(user.hasCompanyLogo){ @if(user?.hasCompanyLogo){
<img src="{{ companyLogoUrl }}" class="rounded-profile" /> <img src="{{ companyLogoUrl }}" class="rounded-profile" />
<!-- <img src="profile/{{ user.id }}.avif" class="rounded-profile" /> -->
} @else { } @else {
<img src="assets/images/placeholder.png" class="rounded-profile" /> <img src="assets/images/placeholder.png" class="rounded-profile" />
} }
<p-fileUpload #companyUpload mode="basic" chooseLabel="Upload" name="file" [customUpload]="true" <p-fileUpload
accept="image/*" [maxFileSize]="maxFileSize" (onSelect)="select($event,'company')" #companyUpload
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"></p-fileUpload> mode="basic"
chooseLabel="Upload"
name="file"
[customUpload]="true"
accept="image/*"
[maxFileSize]="maxFileSize"
(onSelect)="select($event, 'company')"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
></p-fileUpload>
</div> </div>
<p-divider></p-divider> <p-divider></p-divider>
<div class="flex flex-column align-items-center flex-or"> <div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Your Profile Picture</span> <span class="font-medium text-900 mb-2">Your Profile Picture</span>
@if(user.hasProfile){ @if(user?.hasProfile){
<img src="{{ profileUrl }}" class="rounded-profile" /> <img src="{{ profileUrl }}" class="rounded-profile" />
} @else { } @else {
<img src="assets/images/person_placeholder.jpg" class="rounded-profile" /> <img src="assets/images/person_placeholder.jpg" class="rounded-profile" />
} }
<p-fileUpload #profileUpload mode="basic" chooseLabel="Upload" name="file" [customUpload]="true" <p-fileUpload
accept="image/*" [maxFileSize]="maxFileSize" (onSelect)="select($event,'profile')" #profileUpload
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"></p-fileUpload> mode="basic"
chooseLabel="Upload"
name="file"
[customUpload]="true"
accept="image/*"
[maxFileSize]="maxFileSize"
(onSelect)="select($event, 'profile')"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
></p-fileUpload>
</div> </div>
</div> </div>
</div> </div>
@ -145,9 +158,7 @@
<ng-template pTemplate="body" let-subscription let-expanded="expanded"> <ng-template pTemplate="body" let-subscription let-expanded="expanded">
<tr> <tr>
<td> <td>
<button type="button" pButton pRipple [pRowToggler]="subscription" <button type="button" pButton pRipple [pRowToggler]="subscription" class="p-button-text p-button-rounded p-button-plain" [icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
class="p-button-text p-button-rounded p-button-plain"
[icon]="expanded ? 'pi pi-chevron-down' : 'pi pi-chevron-right'"></button>
</td> </td>
<td>{{ subscription.id }}</td> <td>{{ subscription.id }}</td>
<td>{{ subscription.level }}</td> <td>{{ subscription.level }}</td>
@ -173,8 +184,7 @@
<ng-template pTemplate="body" let-invoice> <ng-template pTemplate="body" let-invoice>
<tr> <tr>
<td> <td>
<button pButton pRipple icon="pi pi-print" class="p-button-rounded p-button-success mr-2" <button pButton pRipple icon="pi pi-print" class="p-button-rounded p-button-success mr-2" (click)="printInvoice(invoice)"></button>
(click)="printInvoice(invoice)"></button>
</td> </td>
<td>{{ invoice.id }}</td> <td>{{ invoice.id }}</td>
<td>{{ invoice.date | date }}</td> <td>{{ invoice.date | date }}</td>
@ -192,21 +202,3 @@
</div> </div>
</div> </div>
</div> </div>
<!-- <p-dialog header="Edit Image" [visible]="imageUrl" [modal]="true" [style]="{ width: '50vw' }" [draggable]="false" [resizable]="false">
<angular-cropper #cropper [imageUrl]="imageUrl" [cropperOptions]="config"></angular-cropper>
<ng-template pTemplate="footer" let-config="config">
<div class="flex justify-content-between">
@if(type==='company'){
<div>
<p-selectButton [options]="stateOptions" [ngModel]="value" (ngModelChange)="changeAspectRation($event)" optionLabel="label" optionValue="value"></p-selectButton>
</div>
} @else {
<div></div>
}
<div>
<p-button icon="pi" (click)="cancelUpload()" label="Cancel" [outlined]="true"></p-button>
<p-button icon="pi pi-check" (click)="sendImage()" label="Finish" pAutoFocus [autofocus]="true"></p-button>
</div>
</div>
</ng-template>
</p-dialog> -->

View File

@ -1,40 +1,25 @@
import { HttpEventType } from '@angular/common/http';
import { ChangeDetectorRef, Component, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, ViewChild } 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 { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { InputTextareaModule } from 'primeng/inputtextarea'; import { AngularCropperjsModule } from 'angular-cropperjs';
import { ChipModule } from 'primeng/chip'; import { MessageService } from 'primeng/api';
import { MenuAccountComponent } from '../../menu-account/menu-account.component'; import { DialogModule } from 'primeng/dialog';
import { DividerModule } from 'primeng/divider'; import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { TableModule } from 'primeng/table'; import { EditorModule } from 'primeng/editor';
import { HttpClient, HttpEventType } from '@angular/common/http'; import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { SelectButtonModule } from 'primeng/selectbutton';
import { lastValueFrom } from 'rxjs';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, Invoice, KeyValue, Subscription } 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';
import { ImageService } from '../../../services/image.service';
import { LoadingService } from '../../../services/loading.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { SubscriptionsService } from '../../../services/subscriptions.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { SubscriptionsService } from '../../../services/subscriptions.service';
import { lastValueFrom } from 'rxjs';
import { MessageService } from 'primeng/api';
import { environment } from '../../../../environments/environment';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { AutoCompleteCompleteEvent, Invoice, KeyValue, KeyValueRatio, Subscription, User } from '../../../../../../common-models/src/main.model';
import { GeoService } from '../../../services/geo.service';
import { ChangeDetectionStrategy } from '@angular/compiler';
import { EditorModule } from 'primeng/editor';
import { LoadingService } from '../../../services/loading.service';
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
import { ImageService } from '../../../services/image.service';
import { DialogModule } from 'primeng/dialog';
import { SelectButtonModule } from 'primeng/selectbutton';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
import Quill from 'quill'
import { TOOLBAR_OPTIONS } from '../../utils/defaults'; import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({ @Component({
selector: 'app-account', selector: 'app-account',
@ -42,7 +27,7 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule], imports: [SharedModule, FileUploadModule, EditorModule, AngularCropperjsModule, DialogModule, SelectButtonModule, DynamicDialogModule],
providers: [MessageService, DialogService], providers: [MessageService, DialogService],
templateUrl: './account.component.html', templateUrl: './account.component.html',
styleUrl: './account.component.scss' styleUrl: './account.component.scss',
}) })
export class AccountComponent { export class AccountComponent {
@ViewChild('companyUpload') public companyUpload: FileUpload; @ViewChild('companyUpload') public companyUpload: FileUpload;
@ -54,11 +39,13 @@ export class AccountComponent {
maxFileSize = 1000000; maxFileSize = 1000000;
companyLogoUrl: string; companyLogoUrl: string;
profileUrl: string; profileUrl: string;
type: 'company' | 'profile' type: 'company' | 'profile';
dialogRef: DynamicDialogRef | undefined; dialogRef: DynamicDialogRef | undefined;
environment = environment environment = environment;
editorModules = TOOLBAR_OPTIONS editorModules = TOOLBAR_OPTIONS;
constructor(public userService: UserService, userLicensedIn: KeyValue[];
constructor(
public userService: UserService,
private subscriptionService: SubscriptionsService, private subscriptionService: SubscriptionsService,
private messageService: MessageService, private messageService: MessageService,
private geoService: GeoService, private geoService: GeoService,
@ -67,60 +54,72 @@ export class AccountComponent {
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private loadingService: LoadingService, private loadingService: LoadingService,
private imageUploadService: ImageService, private imageUploadService: ImageService,
public dialogService: DialogService) {} public dialogService: DialogService,
) {}
async ngOnInit() { async ngOnInit() {
this.user = await this.userService.getById(this.id); const keycloakUser = this.userService.getKeycloakUser();
const email = keycloakUser.email;
try {
this.user = await this.userService.getByMail(email);
} catch (e) {
this.user = { email, firstname: keycloakUser.firstname, lastname: keycloakUser.lastname };
this.user = await this.userService.save(this.user);
}
this.userLicensedIn = this.user.licensedIn
? this.user.licensedIn.map(l => {
return { name: l.split('|')[0], value: l.split('|')[1] };
})
: [];
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions()); this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions());
if (!this.user.licensedIn || this.user.licensedIn?.length === 0) { if (!this.user.licensedIn || this.user.licensedIn?.length === 0) {
this.user.licensedIn = [{ name: '', value: '' }] this.user.licensedIn = [''];
} }
this.user = await this.userService.getById(this.user.id); this.profileUrl = this.user.hasProfile ? `pictures/profile/${this.user.id}.avif` : `/assets/images/placeholder.png`;
this.profileUrl = this.user.hasProfile ? `${environment.apiBaseUrl}/profile/${this.user.id}.avif` : `/assets/images/placeholder.png` this.companyLogoUrl = this.user.hasCompanyLogo ? `pictures/logo/${this.user.id}.avif` : `/assets/images/placeholder.png`;
this.companyLogoUrl = this.user.hasCompanyLogo ? `${environment.apiBaseUrl}/logo/${this.user.id}.avif` : `/assets/images/placeholder.png`
} }
printInvoice(invoice: Invoice) {} printInvoice(invoice: Invoice) {}
async updateProfile(user: User) { async updateProfile(user: User) {
this.user.licensedIn = this.userLicensedIn.map(l => `${l.name}|${l.value}`);
await this.userService.save(this.user); await this.userService.save(this.user);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Acount changes have been persisted', life: 3000 });
} }
onUploadCompanyLogo(event: any) { onUploadCompanyLogo(event: any) {
const uniqueSuffix = '?_ts=' + new Date().getTime(); const uniqueSuffix = '?_ts=' + new Date().getTime();
this.companyLogoUrl = `${environment.apiBaseUrl}/logo/${this.user.id}${uniqueSuffix}` //`http://IhrServer:Port/${newImagePath}${uniqueSuffix}`; this.companyLogoUrl = `pictures/logo/${this.user.id}${uniqueSuffix}`;
} }
onUploadProfilePicture(event: any) { onUploadProfilePicture(event: any) {
const uniqueSuffix = '?_ts=' + new Date().getTime(); const uniqueSuffix = '?_ts=' + new Date().getTime();
this.profileUrl = `${environment.apiBaseUrl}/profile/${this.user.id}${uniqueSuffix}` //`http://IhrServer:Port/${newImagePath}${uniqueSuffix}`; this.profileUrl = `pictures/profile/${this.user.id}${uniqueSuffix}`;
} }
setImageToFallback(event: Event) { setImageToFallback(event: Event) {
(event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild (event.target as HTMLImageElement).src = `/assets/images/placeholder.png`; // Pfad zum Platzhalterbild
} }
suggestions: string[] | undefined; suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) { async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query)) const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query));
this.suggestions = result.map(r => `${r.city} - ${r.state_code}`).slice(0, 5); this.suggestions = result.map(r => `${r.city} - ${r.state_code}`).slice(0, 5);
} }
addLicence() { addLicence() {
this.user.licensedIn.push({ name: '', value: '' }); this.userLicensedIn.push({ name: '', value: '' });
} }
removeLicence() { removeLicence() {
this.user.licensedIn.splice(this.user.licensedIn.length - 2, 1); this.userLicensedIn.splice(this.user.licensedIn.length - 2, 1);
} }
select(event: any, type: 'company' | 'profile') { select(event: any, type: 'company' | 'profile') {
const imageUrl = URL.createObjectURL(event.files[0]); const imageUrl = URL.createObjectURL(event.files[0]);
this.type = type this.type = type;
const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value } const config = { aspectRatio: type === 'company' ? stateOptions[0].value : stateOptions[2].value };
this.dialogRef = this.dialogService.open(ImageCropperComponent, { this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: { data: {
imageUrl: imageUrl, imageUrl: imageUrl,
fileUpload: type === 'company' ? this.companyUpload : this.profileUpload, fileUpload: type === 'company' ? this.companyUpload : this.profileUpload,
config: config, config: config,
ratioVariable: type === 'company' ? true : false ratioVariable: type === 'company' ? true : false,
}, },
header: 'Edit Image', header: 'Edit Image',
width: '50vw', width: '50vw',
@ -130,27 +129,30 @@ export class AccountComponent {
closable: false, closable: false,
breakpoints: { breakpoints: {
'960px': '75vw', '960px': '75vw',
'640px': '90vw' '640px': '90vw',
}, },
}); });
this.dialogRef.onClose.subscribe(cropper => { this.dialogRef.onClose.subscribe(cropper => {
if (cropper) { if (cropper) {
this.loadingService.startLoading('uploadImage'); this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async (blob) => { cropper.getCroppedCanvas().toBlob(async blob => {
this.imageUploadService.uploadImage(blob, type==='company'?'uploadCompanyLogo':'uploadProfile',this.user.id).subscribe(async(event) => { this.imageUploadService.uploadImage(blob, type === 'company' ? 'uploadCompanyLogo' : 'uploadProfile', this.user.id).subscribe(
async event => {
if (event.type === HttpEventType.Response) { if (event.type === HttpEventType.Response) {
this.loadingService.stopLoading('uploadImage'); this.loadingService.stopLoading('uploadImage');
if (this.type === 'company') { if (this.type === 'company') {
this.user.hasCompanyLogo=true; this.user.hasCompanyLogo = true; //
this.companyLogoUrl=`${environment.apiBaseUrl}/logo/${this.user.id}.avif?_ts=${new Date().getTime()}` this.companyLogoUrl = `${environment.apiBaseUrl}/logo/${this.user.id}.avif?_ts=${new Date().getTime()}`;
} else { } else {
this.user.hasProfile = true; this.user.hasProfile = true;
this.profileUrl=`${environment.apiBaseUrl}/profile/${this.user.id}.avif?_ts=${new Date().getTime()}` this.profileUrl = `${environment.apiBaseUrl}/profile/${this.user.id}.avif?_ts=${new Date().getTime()}`;
} }
} }
}, error => console.error('Fehler beim Upload:', error)); },
}) error => console.error('Fehler beim Upload:', error),
);
});
} }
}) });
} }
} }

View File

@ -0,0 +1,151 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<div *ngIf="listing" class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">{{ mode === 'create' ? 'New' : 'Edit' }} Listing</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Listing category</label>
<p-dropdown
id="listingCategory"
[options]="selectOptions?.listingCategories"
[ngModel]="listingsCategory"
optionLabel="name"
optionValue="value"
(ngModelChange)="changeListingCategory($event)"
placeholder="Listing category"
[disabled]="mode === 'edit'"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="mb-4">
<label for="email" class="block font-medium text-900 mb-2">Title of Listing</label>
<input id="email" type="text" pInputText [(ngModel)]="listing.title" />
</div>
<div>
<div class="mb-4">
<label for="description" class="block font-medium text-900 mb-2">Description</label>
<!-- <textarea id="description" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.description"></textarea> -->
<p-editor [(ngModel)]="listing.description" [style]="{ height: '320px' }" [modules]="editorModules">
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
</div>
<div class="mb-4">
<label for="type" class="block font-medium text-900 mb-2">Type of business</label>
<p-dropdown id="type" [options]="typesOfBusiness" [(ngModel)]="listing.type" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Type of business" [style]="{ width: '100%' }"></p-dropdown>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="listingCategory" class="block font-medium text-900 mb-2">State</label>
<p-dropdown
id="listingCategory"
[options]="selectOptions?.states"
[(ngModel)]="listing.state"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="State"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="listingCategory" class="block font-medium text-900 mb-2">City</label>
<p-autoComplete [(ngModel)]="listing.city" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
</div>
</div>
</div>
</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="salesRevenue" class="block font-medium text-900 mb-2">Sales Revenue</label>
<app-inputNumber mode="currency" currency="USD" inputId="salesRevenue" [(ngModel)]="listing.salesRevenue"></app-inputNumber>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="cashFlow" class="block font-medium text-900 mb-2">Cash Flow</label>
<app-inputNumber mode="currency" currency="USD" inputId="cashFlow" [(ngModel)]="listing.cashFlow"></app-inputNumber>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="employees" class="block font-medium text-900 mb-2">Years Established Since</label>
<app-inputNumber mode="decimal" inputId="established" [(ngModel)]="listing.established"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="employees" class="block font-medium text-900 mb-2">Employees</label>
<app-inputNumber mode="decimal" inputId="employees" [(ngModel)]="listing.employees"></app-inputNumber>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-4">
<p-checkbox [binary]="true" [(ngModel)]="listing.realEstateIncluded"></p-checkbox>
<span class="ml-2 text-900">Real Estate Included</span>
</div>
<div class="mb-4 col-12 md:col-4">
<p-checkbox [binary]="true" [(ngModel)]="listing.leasedLocation"></p-checkbox>
<span class="ml-2 text-900">Leased Location</span>
</div>
<div class="mb-4 col-12 md:col-4">
<p-checkbox [binary]="true" [(ngModel)]="listing.franchiseResale"></p-checkbox>
<span class="ml-2 text-900">Franchise Re-Sale</span>
</div>
</div>
<div class="mb-4">
<label for="supportAndTraining" class="block font-medium text-900 mb-2">Support & Training</label>
<!-- <textarea id="inventory" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.supportAndTraining"></textarea> -->
<input id="supportAndTraining" type="text" pInputText [(ngModel)]="listing.supportAndTraining" />
</div>
<div class="mb-4">
<label for="reasonForSale" class="block font-medium text-900 mb-2">Reason for Sale</label>
<textarea id="reasonForSale" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.reasonForSale"></textarea>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="brokerLicensing" class="block font-medium text-900 mb-2">Broker Licensing</label>
<input id="brokerLicensing" type="text" pInputText [(ngModel)]="listing.brokerLicencing" />
</div>
<div class="mb-4 col-12 md:col-6">
<label for="internalListingNumber" class="block font-medium text-900 mb-2">Internal Listing Number</label>
<app-inputNumber mode="decimal" inputId="internalListingNumber" type="text" [(ngModel)]="listing.internalListingNumber"></app-inputNumber>
</div>
</div>
<div class="mb-4">
<label for="internalListing" class="block font-medium text-900 mb-2">Internal Notes (Will not be shown on the listing, for your records only.)</label>
<input id="internalListing" type="text" pInputText [(ngModel)]="listing.internals" />
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<!-- <p-tag value="New"></p-tag> -->
<!-- <p-checkbox [binary]="true" [(ngModel)]="listing.draft"></p-checkbox> -->
<p-inputSwitch inputId="draft" [(ngModel)]="listing.draft"></p-inputSwitch>
<!-- <span class="ml-2 text-900">Share my data with contacts</span> -->
<span class="ml-2 text-900 absolute translate-y-5">Draft Mode (Will not be shown as public listing)</span>
</div>
</div>
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
}
</div>
</div>
</div>
</div>
</div>
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>

View File

@ -0,0 +1,147 @@
import { Component, ViewChild } from '@angular/core';
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 { createGenericObject, getListingType, routeListingWithState } from '../../../utils/utils';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { ConfirmationService, MessageService } from 'primeng/api';
import { CarouselModule } from 'primeng/carousel';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
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 { environment } from '../../../../environments/environment';
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { LoadingService } from '../../../services/loading.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({
selector: 'business-listing',
standalone: true,
imports: [
SharedModule,
ArrayToStringPipe,
InputNumberModule,
CarouselModule,
DialogModule,
AngularCropperjsModule,
FileUploadModule,
EditorModule,
DynamicDialogModule,
DragDropModule,
ConfirmDialogModule,
MixedCdkDragDropModule,
],
providers: [MessageService, DialogService, ConfirmationService],
templateUrl: './edit-business-listing.component.html',
styleUrl: './edit-business-listing.component.scss',
})
export class EditBusinessListingComponent {
@ViewChild(FileUpload) public fileUpload: FileUpload;
listingsCategory = 'business';
category: string;
location: string;
mode: 'edit' | 'create';
separator: '\n\n';
listing: BusinessListing;
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',
numVisible: 1,
numScroll: 1,
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1,
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1,
},
];
config = { aspectRatio: 16 / 9 };
editorModules = TOOLBAR_OPTIONS;
dialogRef: DynamicDialogRef | undefined;
draggedImage: ImageProperty;
faTrash = faTrash;
data: CommercialPropertyListing;
typesOfBusiness = [];
constructor(
public selectOptions: SelectOptionsService,
private router: Router,
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
public userService: UserService,
private messageService: MessageService,
private geoService: GeoService,
private imageService: ImageService,
private loadingService: LoadingService,
public dialogService: DialogService,
private confirmationService: ConfirmationService,
private route: ActivatedRoute,
) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.mode = event.url === '/createBusinessListing' ? 'create' : 'edit';
}
});
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
this.data = this.router.getCurrentNavigation().extras.state['data'];
}
});
this.typesOfBusiness = selectOptions.typesOfBusiness.map(e => {
return { name: e.name, value: parseInt(e.value) };
});
}
async ngOnInit() {
if (this.mode === 'edit') {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
} else {
this.listing = createGenericObject<BusinessListing>();
this.listing.listingsCategory = 'business';
this.listing.userId = await this.userService.getId();
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() {
this.listing = await this.listingsService.save(this.listing, getListingType(this.listing));
this.router.navigate(['editBusinessListing', this.listing.id]);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 });
}
suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
this.suggestions = result.map(r => r.city).slice(0, 5);
}
changeListingCategory(value: 'business' | 'commercialProperty') {
routeListingWithState(this.router, value, this.listing);
}
}

View File

@ -0,0 +1,126 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<div *ngIf="listing" class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">{{ mode === 'create' ? 'New' : 'Edit' }} Listing</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Listing category</label>
<p-dropdown
id="listingCategory"
[options]="selectOptions?.listingCategories"
[ngModel]="listingsCategory"
optionLabel="name"
optionValue="value"
(ngModelChange)="changeListingCategory($event)"
placeholder="Listing category"
[disabled]="mode === 'edit'"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="mb-4">
<label for="email" class="block font-medium text-900 mb-2">Title of Listing</label>
<input id="email" type="text" pInputText [(ngModel)]="listing.title" />
</div>
<div>
<div class="mb-4">
<label for="description" class="block font-medium text-900 mb-2">Description</label>
<!-- <textarea id="description" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.description"></textarea> -->
<p-editor [(ngModel)]="listing.description" [style]="{ height: '320px' }" [modules]="editorModules">
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
</div>
<div class="mb-4">
<label for="type" class="block font-medium text-900 mb-2">Property Category</label>
<p-dropdown
id="type"
[options]="typesOfCommercialProperty"
[(ngModel)]="listing.type"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="Property Category"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="states" class="block font-medium text-900 mb-2">State</label>
<p-dropdown id="states" [options]="selectOptions?.states" [(ngModel)]="listing.state" optionLabel="name" optionValue="value" [showClear]="true" placeholder="State" [style]="{ width: '100%' }"></p-dropdown>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="city" class="block font-medium text-900 mb-2">City</label>
<p-autoComplete id="city" [(ngModel)]="listing.city" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="zipCode" class="block font-medium text-900 mb-2">Zip Code</label>
<input id="zipCode" type="text" pInputText [(ngModel)]="listing.zipCode" />
</div>
<div class="mb-4 col-12 md:col-6">
<label for="county" class="block font-medium text-900 mb-2">County</label>
<input id="county" type="text" pInputText [(ngModel)]="listing.county" />
</div>
</div>
</div>
</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Property Pictures</span>
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
<p-fileUpload
mode="basic"
chooseLabel="Upload"
[customUpload]="true"
name="file"
accept="image/*"
[maxFileSize]="maxFileSize"
(onSelect)="select($event)"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
[disabled]="!listing.id"
>
</p-fileUpload>
</div>
</div>
</div>
@if (propertyImages?.length>0){
<div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop (dropped)="onDrop($event)" cdkDropListOrientation="horizontal">
@for (image of propertyImages; track image) {
<span cdkDropList mixedCdkDropList>
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
<img src="pictures/property/{{ listing.imagePath }}/{{ image }}" [alt]="image" class="shadow-2" cdkDrag />
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image)"></fa-icon>
</div>
</span>
}
</div>
}
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
}
</div>
</div>
</div>
</div>
</div>
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>

View File

@ -0,0 +1,57 @@
.translate-y-5 {
transform: translateY(5px);
}
.image-container {
display: flex; /* Erlaubt ein flexibles Box-Layout */
flex-wrap: wrap; /* Erlaubt das Umfließen der Elemente auf die nächste Zeile */
justify-content: flex-start; /* Startet die Anordnung der Elemente am Anfang des Containers */
align-items: flex-start; /* Ausrichtung der Elemente am Anfang der Querachse */
padding: 10px; /* Abstand zwischen den Inhalten des Containers und dessen Rand */
}
.image-container span {
flex-flow: row;
display: flex;
width: fit-content;
height: fit-content;
}
.image-container span img {
max-height: 150px; /* Maximale Höhe der Bilder */
width: auto; /* Die Breite der Bilder passt sich automatisch an die Höhe an */
cursor: pointer;
margin: 10px;
}
// .image-container fa-icon {
// top: 0; /* Positioniert das Icon am oberen Rand des Bildes */
// right: 0; /* 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 */
// cursor: pointer; /* Verwandelt den Cursor in eine Hand, um Interaktivität anzudeuten */
// }
.image-wrap {
position: relative; /* Ermöglicht die absolute Positionierung des Icons bezogen auf diesen Container */
display: inline-block; /* Erlaubt die Inline-Anordnung, falls mehrere Bilder vorhanden sind */
}
/* Stil für das Bild */
.image-wrap img {
max-height: 150px;
width: auto;
display: block; /* Verhindert unerwünschten Abstand unter dem Bild */
}
/* Stil für das FontAwesome Icon */
.image-wrap fa-icon {
position: absolute;
top: 15px; /* Positioniert das Icon am oberen Rand des Bildes */
right: 15px; /* 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 */
cursor: pointer; /* Verwandelt den Cursor in eine Hand, um Interaktivität anzudeuten */
border-radius: 8px; /* Optional: Abrunden der linken unteren Ecke für ästhetische Zwecke */
}

View File

@ -1,97 +1,97 @@
import { Component, ViewChild } from '@angular/core'; import { Component, ViewChild } 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 dataListings from '../../../../assets/data/listings.json';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { ActivatedRoute, NavigationEnd, Router } 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 { createGenericObject } from '../../../utils/utils';
import { ListingsService } from '../../../services/listings.service';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { createGenericObject, getListingType, routeListingWithState } from '../../../utils/utils';
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { HttpEventType } from '@angular/common/http';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { ConfirmationService, MessageService } from 'primeng/api';
import { CarouselModule } from 'primeng/carousel';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
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 { environment } from '../../../../environments/environment';
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe'; import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { LoadingService } from '../../../services/loading.service';
import { UserService } from '../../../services/user.service'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module'; import { SharedModule } from '../../../shared/shared/shared.module';
import { ConfirmationService, MessageService } from 'primeng/api';
import { AutoCompleteCompleteEvent, BusinessListing, CommercialPropertyListing, ImageProperty, ListingType, User } from '../../../../../../common-models/src/main.model';
import { GeoResult, GeoService } from '../../../services/geo.service';
import { InputNumberComponent, InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
import { environment } from '../../../../environments/environment';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { CarouselModule } from 'primeng/carousel';
import { v4 as uuidv4 } from 'uuid';
import { DialogModule } from 'primeng/dialog';
import { AngularCropperjsModule, CropperComponent } from 'angular-cropperjs';
import { HttpClient, HttpEventType } from '@angular/common/http';
import { ImageService } from '../../../services/image.service'
import { LoadingService } from '../../../services/loading.service';
import { TOOLBAR_OPTIONS } from '../../utils/defaults'; import { TOOLBAR_OPTIONS } from '../../utils/defaults';
import { EditorModule } from 'primeng/editor';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { CdkDragDrop, CdkDragEnter, CdkDragExit, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
@Component({ @Component({
selector: 'create-listing', selector: 'commercial-property-listing',
standalone: true, standalone: true,
imports: [SharedModule, ArrayToStringPipe, InputNumberModule, CarouselModule, imports: [
DialogModule, AngularCropperjsModule, FileUploadModule, EditorModule, DynamicDialogModule, DragDropModule, SharedModule,
ConfirmDialogModule, MixedCdkDragDropModule], ArrayToStringPipe,
InputNumberModule,
CarouselModule,
DialogModule,
AngularCropperjsModule,
FileUploadModule,
EditorModule,
DynamicDialogModule,
DragDropModule,
ConfirmDialogModule,
MixedCdkDragDropModule,
],
providers: [MessageService, DialogService, ConfirmationService], providers: [MessageService, DialogService, ConfirmationService],
templateUrl: './edit-listing.component.html', templateUrl: './edit-commercial-property-listing.component.html',
styleUrl: './edit-listing.component.scss' styleUrl: './edit-commercial-property-listing.component.scss',
}) })
export class EditListingComponent { export class EditCommercialPropertyListingComponent {
@ViewChild(FileUpload) public fileUpload: FileUpload; @ViewChild(FileUpload) public fileUpload: FileUpload;
listingCategory: 'Business' | 'Commercial Property'; listingsCategory = 'commercialProperty';
category: string; category: string;
location: string; location: string;
mode: 'edit' | 'create'; mode: 'edit' | 'create';
separator: '\n\n' separator: '\n\n';
listing: ListingType listing: CommercialPropertyListing;
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User; user: User;
maxFileSize = 3000000; maxFileSize = 3000000;
uploadUrl: string; uploadUrl: string;
environment = environment; environment = environment;
propertyImages: ImageProperty[] propertyImages: string[];
responsiveOptions = [ responsiveOptions = [
{ {
breakpoint: '1199px', breakpoint: '1199px',
numVisible: 1, numVisible: 1,
numScroll: 1 numScroll: 1,
}, },
{ {
breakpoint: '991px', breakpoint: '991px',
numVisible: 2, numVisible: 2,
numScroll: 1 numScroll: 1,
}, },
{ {
breakpoint: '767px', breakpoint: '767px',
numVisible: 1, numVisible: 1,
numScroll: 1 numScroll: 1,
} },
]; ];
config = { aspectRatio: 16 / 9 } config = { aspectRatio: 16 / 9 };
editorModules = TOOLBAR_OPTIONS editorModules = TOOLBAR_OPTIONS;
dialogRef: DynamicDialogRef | undefined; dialogRef: DynamicDialogRef | undefined;
draggedImage: ImageProperty draggedImage: ImageProperty;
faTrash = faTrash; faTrash = faTrash;
constructor(public selectOptions: SelectOptionsService, suggestions: string[] | undefined;
data: BusinessListing;
userId: string;
typesOfCommercialProperty = [];
constructor(
public selectOptions: SelectOptionsService,
private router: Router, private router: Router,
private activatedRoute: ActivatedRoute, private activatedRoute: ActivatedRoute,
private listingsService: ListingsService, private listingsService: ListingsService,
@ -101,41 +101,45 @@ export class EditListingComponent {
private imageService: ImageService, private imageService: ImageService,
private loadingService: LoadingService, private loadingService: LoadingService,
public dialogService: DialogService, public dialogService: DialogService,
private confirmationService: ConfirmationService) { private confirmationService: ConfirmationService,
this.user = this.userService.getUser(); private route: ActivatedRoute,
) {
// Abonniere Router-Events, um den aktiven Link zu ermitteln // Abonniere Router-Events, um den aktiven Link zu ermitteln
this.router.events.subscribe(event => { this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
this.mode = event.url === '/createListing' ? 'create' : 'edit'; this.mode = event.url === '/createCommercialPropertyListing' ? 'create' : 'edit';
} }
}); });
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
this.data = this.router.getCurrentNavigation().extras.state['data'];
}
});
this.typesOfCommercialProperty = selectOptions.typesOfCommercialProperty.map(e => {
return { name: e.name, value: parseInt(e.value) };
});
} }
async ngOnInit() { async ngOnInit() {
if (this.mode === 'edit') { if (this.mode === 'edit') {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id)); this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
} else { } else {
const uuid = sessionStorage.getItem('uuid') ? sessionStorage.getItem('uuid') : uuidv4(); this.listing = createGenericObject<CommercialPropertyListing>();
sessionStorage.setItem('uuid', uuid); this.listing.userId = await this.userService.getId();
this.listing = createGenericObject<BusinessListing>(); this.listing.title = this.data?.title;
this.listing.id = uuid this.listing.description = this.data?.description;
this.listing.userId = this.user.id
this.listing.listingsCategory = 'business';
} }
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`; this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id) this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
} }
async save() { async save() {
sessionStorage.removeItem('uuid') this.listing = await this.listingsService.save(this.listing, getListingType(this.listing));
await this.listingsService.save(this.listing, this.listing.listingsCategory); this.router.navigate(['editCommercialPropertyListing', this.listing.id]);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 }); this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 });
} }
suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) { async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state)) const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
this.suggestions = result.map(r => r.city).slice(0, 5); this.suggestions = result.map(r => r.city).slice(0, 5);
} }
@ -145,7 +149,7 @@ export class EditListingComponent {
data: { data: {
imageUrl: imageUrl, imageUrl: imageUrl,
fileUpload: this.fileUpload, fileUpload: this.fileUpload,
ratioVariable: false ratioVariable: false,
}, },
header: 'Edit Image', header: 'Edit Image',
width: '50vw', width: '50vw',
@ -155,38 +159,27 @@ export class EditListingComponent {
closable: false, closable: false,
breakpoints: { breakpoints: {
'960px': '75vw', '960px': '75vw',
'640px': '90vw' '640px': '90vw',
}, },
}); });
this.dialogRef.onClose.subscribe(cropper => { this.dialogRef.onClose.subscribe(cropper => {
if (cropper) { if (cropper) {
this.loadingService.startLoading('uploadImage'); this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async (blob) => { cropper.getCroppedCanvas().toBlob(async blob => {
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.id).subscribe(async (event) => { this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.id).subscribe(
async event => {
if (event.type === HttpEventType.Response) { if (event.type === HttpEventType.Response) {
console.log('Upload abgeschlossen', event.body); console.log('Upload abgeschlossen', event.body);
this.loadingService.stopLoading('uploadImage'); this.loadingService.stopLoading('uploadImage');
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id) this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
} }
}, error => console.error('Fehler beim Upload:', error)); },
error => console.error('Fehler beim Upload:', error),
);
}, 'image/jpg'); }, 'image/jpg');
cropper.destroy(); cropper.destroy();
} }
}) });
// this.dialogRef.onClose.subscribe(blob => {
// if (blob) {
// // this.loadingService.startLoading('uploadImage');
// setTimeout(()=>{
// this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.id).subscribe(async (event) => {
// if (event.type === HttpEventType.Response) {
// console.log('Upload abgeschlossen', event.body);
// // this.loadingService.stopLoading('uploadImage');
// this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
// }
// }, error => console.error('Fehler beim Upload:', error));
// },10)
// }
// });
} }
deleteConfirm(imageName: string) { deleteConfirm(imageName: string) {
@ -195,27 +188,28 @@ export class EditListingComponent {
message: `Do you want to delete this image ${imageName}?`, message: `Do you want to delete this image ${imageName}?`,
header: 'Delete Confirmation', header: 'Delete Confirmation',
icon: 'pi pi-info-circle', icon: 'pi pi-info-circle',
acceptButtonStyleClass: "p-button-danger p-button-text", acceptButtonStyleClass: 'p-button-danger p-button-text',
rejectButtonStyleClass: "p-button-text p-button-text", rejectButtonStyleClass: 'p-button-text p-button-text',
acceptIcon: "none", acceptIcon: 'none',
rejectIcon: "none", rejectIcon: 'none',
accept: async () => { accept: async () => {
await this.imageService.deleteListingImage(this.listing.id, imageName); await this.imageService.deleteListingImage(this.listing.id, imageName);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' }); this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id) this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
}, },
reject: () => { reject: () => {
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' }); // this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
console.log('deny') console.log('deny');
} },
}); });
} }
onDrop(event: { previousIndex: number; currentIndex: number }) { onDrop(event: { previousIndex: number; currentIndex: number }) {
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex); moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages) this.listingsService.changeImageOrder(this.listing.id, this.propertyImages);
}
changeListingCategory(value: 'business' | 'commercialProperty') {
routeListingWithState(this.router, value, this.listing);
} }
} }

View File

@ -1,201 +0,0 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<div *ngIf="listing" class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">{{mode==='create'?'New':'Edit'}} Listing</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Listing category</label>
<p-dropdown id="listingCategory" [options]="selectOptions?.listingCategories" [(ngModel)]="listing.listingsCategory" optionLabel="name"
optionValue="value" placeholder="Listing category" [disabled]="mode==='edit'"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="mb-4">
<label for="email" class="block font-medium text-900 mb-2">Title of Listing</label>
<input id="email" type="text" pInputText [(ngModel)]="listing.title">
</div>
<div>
<div class="mb-4">
<label for="description" class="block font-medium text-900 mb-2">Description</label>
<!-- <textarea id="description" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.description"></textarea> -->
<p-editor [(ngModel)]="listing.description" [style]="{ height: '320px' }" [modules]="editorModules">
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
</div>
@if (listing.listingsCategory==='business'){
<div class="mb-4">
<label for="type" class="block font-medium text-900 mb-2">Type of business</label>
<p-dropdown id="type" [options]="selectOptions?.typesOfBusiness" [(ngModel)]="listing.type" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Type of business"
[style]="{ width: '100%'}"></p-dropdown>
</div>
}
@if (listing.listingsCategory==='commercialProperty'){
<div class="mb-4">
<label for="type" class="block font-medium text-900 mb-2">Property Category</label>
<p-dropdown id="type" [options]="selectOptions?.typesOfCommercialProperty" [(ngModel)]="listing.type" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="Property Category"
[style]="{ width: '100%'}"></p-dropdown>
</div>
}
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="listingCategory" class="block font-medium text-900 mb-2">State</label>
<p-dropdown id="listingCategory" [options]="selectOptions?.states" [(ngModel)]="listing.state" optionLabel="name"
optionValue="value" [showClear]="true" placeholder="State"
[style]="{ width: '100%'}"></p-dropdown>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="listingCategory" class="block font-medium text-900 mb-2">City</label>
<p-autoComplete [(ngModel)]="listing.city" [suggestions]="suggestions" (completeMethod)="search($event)"></p-autoComplete>
</div>
</div>
@if (listing.listingsCategory==='commercialProperty'){
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="zipCode" class="block font-medium text-900 mb-2">Zip Code</label>
<input id="zipCode" type="text" pInputText [(ngModel)]="listing.zipCode">
</div>
<div class="mb-4 col-12 md:col-6">
<label for="county" class="block font-medium text-900 mb-2">County</label>
<input id="county" type="text" pInputText [(ngModel)]="listing.county">
</div>
</div>
}
</div>
</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
@if (listing.listingsCategory==='commercialProperty'){
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<div class="flex flex-column align-items-center flex-or">
<span class="font-medium text-900 mb-2">Property Pictures</span>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
<p-fileUpload mode="basic"
chooseLabel="Upload"
[customUpload]="true"
name="file"
accept="image/*"
[maxFileSize]="maxFileSize"
(onSelect)="select($event)"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4">
</p-fileUpload>
</div>
</div>
</div>
<div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop
(dropped)="onDrop($event)"
cdkDropListOrientation="horizontal">
@for (image of propertyImages; track image) {
<span cdkDropList mixedCdkDropList>
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
<img src="{{environment.apiBaseUrl}}/property/{{listing.id}}/{{image.name}}"
[alt]="image.name" class="shadow-2" cdkDrag>
<fa-icon [icon]="faTrash" (click)="deleteConfirm(image.name)"></fa-icon>
</div>
</span>
}
</div>
}
@if (listing.listingsCategory==='business'){
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="price" class="block font-medium text-900 mb-2">Price</label>
<!-- <p-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price" ></p-inputNumber> -->
<app-inputNumber mode="currency" currency="USD" locale="en-US" inputId="price" [(ngModel)]="listing.price"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="salesRevenue" class="block font-medium text-900 mb-2">Sales Revenue</label>
<app-inputNumber mode="currency" currency="USD" inputId="salesRevenue" [(ngModel)]="listing.salesRevenue"></app-inputNumber>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="cashFlow" class="block font-medium text-900 mb-2">Cash Flow</label>
<app-inputNumber mode="currency" currency="USD" inputId="cashFlow" [(ngModel)]="listing.cashFlow"></app-inputNumber>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="employees" class="block font-medium text-900 mb-2">Years Established Since</label>
<app-inputNumber mode="decimal" inputId="established" [(ngModel)]="listing.established"></app-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="employees" class="block font-medium text-900 mb-2">Employees</label>
<app-inputNumber mode="decimal" inputId="employees" [(ngModel)]="listing.employees"></app-inputNumber>
</div>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-4 ">
<p-checkbox [binary]="true" [(ngModel)]="listing.realEstateIncluded"></p-checkbox>
<span class="ml-2 text-900">Real Estate Included</span>
</div>
<div class="mb-4 col-12 md:col-4">
<p-checkbox [binary]="true" [(ngModel)]="listing.leasedLocation"></p-checkbox>
<span class="ml-2 text-900">Leased Location</span>
</div>
<div class="mb-4 col-12 md:col-4">
<p-checkbox [binary]="true" [(ngModel)]="listing.franchiseResale"></p-checkbox>
<span class="ml-2 text-900">Franchise Re-Sale</span>
</div>
</div>
<div class="mb-4">
<label for="supportAndTraining" class="block font-medium text-900 mb-2">Support & Training</label>
<!-- <textarea id="inventory" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.supportAndTraining"></textarea> -->
<input id="supportAndTraining" type="text" pInputText [(ngModel)]="listing.supportAndTraining">
</div>
<div class="mb-4">
<label for="reasonForSale" class="block font-medium text-900 mb-2">Reason for Sale</label>
<textarea id="reasonForSale" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.reasonForSale"></textarea>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="brokerLicensing" class="block font-medium text-900 mb-2">Broker Licensing</label>
<input id="brokerLicensing" type="text" pInputText [(ngModel)]="listing.brokerLicencing">
</div>
<div class="mb-4 col-12 md:col-6">
<label for="internalListingNumber" class="block font-medium text-900 mb-2">Internal Listing Number</label>
<app-inputNumber mode="decimal" inputId="internalListingNumber" type="text" [(ngModel)]="listing.internalListingNumber"></app-inputNumber>
</div>
</div>
<div class="mb-4">
<label for="internalListing" class="block font-medium text-900 mb-2">Internal Notes (Will not be shown on the listing, for your records only.)</label>
<input id="internalListing" type="text" pInputText [(ngModel)]="listing.internals">
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6 ">
<!-- <p-tag value="New"></p-tag> -->
<!-- <p-checkbox [binary]="true" [(ngModel)]="listing.draft"></p-checkbox> -->
<p-inputSwitch inputId="draft" [(ngModel)]="listing.draft"></p-inputSwitch>
<!-- <span class="ml-2 text-900">Share my data with contacts</span> -->
<span class="ml-2 text-900 absolute translate-y-5">Draft Mode (Will not be shown as public listing)</span>
</div>
</div>
}
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
}
</div>
</div>
</div>
</div>
</div>
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog>

View File

@ -1,26 +1,25 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { MenuAccountComponent } from '../../menu-account/menu-account.component'; import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import dataListings from '../../../../assets/data/listings.json'; import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { SharedModule } from '../../../shared/shared/shared.module';
import { UserService } from '../../../services/user.service';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service'; import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service'; import { SelectOptionsService } from '../../../services/select-options.service';
import { BusinessListing, ListingType, User } from '../../../../../../common-models/src/main.model'; import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
@Component({ @Component({
selector: 'app-favorites', selector: 'app-favorites',
standalone: true, standalone: true,
imports: [MenuAccountComponent, SharedModule], imports: [MenuAccountComponent, SharedModule],
templateUrl: './favorites.component.html', templateUrl: './favorites.component.html',
styleUrl: './favorites.component.scss' styleUrl: './favorites.component.scss',
}) })
export class FavoritesComponent { export class FavoritesComponent {
user: User; user: User;
listings: Array<ListingType> =[]//= dataListings as unknown as Array<BusinessListing>; listings: Array<ListingType> = []; //= dataListings as unknown as Array<BusinessListing>;
favorites: Array<ListingType> favorites: Array<ListingType>;
constructor(public userService: UserService, private listingsService: ListingsService, public selectOptions: SelectOptionsService) { constructor(public userService: UserService, private listingsService: ListingsService, public selectOptions: SelectOptionsService) {
this.user=this.userService.getUser(); this.user = this.userService.getKeycloakUser();
} }
async ngOnInit() { async ngOnInit() {
// this.listings=await lastValueFrom(this.listingsService.getAllListings()); // this.listings=await lastValueFrom(this.listingsService.getAllListings());

View File

@ -1,4 +1,3 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full"> <div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
<div class="p-fluid flex flex-column lg:flex-row"> <div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account> <menu-account></menu-account>
@ -7,7 +6,16 @@
<div class="surface-card p-5 shadow-2 border-round flex-auto"> <div class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">My Listings</div> <div class="text-900 font-semibold text-lg mt-3">My Listings</div>
<p-divider></p-divider> <p-divider></p-divider>
<p-table [value]="myListings" [tableStyle]="{ 'min-width': '50rem' }" dataKey="id" [paginator]="true" [rows]="10" [rowsPerPageOptions]="[10, 20, 50]" [showCurrentPageReport]="true" currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"> <p-table
[value]="myListings"
[tableStyle]="{ 'min-width': '50rem' }"
dataKey="id"
[paginator]="true"
[rows]="10"
[rowsPerPageOptions]="[10, 20, 50]"
[showCurrentPageReport]="true"
currentPageReportTemplate="Showing {first} to {last} of {totalRecords} entries"
>
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th class="wide-column">Title</th> <th class="wide-column">Title</th>
@ -20,9 +28,13 @@
<tr> <tr>
<td class="wide-column line-height-3">{{ listing.title }}</td> <td class="wide-column line-height-3">{{ listing.title }}</td>
<td>{{ selectOptions.getListingsCategory(listing.listingsCategory) }}</td> <td>{{ selectOptions.getListingsCategory(listing.listingsCategory) }}</td>
<td>{{ selectOptions.getState(listing.location) }}</td> <td>{{ selectOptions.getState(listing.state) }}</td>
<td> <td>
<button pButton pRipple icon="pi pi-pencil" class="p-button-rounded p-button-success mr-2" [routerLink]="['/editListing',listing.id]"></button> @if(isBusinessListing(listing)){
<button pButton pRipple icon="pi pi-pencil" class="p-button-rounded p-button-success mr-2" [routerLink]="['/editBusinessListing', listing.id]"></button>
} @if(isCommercialPropertyListing(listing)){
<button pButton pRipple icon="pi pi-pencil" class="p-button-rounded p-button-success mr-2" [routerLink]="['/editCommercialPropertyListing', listing.id]"></button>
}
<button pButton pRipple icon="pi pi-trash" class="p-button-rounded p-button-warning" (click)="confirm($event, listing)"></button> <button pButton pRipple icon="pi pi-trash" class="p-button-rounded p-button-warning" (click)="confirm($event, listing)"></button>
</td> </td>
</tr> </tr>

Some files were not shown because too many files have changed in this diff Show More