Umbau auf postgres 2. step

This commit is contained in:
Andreas Knuth 2024-04-22 22:26:44 +02:00
parent c90d6b72b7
commit 7f0f21b598
77 changed files with 3325 additions and 3066 deletions

View File

@ -17,6 +17,49 @@
"sourceMaps": true,
"stopOnEntry": false,
"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,
},
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,8 @@
"generate": "drizzle-kit generate:pg",
"drop": "drizzle-kit drop",
"migrate": "tsx src/drizzle/migrate.ts",
"import": "tsx src/drizzle/import.ts"
"import": "tsx src/drizzle/import.ts",
"generateTypes":"tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts"
},
"dependencies": {
"@nestjs-modules/mailer": "^1.10.3",
@ -55,6 +56,8 @@
"winston": "^3.11.0"
},
"devDependencies": {
"@babel/parser": "^7.24.4",
"@babel/traverse": "^7.24.1",
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
@ -70,11 +73,13 @@
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"commander": "^12.0.0",
"drizzle-kit": "^0.20.16",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"kysely-codegen": "^0.15.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",

View File

@ -18,7 +18,7 @@ import { PG_CONNECTION } from './schema.js';
// ssl: true,
});
return drizzle(pool, { schema });
return drizzle(pool, { schema, logger:true });
},
},
],

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

@ -4,7 +4,8 @@ import pkg from 'pg';
const { Pool } = pkg;
import * as schema from './schema.js';
import { readFileSync } from 'fs';
import { User } from './schema.js';
import { BusinessListing, CommercialPropertyListing, User } from 'src/models/db.model.js';
const connectionString = process.env.DATABASE_URL
// const pool = new Pool({connectionString})
@ -31,7 +32,7 @@ for (const user of userData) {
//Business Listings
filePath = `./data/businesses.json`
data = readFileSync(filePath, 'utf8');
const businessJsonData = JSON.parse(data) as schema.BusinessListing[]; // Erwartet ein Array von Objekten
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
for (const business of businessJsonData) {
delete business.id
@ -42,7 +43,7 @@ for (const business of businessJsonData) {
//Corporate Listings
filePath = `./data/commercials.json`
data = readFileSync(filePath, 'utf8');
const commercialJsonData = JSON.parse(data) as schema.CommercialPropertyListing[]; // Erwartet ein Array von Objekten
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
for (const commercial of commercialJsonData) {
delete commercial.id
commercial.created = getRandomDateWithinLastYear();

View File

@ -1,20 +1,20 @@
CREATE TABLE IF NOT EXISTS "businesses" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"userId" uuid,
"type" varchar(255),
"type" integer,
"title" varchar(255),
"description" text,
"city" varchar(255),
"state" char(2),
"price" numeric(10, 2),
"price" double precision,
"favoritesForUser" varchar(30)[],
"draft" boolean,
"listingsCategory" varchar(255),
"realEstateIncluded" boolean,
"leasedLocation" boolean,
"franchiseResale" boolean,
"salesRevenue" numeric(10, 2),
"cashFlow" numeric(10, 2),
"salesRevenue" double precision,
"cashFlow" double precision,
"supportAndTraining" text,
"employees" integer,
"established" integer,
@ -31,12 +31,12 @@ CREATE TABLE IF NOT EXISTS "businesses" (
CREATE TABLE IF NOT EXISTS "commercials" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"userId" uuid,
"type" varchar(255),
"type" integer,
"title" varchar(255),
"description" text,
"city" varchar(255),
"state" char(2),
"price" numeric(10, 2),
"price" double precision,
"favoritesForUser" varchar(30)[],
"hideImage" boolean,
"draft" boolean,
@ -46,6 +46,7 @@ CREATE TABLE IF NOT EXISTS "commercials" (
"website" varchar(255),
"phoneNumber" varchar(255),
"imageOrder" varchar(30)[],
"imagePath" varchar(30)[],
"created" timestamp,
"updated" timestamp,
"visits" integer,

View File

@ -1 +0,0 @@
ALTER TABLE "commercials" ADD COLUMN "imagePath" varchar(30)[];

View File

@ -1,4 +0,0 @@
ALTER TABLE "businesses" ALTER COLUMN "price" SET DATA TYPE numeric(12, 2);--> statement-breakpoint
ALTER TABLE "businesses" ALTER COLUMN "salesRevenue" SET DATA TYPE numeric(12, 2);--> statement-breakpoint
ALTER TABLE "businesses" ALTER COLUMN "cashFlow" SET DATA TYPE numeric(12, 2);--> statement-breakpoint
ALTER TABLE "commercials" ALTER COLUMN "price" SET DATA TYPE numeric(12, 2);

View File

@ -1,4 +0,0 @@
ALTER TABLE "businesses" ALTER COLUMN "price" SET DATA TYPE double precision;--> statement-breakpoint
ALTER TABLE "businesses" ALTER COLUMN "salesRevenue" SET DATA TYPE double precision;--> statement-breakpoint
ALTER TABLE "businesses" ALTER COLUMN "cashFlow" SET DATA TYPE double precision;--> statement-breakpoint
ALTER TABLE "commercials" ALTER COLUMN "price" SET DATA TYPE double precision;

View File

@ -1,5 +1,5 @@
{
"id": "221e028b-75cd-43da-83aa-9e3908ea9788",
"id": "e8d0776a-ea8b-4c75-8a3a-e741620c4c4d",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "5",
"dialect": "pg",
@ -23,7 +23,7 @@
},
"type": {
"name": "type",
"type": "varchar(255)",
"type": "integer",
"primaryKey": false,
"notNull": false
},
@ -53,7 +53,7 @@
},
"price": {
"name": "price",
"type": "numeric(10, 2)",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
@ -95,13 +95,13 @@
},
"salesRevenue": {
"name": "salesRevenue",
"type": "numeric(10, 2)",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "numeric(10, 2)",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
@ -210,7 +210,7 @@
},
"type": {
"name": "type",
"type": "varchar(255)",
"type": "integer",
"primaryKey": false,
"notNull": false
},
@ -240,7 +240,7 @@
},
"price": {
"name": "price",
"type": "numeric(10, 2)",
"type": "double precision",
"primaryKey": false,
"notNull": false
},
@ -298,6 +298,12 @@
"primaryKey": false,
"notNull": false
},
"imagePath": {
"name": "imagePath",
"type": "varchar(30)[]",
"primaryKey": false,
"notNull": false
},
"created": {
"name": "created",
"type": "timestamp",

View File

@ -1,460 +0,0 @@
{
"id": "e3a1fac7-b93b-49e6-9ab4-b6dbb2362188",
"prevId": "221e028b-75cd-43da-83aa-9e3908ea9788",
"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": "varchar(255)",
"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": "numeric(10, 2)",
"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": "numeric(10, 2)",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "numeric(10, 2)",
"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": "varchar(255)",
"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": "numeric(10, 2)",
"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(30)[]",
"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

@ -1,460 +0,0 @@
{
"id": "7bf04c81-2206-4dfd-a4d1-3f47dc91feff",
"prevId": "e3a1fac7-b93b-49e6-9ab4-b6dbb2362188",
"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": "varchar(255)",
"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": "numeric(12, 2)",
"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": "numeric(12, 2)",
"primaryKey": false,
"notNull": false
},
"cashFlow": {
"name": "cashFlow",
"type": "numeric(12, 2)",
"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": "varchar(255)",
"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": "numeric(12, 2)",
"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(30)[]",
"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

@ -1,460 +0,0 @@
{
"id": "5e653a3e-6fb0-4ab1-9cce-46886e4a0c41",
"prevId": "7bf04c81-2206-4dfd-a4d1-3f47dc91feff",
"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": "varchar(255)",
"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": "varchar(255)",
"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(30)[]",
"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

@ -5,29 +5,8 @@
{
"idx": 0,
"version": "5",
"when": 1713620510315,
"tag": "0000_wet_wasp",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1713620815960,
"tag": "0001_burly_daimon_hellstrom",
"breakpoints": true
},
{
"idx": 2,
"version": "5",
"when": 1713631666329,
"tag": "0002_same_loners",
"breakpoints": true
},
{
"idx": 3,
"version": "5",
"when": 1713638770444,
"tag": "0003_solid_senator_kelly",
"when": 1713791559934,
"tag": "0000_safe_natasha_romanoff",
"breakpoints": true
}
]

View File

@ -24,7 +24,7 @@ export const users = pgTable('users', {
export const businesses = pgTable('businesses', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('userId').references(()=>users.id),
type: varchar('type', { length: 255 }),
type: integer('type'),
title: varchar('title', { length: 255 }),
description: text('description'),
city: varchar('city', { length: 255 }),
@ -50,10 +50,11 @@ export const businesses = pgTable('businesses', {
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
});
export const commercials = pgTable('commercials', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('userId').references(()=>users.id),
type: varchar('type', { length: 255 }),
type: integer('type'),
title: varchar('title', { length: 255 }),
description: text('description'),
city: varchar('city', { length: 255 }),
@ -74,6 +75,4 @@ export const commercials = pgTable('commercials', {
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
});
export type BusinessListing = InferInsertModel<typeof businesses>;
export type CommercialPropertyListing = InferInsertModel<typeof commercials>;
export type User = InferSelectModel<typeof users>;

View File

@ -14,8 +14,7 @@ import { DrizzleModule } from '../drizzle/drizzle.module.js';
@Module({
imports: [RedisModule,DrizzleModule
],
imports: [RedisModule,DrizzleModule],
controllers: [BusinessListingsController, CommercialPropertyListingsController,UnknownListingsController,BrokerListingsController],
providers: [ListingsService,FileService,UserService],
exports: [ListingsService],

View File

@ -11,10 +11,11 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { EntityData, EntityId, Schema, SchemaDefinition } from 'redis-om';
import { SQL, eq, gte, ilike, lte, sql, and} from 'drizzle-orm';
import { BusinessListing, CommercialPropertyListing, PG_CONNECTION, businesses, commercials, } from '../drizzle/schema.js';
import { PG_CONNECTION, businesses, commercials, } from '../drizzle/schema.js';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../drizzle/schema.js';
import { PgTableFn, PgTableWithColumns, QueryBuilder } from 'drizzle-orm/pg-core';
import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js';
@Injectable()
export class ListingsService {
@ -22,17 +23,6 @@ export class ListingsService {
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,) {
}
// private buildWhereClause(criteria: ListingCriteria): SQL {
// const finalSql = sql`1=1`;
// finalSql.append(criteria.type ? sql` AND 'type' = ${criteria.type}` : sql``)
// finalSql.append(criteria.state ? sql` AND data->>'state' = ${criteria.state}` : sql``)
// finalSql.append(criteria.minPrice ? sql` AND CAST(data->>'price' AS NUMERIC) >= ${parseFloat(criteria.minPrice)}` : sql``)
// finalSql.append(criteria.maxPrice ? sql` AND CAST(data->>'price' AS NUMERIC) < ${parseFloat(criteria.maxPrice)}` : sql``)
// finalSql.append(criteria.realEstateChecked !== undefined ? sql` AND CAST(data->>'realEstateIncluded' AS BOOLEAN) = ${criteria.realEstateChecked}` : sql``)
// finalSql.append(criteria.title ? sql` AND LOWER(data->>'title') LIKE LOWER('%' || ${criteria.title} || '%')` : sql``)
// return finalSql
// }
private getConditions(criteria: ListingCriteria): any[] {
const conditions = [];
if (criteria.type) {
@ -76,20 +66,10 @@ export class ListingsService {
return result[0] as BusinessListing | CommercialPropertyListing
}
// async findByPriceRange(minPrice: number, maxPrice: number, table: typeof businesses | typeof commercials): Promise<BusinessesJson[]> {
// return this.conn.select().from(table).where(sql`${table}->>'price' BETWEEN ${minPrice} AND ${maxPrice}`);
// }
// async findByState(state: string, table: typeof businesses | typeof commercials): Promise<BusinessesJson[]> {
// return this.conn.select().from(table).where(sql`${table}->>'state' = ${state}`);
// }
async findByUserId(userId: string, table: typeof businesses | typeof commercials): Promise<BusinessListing[] | CommercialPropertyListing[]> {
return await this.conn.select().from(table).where(eq(table.userId, userId)) as BusinessListing[] | CommercialPropertyListing[]
}
// async findByTitleContains(title: string, table: typeof businesses | typeof commercials): Promise<BusinessesJson[]> {
// return this.conn.select().from(table).where(sql`${table}->>'title' ILIKE '%' || ${title} || '%'`);
// }
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
const newListing = { data, created: data.created, updated: data.updated, visits: 0, last_visit: null }
const [createdListing] = await this.conn.insert(table).values(newListing).returning();

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;
listingsCategory?: string;
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;
}
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;
}

View File

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

View File

@ -0,0 +1,156 @@
import { BusinessListing, CommercialPropertyListing } from "./db.model";
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 interface ListingCriteria {
start:number,
length:number,
page:number,
pageCount:number,
type:number,
state:string,
minPrice:number,
maxPrice:number,
realEstateChecked:boolean,
title:string,
listingsCategory:'business' | 'commercialProperty',
category:'professional|broker'
}
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;
}
export interface Sender {
name?: string;
email?: string;
phoneNumber?: string;
state?: string;
comments?: string;
}
export interface ImageProperty {
id:string;
code:string;
name:string;
}

View File

@ -2,8 +2,7 @@ import { Body, Controller, Get, Inject, Param, Post, Put } from '@nestjs/common'
import { UserService } from './user.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { User } from 'src/drizzle/schema.js';
import { User } from 'src/models/db.model.js';
@Controller('user')
export class UserController {

View File

@ -3,9 +3,10 @@ import { UserController } from './user.controller.js';
import { UserService } from './user.service.js';
import { RedisModule } from '../redis/redis.module.js';
import { FileService } from '../file/file.service.js';
import { DrizzleModule } from '../drizzle/drizzle.module.js';
@Module({
imports: [RedisModule],
imports: [DrizzleModule],
controllers: [UserController],
providers: [UserService,FileService]
})

View File

@ -4,42 +4,45 @@ import { Entity, Repository, Schema } from 'redis-om';
import { ListingCriteria } from '../models/main.model.js';
import { REDIS_CLIENT } from '../redis/redis.module.js';
import { FileService } from '../file/file.service.js';
import { User } from 'src/drizzle/schema.js';
import { User } from 'src/models/db.model.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { PG_CONNECTION } from 'src/drizzle/schema.js';
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
import * as schema from '../drizzle/schema.js';
import { eq, sql,and } from 'drizzle-orm';
@Injectable()
export class UserService {
userRepository:Repository;
userSchema = new Schema('user',{
id: { type: 'string' },
firstname: { type: 'string' },
lastname: { type: 'string' },
email: { type: 'string' },
phoneNumber: { type: 'string' },
companyOverview:{ type: 'string' },
companyWebsite:{ type: 'string' },
companyLocation:{ type: 'string' },
offeredServices:{ type: 'string' },
areasServed:{ type: 'string[]' },
names:{ type: 'string[]', path:'$.licensedIn.name' },
values:{ type: 'string[]', path:'$.licensedIn.value' }
}, {
dataStructure: 'JSON'
})
constructor(private fileService:FileService){
// this.userRepository = new Repository(this.userSchema, redis)
// this.userRepository.createIndex();
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,private fileService:FileService) {
}
private getConditions(criteria: ListingCriteria): any[] {
const conditions = [];
if (criteria.state) {
conditions.push();
}
return conditions;
}
async getUserById( id:string){
const user = await this.userRepository.fetch(id) as User;
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.hasProfile=this.fileService.hasProfile(id);
return user;
}
async saveUser(user:any):Promise<User>{
return await this.userRepository.save(user.id,user) as User
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){
return await this.userRepository.search().return.all();
const users = await this.conn.execute(sql`SELECT * FROM users WHERE EXISTS (SELECT 1 FROM unnest(users."areasServed") AS area WHERE area LIKE '%' || ${criteria.state} || '%')`)
return users.rows
}
}

View File

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

View File

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

View File

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

View File

@ -11,18 +11,32 @@ import { authGuard } from './guards/auth.guard';
import { PricingComponent } from './pages/pricing/pricing.component';
import { LogoutComponent } from './components/logout/logout.component';
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
import { BusinessListingsComponent } from './pages/listings/business-listings/business-listings.component';
import { CommercialPropertyListingsComponent } from './pages/listings/commercial-property-listings/commercial-property-listings.component';
import { BrokerListingsComponent } from './pages/listings/broker-listings/broker-listings.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';
export const routes: Routes = [
{
path: 'listings/:type',
component: ListingsComponent,
},
// {
// path: 'listings/:type',
// component: ListingsComponent,
// },
// Umleitung von /listing zu /listing/business
{
path: 'listings',
pathMatch: 'full',
redirectTo: 'listings/business',
path: 'businessListings',
component: BusinessListingsComponent,
runGuardsAndResolvers:'always'
},
{
path: 'commercialPropertyListings',
component: CommercialPropertyListingsComponent,
runGuardsAndResolvers:'always'
},
{
path: 'brokerListings',
component: BrokerListingsComponent,
runGuardsAndResolvers:'always'
},
{
@ -47,15 +61,25 @@ export const routes: Routes = [
canActivate: [authGuard],
},
{
path: 'editListing/:id',
component: EditListingComponent,
path: 'editBusinessListing/:id',
component: EditBusinessListingComponent,
canActivate: [authGuard],
},
{
path: 'createListing',
component: EditListingComponent,
path: 'createBusinessListing',
component: EditBusinessListingComponent,
canActivate: [authGuard],
},
{
path: 'editCommercialPropertyListing/:id',
component: EditCommercialPropertyListingComponent,
canActivate: [authGuard],
},
{
path: 'createCommercialPropertyListing',
component: EditCommercialPropertyListingComponent,
canActivate: [authGuard],
},
{
path: 'myListings',
component: MyListingComponent,

View File

@ -10,8 +10,7 @@ import { TabMenuModule } from 'primeng/tabmenu';
import { Observable } from 'rxjs';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { Router } from '@angular/router';
import { User } from '../../../../../common-models/src/main.model';
import {User} from '../../../../../bizmatch-server/src/models/db.model'
@Component({
selector: 'header',
standalone: true,

View File

@ -6,9 +6,10 @@ import { HttpEventType } from '@angular/common/http';
import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
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 { SelectButtonModule } from 'primeng/selectbutton';
import { KeyValueRatio } from '../../../../../bizmatch-server/src/models/main.model';
export const stateOptions:KeyValueRatio[]=[
{label:'16/9',value:16/9},
{label:'1/1',value:1},

View File

@ -18,13 +18,14 @@ 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 { ImageProperty, ListingCriteria, ListingType, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'app-details-listing',
standalone: true,

View File

@ -90,7 +90,7 @@
">
<div class="text-500 w-full md:w-2 font-medium">Licensed In</div>
<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>

View File

@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { SharedModule } from '../../../shared/shared/shared.module';
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 { UserService } from '../../../services/user.service';
@ -11,6 +11,8 @@ import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ImageService } from '../../../services/image.service';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeyValue, ListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model';
@Component({
selector: 'app-details-user',
@ -29,6 +31,7 @@ export class DetailsUserComponent {
userListings:BusinessListing[]
companyOverview:SafeHtml;
offeredServices:SafeHtml;
userLicensedIn :KeyValue[]
constructor(private activatedRoute: ActivatedRoute,
private router: Router,
private userService: UserService,
@ -41,7 +44,7 @@ export class DetailsUserComponent {
async ngOnInit() {
this.user = await this.userService.getById(this.id);
this.userLicensedIn = this.user.licensedIn.map(l=>{return {name:l.split('|')[0],value:l.split('|')[1]}})
this.userListings = await this.listingsService.getListingByUserId(this.id);
this.user$ = this.userService.getUserObservable();
this.companyOverview=this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview);

View File

@ -12,8 +12,9 @@ import { SelectOptionsService } from '../../services/select-options.service';
import { UserService } from '../../services/user.service';
import onChange from 'on-change';
import { getCriteriaStateObject, getSessionStorageHandler } from '../../utils/utils';
import { ListingCriteria, User } from '../../../../../common-models/src/main.model';
import { Observable } from 'rxjs';
import { ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'app-home',
standalone: true,

View File

@ -0,0 +1,59 @@
<div id="sky-line" class="hidden-lg-down">
</div>
<div class="search">
<div class="wrapper">
<div class="grid p-4 align-items-center">
<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 (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

@ -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,115 @@
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 { 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 { Observable, lastValueFrom } from 'rxjs';
import { PaginatorModule } from 'primeng/paginator';
import onChange from 'on-change';
import { InitEditableRow } from 'primeng/table';
import { SelectOptionsService } from '../../../services/select-options.service';
import { ListingsService } from '../../../services/listings.service';
import { UserService } from '../../../services/user.service';
import { ImageService } from '../../../services/image.service';
import { createGenericObject, getCriteriaStateObject, getListingType, getSessionStorageHandler } from '../../../utils/utils';
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'app-broker-listings',
standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, StyleClassModule, ToggleButtonModule, RouterModule, PaginatorModule],
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;
// 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.init()
})
}
async ngOnInit(){
}
async init(){
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,'professionals_brokers');
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.criteria.start=event.first;
this.criteria.length=event.rows;
this.criteria.page=event.page;
this.criteria.pageCount=event.pageCount;
}
imageErrorHandler(listing: ListingType) {
// listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann
}
}

View File

@ -0,0 +1,87 @@
<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>
}
<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 listings; 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 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>
}
</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,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,114 @@
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 { 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 { Observable, lastValueFrom } from 'rxjs';
import { PaginatorModule } from 'primeng/paginator';
import onChange from 'on-change';
import { InitEditableRow } from 'primeng/table';
import { SelectOptionsService } from '../../../services/select-options.service';
import { ListingsService } from '../../../services/listings.service';
import { UserService } from '../../../services/user.service';
import { ImageService } from '../../../services/image.service';
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'app-business-listings',
standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, StyleClassModule, ToggleButtonModule, RouterModule, PaginatorModule],
templateUrl: './business-listings.component.html',
styleUrl: './business-listings.component.scss'
})
export class BusinessListingsComponent {
environment=environment;
listings: Array<BusinessListing>;
users: Array<User>
filteredListings: Array<BusinessListing>;
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.init()
})
}
async ngOnInit(){
}
async init(){
this.users=[]
this.listings=await this.listingsService.getListings(this.criteria,'business');
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();
}
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,'business');
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.criteria.start=event.first;
this.criteria.length=event.rows;
this.criteria.page=event.page;
this.criteria.pageCount=event.pageCount;
}
imageErrorHandler(listing: ListingType) {
// listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann
}
}

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 [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 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]}}"
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>
}
</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,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,113 @@
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 { 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 { Observable, lastValueFrom } from 'rxjs';
import { PaginatorModule } from 'primeng/paginator';
import onChange from 'on-change';
import { InitEditableRow } from 'primeng/table';
import { SelectOptionsService } from '../../../services/select-options.service';
import { ListingsService } from '../../../services/listings.service';
import { UserService } from '../../../services/user.service';
import { ImageService } from '../../../services/image.service';
import { createGenericObject, getCriteriaStateObject, getSessionStorageHandler } from '../../../utils/utils';
import { ListingCriteria, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'app-commercial-property-listings',
standalone: true,
imports: [CommonModule, StyleClassModule, ButtonModule, CheckboxModule, InputTextModule, DropdownModule, FormsModule, StyleClassModule, ToggleButtonModule, RouterModule, PaginatorModule],
templateUrl: './commercial-property-listings.component.html',
styleUrl: './commercial-property-listings.component.scss'
})
export class CommercialPropertyListingsComponent {
environment=environment;
listings: Array<CommercialPropertyListing>;
users: Array<User>
filteredListings: Array<CommercialPropertyListing>;
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()
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.init()
})
}
async ngOnInit(){
}
async init(){
this.users=[]
this.listings=await this.listingsService.getListings(this.criteria,'commercialProperty');
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();
}
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,'commercialProperty');
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);
this.criteria.start=event.first;
this.criteria.length=event.rows;
this.criteria.page=event.page;
this.criteria.pageCount=event.pageCount;
}
imageErrorHandler(listing: ListingType) {
// listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann
}
}

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 listings; 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

@ -16,9 +16,11 @@ 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';
import { ListingCriteria, ListingType } from '../../../../../bizmatch-server/src/models/main.model';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'app-listings',
standalone: true,
@ -27,102 +29,96 @@ import { ImageService } from '../../services/image.service';
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;
// environment=environment;
// listings: Array<ListingType>;
// 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) {
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()
})
// 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);
// }
// 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();
}
// this.setStates();
// this.totalRecords=this.listings.length
// 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);
this.criteria.start=event.first;
this.criteria.length=event.rows;
this.criteria.page=event.page;
this.criteria.pageCount=event.pageCount;
}
imageErrorHandler(listing: ListingType) {
listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann
}
// }
// 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.criteria.start=event.first;
// this.criteria.length=event.rows;
// this.criteria.page=event.page;
// this.criteria.pageCount=event.pageCount;
// }
// imageErrorHandler(listing: ListingType) {
// listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann
// }
}

View File

@ -74,7 +74,7 @@
</div>
<div>
<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="flex col-12 md:col-6">
<p-dropdown id="states" [options]="selectOptions?.states" [(ngModel)]="licensedIn.name"
@ -192,21 +192,3 @@
</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

@ -23,7 +23,6 @@ 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';
@ -36,6 +35,8 @@ import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dy
import { ImageCropperComponent, stateOptions } from '../../../components/image-cropper/image-cropper.component';
import Quill from 'quill'
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, Invoice, KeyValue, Subscription } from '../../../../../../bizmatch-server/src/models/main.model';
@Component({
selector: 'app-account',
standalone: true,
@ -58,6 +59,7 @@ export class AccountComponent {
dialogRef: DynamicDialogRef | undefined;
environment = environment
editorModules = TOOLBAR_OPTIONS
userLicensedIn :KeyValue[]
constructor(public userService: UserService,
private subscriptionService: SubscriptionsService,
private messageService: MessageService,
@ -70,9 +72,10 @@ export class AccountComponent {
public dialogService: DialogService) {}
async ngOnInit() {
this.user = await this.userService.getById(this.id);
this.userLicensedIn = this.user.licensedIn.map(l=>{return {name:l.split('|')[0],value:l.split('|')[1]}})
this.userSubscriptions = await lastValueFrom(this.subscriptionService.getAllSubscriptions());
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 ? `${environment.apiBaseUrl}/profile/${this.user.id}.avif` : `/assets/images/placeholder.png`
@ -105,10 +108,10 @@ export class AccountComponent {
this.suggestions = result.map(r => `${r.city} - ${r.state_code}`).slice(0, 5);
}
addLicence() {
this.user.licensedIn.push({ name: '', value: '' });
this.userLicensedIn.push({ name: '', value: '' });
}
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') {
@ -141,7 +144,7 @@ export class AccountComponent {
if (event.type === HttpEventType.Response) {
this.loadingService.stopLoading('uploadImage');
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()}`
} else {
this.user.hasProfile=true;

View File

@ -0,0 +1,155 @@
<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"
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]="selectOptions?.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,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

@ -0,0 +1,207 @@
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 { 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, getListingType } from '../../../utils/utils';
import { ListingsService } from '../../../services/listings.service';
import { lastValueFrom } from 'rxjs';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { ConfirmationService, MessageService } from 'primeng/api';
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 { 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';
import { AutoCompleteCompleteEvent, ImageProperty, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'create-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 = 'commercialProperty'
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: ImageProperty[]
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;
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) {
this.user = this.userService.getUser();
// Abonniere Router-Events, um den aktiven Link zu ermitteln
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.mode = event.url === '/createListing' ? 'create' : 'edit';
}
});
}
async ngOnInit() {
if (this.mode === 'edit') {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id));
} else {
const uuid = sessionStorage.getItem('uuid') ? sessionStorage.getItem('uuid') : uuidv4();
sessionStorage.setItem('uuid', uuid);
this.listing = createGenericObject<BusinessListing>();
this.listing.id = uuid
this.listing.userId = this.user.id
}
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
}
async save() {
sessionStorage.removeItem('uuid')
await this.listingsService.save(this.listing, getListingType(this.listing));
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);
}
select(event: any) {
const imageUrl = URL.createObjectURL(event.files[0]);
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: {
imageUrl: imageUrl,
fileUpload: this.fileUpload,
ratioVariable: false
},
header: 'Edit Image',
width: '50vw',
modal: true,
closeOnEscape: true,
keepInViewport: true,
closable: false,
breakpoints: {
'960px': '75vw',
'640px': '90vw'
},
});
this.dialogRef.onClose.subscribe(cropper => {
if (cropper){
this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async (blob) => {
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.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));
}, 'image/jpg');
cropper.destroy();
}
})
}
deleteConfirm(imageName: string) {
this.confirmationService.confirm({
target: event.target as EventTarget,
message: `Do you want to delete this image ${imageName}?`,
header: 'Delete Confirmation',
icon: 'pi pi-info-circle',
acceptButtonStyleClass: "p-button-danger p-button-text",
rejectButtonStyleClass: "p-button-text p-button-text",
acceptIcon: "none",
rejectIcon: "none",
accept: async () => {
await this.imageService.deleteListingImage(this.listing.id, imageName);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
},
reject: () => {
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
console.log('deny')
}
});
}
onDrop(event: { previousIndex: number; currentIndex: number }) {
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages)
}
}

View File

@ -0,0 +1,110 @@
<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"
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]="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="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>
<!-- <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>
<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

@ -0,0 +1,207 @@
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 { 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, getListingType } from '../../../utils/utils';
import { ListingsService } from '../../../services/listings.service';
import { lastValueFrom } from 'rxjs';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { ConfirmationService, MessageService } from 'primeng/api';
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 { 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';
import { AutoCompleteCompleteEvent, ImageProperty, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'create-listing',
standalone: true,
imports: [SharedModule, ArrayToStringPipe, InputNumberModule, CarouselModule,
DialogModule, AngularCropperjsModule, FileUploadModule, EditorModule, DynamicDialogModule, DragDropModule,
ConfirmDialogModule, MixedCdkDragDropModule],
providers: [MessageService, DialogService, ConfirmationService],
templateUrl: './edit-commercial-property-listing.component.html',
styleUrl: './edit-commercial-property-listing.component.scss'
})
export class EditCommercialPropertyListingComponent {
@ViewChild(FileUpload) public fileUpload: FileUpload;
listingsCategory = 'commercialProperty'
category: string;
location: string;
mode: 'edit' | 'create';
separator: '\n\n'
listing: CommercialPropertyListing
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User;
maxFileSize = 3000000;
uploadUrl: string;
environment = environment;
propertyImages: ImageProperty[]
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;
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) {
this.user = this.userService.getUser();
// Abonniere Router-Events, um den aktiven Link zu ermitteln
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.mode = event.url === '/createListing' ? 'create' : 'edit';
}
});
}
async ngOnInit() {
if (this.mode === 'edit') {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id));
} else {
const uuid = sessionStorage.getItem('uuid') ? sessionStorage.getItem('uuid') : uuidv4();
sessionStorage.setItem('uuid', uuid);
this.listing = createGenericObject<CommercialPropertyListing>();
this.listing.id = uuid
this.listing.userId = this.user.id
}
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
}
async save() {
sessionStorage.removeItem('uuid')
await this.listingsService.save(this.listing, getListingType(this.listing));
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);
}
select(event: any) {
const imageUrl = URL.createObjectURL(event.files[0]);
this.dialogRef = this.dialogService.open(ImageCropperComponent, {
data: {
imageUrl: imageUrl,
fileUpload: this.fileUpload,
ratioVariable: false
},
header: 'Edit Image',
width: '50vw',
modal: true,
closeOnEscape: true,
keepInViewport: true,
closable: false,
breakpoints: {
'960px': '75vw',
'640px': '90vw'
},
});
this.dialogRef.onClose.subscribe(cropper => {
if (cropper){
this.loadingService.startLoading('uploadImage');
cropper.getCroppedCanvas().toBlob(async (blob) => {
this.imageService.uploadImage(blob, 'uploadPropertyPicture', this.listing.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));
}, 'image/jpg');
cropper.destroy();
}
})
}
deleteConfirm(imageName: string) {
this.confirmationService.confirm({
target: event.target as EventTarget,
message: `Do you want to delete this image ${imageName}?`,
header: 'Delete Confirmation',
icon: 'pi pi-info-circle',
acceptButtonStyleClass: "p-button-danger p-button-text",
rejectButtonStyleClass: "p-button-text p-button-text",
acceptIcon: "none",
rejectIcon: "none",
accept: async () => {
await this.imageService.deleteListingImage(this.listing.id, imageName);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Image deleted' });
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
},
reject: () => {
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
console.log('deny')
}
});
}
onDrop(event: { previousIndex: number; currentIndex: number }) {
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages)
}
}

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

@ -17,7 +17,7 @@ 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 { createGenericObject, getListingType } from '../../../utils/utils';
import { ListingsService } from '../../../services/listings.service';
import { lastValueFrom } from 'rxjs';
@ -25,7 +25,6 @@ import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { UserService } from '../../../services/user.service';
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';
@ -45,6 +44,8 @@ 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';
import { AutoCompleteCompleteEvent, ImageProperty, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
@Component({
selector: 'create-listing',
standalone: true,
@ -120,7 +121,6 @@ export class EditListingComponent {
this.listing = createGenericObject<BusinessListing>();
this.listing.id = uuid
this.listing.userId = this.user.id
this.listing.listingsCategory = 'business';
}
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
@ -128,7 +128,7 @@ export class EditListingComponent {
async save() {
sessionStorage.removeItem('uuid')
await this.listingsService.save(this.listing, this.listing.listingsCategory);
await this.listingsService.save(this.listing, getListingType(this.listing));
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 });
}
@ -173,20 +173,6 @@ export class EditListingComponent {
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) {

View File

@ -6,7 +6,8 @@ import { UserService } from '../../../services/user.service';
import { lastValueFrom } from 'rxjs';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { BusinessListing, ListingType, User } from '../../../../../../common-models/src/main.model';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
@Component({
selector: 'app-favorites',

View File

@ -7,7 +7,10 @@ import { ListingsService } from '../../../services/listings.service';
import { lastValueFrom } from 'rxjs';
import { SelectOptionsService } from '../../../services/select-options.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { BusinessListing, ListingType, User } from '../../../../../../common-models/src/main.model';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { getListingType } from '../../../utils/utils';
@Component({
selector: 'app-my-listing',
standalone: true,
@ -30,7 +33,7 @@ export class MyListingComponent {
}
async deleteListing(listing:ListingType){
await this.listingsService.deleteListing(listing.id,listing.listingsCategory);
await this.listingsService.deleteListing(listing.id,getListingType(listing));
// this.listings=await lastValueFrom(this.listingsService.getAllListings());
this.myListings=this.listings.filter(l=>l.userId===this.user.id);
}

View File

@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { lastValueFrom } from 'rxjs';
import { ImageType } from '../../../../common-models/src/main.model';
import { ImageType } from '../../../../bizmatch-server/src/models/main.model';
@Injectable({
providedIn: 'root'

View File

@ -2,9 +2,10 @@ import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, lastValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
import { BusinessListing, CommercialPropertyListing, ImageProperty, ListingCriteria, ListingType, ResponseBusinessListing, ResponseBusinessListingArray, ResponseCommercialPropertyListing, ResponseCommercialPropertyListingArray } from '../../../../common-models/src/main.model';
import onChange from 'on-change';
import { getSessionStorageHandler } from '../utils/utils';
import { getListingType, getSessionStorageHandler } from '../utils/utils';
import { ImageProperty, ListingCriteria, ListingType, ResponseBusinessListingArray, ResponseCommercialPropertyListingArray } from '../../../../bizmatch-server/src/models/main.model';
import { BusinessListing } from '../../../../bizmatch-server/src/models/db.model';
@Injectable({
providedIn: 'root'
@ -17,8 +18,8 @@ export class ListingsService {
// getAllListings():Observable<ListingType[]>{
// return this.http.get<ListingType[]>(`${this.apiBaseUrl}/bizmatch/business-listings`);
// }
async getListings(criteria:ListingCriteria):Promise<ListingType[]>{
const result = await lastValueFrom(this.http.post<ResponseBusinessListingArray|ResponseCommercialPropertyListingArray>(`${this.apiBaseUrl}/bizmatch/listings/${criteria.listingsCategory}/search`,criteria));
async getListings(criteria:ListingCriteria,listingsCategory:'business'|'professionals_brokers'|'commercialProperty'):Promise<ListingType[]>{
const result = await lastValueFrom(this.http.post<ResponseBusinessListingArray>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/search`,criteria));
return result.data;
}
getListingById(id:string,listingsCategory?:'business'|'commercialProperty'):Observable<ListingType>{

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core';
import { MailInfo } from '../../../../common-models/src/main.model';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { lastValueFrom } from 'rxjs';
import { MailInfo } from '../../../../bizmatch-server/src/models/main.model';
@Injectable({
providedIn: 'root'

View File

@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { InitEditableRow } from 'primeng/table';
import { lastValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
import { KeyValue, KeyValueStyle } from '../../../../common-models/src/main.model';
import { KeyValue, KeyValueStyle } from '../../../../bizmatch-server/src/models/main.model';
@Injectable({
providedIn: 'root',
@ -38,12 +38,11 @@ export class SelectOptionsService {
getState(value:string):string{
return this.states.find(l=>l.value===value)?.name
}
getBusiness(value:string):string{
return this.typesOfBusiness.find(t=>t.value===value)?.name
getBusiness(value:number):string{
return this.typesOfBusiness.find(t=>t.value===String(value))?.name
}
getCommercialProperty(value:string):string{
return this.typesOfCommercialProperty.find(t=>t.value===value)?.name
getCommercialProperty(value:number):string{
return this.typesOfCommercialProperty.find(t=>t.value===String(value))?.name
}
getListingsCategory(value:string):string{
return this.listingCategories.find(l=>l.value===value)?.name
@ -70,11 +69,11 @@ export class SelectOptionsService {
getTextColorType(value:string):string{
return this.typesOfBusiness.find(c=>c.value===value)?.textColorClass
}
getBgColorType(value:string):string{
return this.typesOfBusiness.find(c=>c.value===value)?.bgColorClass
getBgColorType(value:number):string{
return this.typesOfBusiness.find(c=>c.value===String(value))?.bgColorClass
}
getIconAndTextColorType(value:string):string{
const category = this.typesOfBusiness.find(c=>c.value===value)
getIconAndTextColorType(value:number):string{
const category = this.typesOfBusiness.find(c=>c.value===String(value))
return `${category?.icon} ${category?.textColorClass}`
}
}

View File

@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { Subscription } from '../../../../common-models/src/main.model';
import { Subscription } from '../../../../bizmatch-server/src/models/main.model';
@Injectable({
providedIn: 'root'

View File

@ -4,9 +4,10 @@ import { jwtDecode } from 'jwt-decode';
import { Observable, distinctUntilChanged, filter, from, lastValueFrom, map } from 'rxjs';
import { CommonModule } from '@angular/common';
import { KeycloakService } from './keycloak.service';
import { JwtToken, ListingCriteria, User } from '../../../../common-models/src/main.model';
import { environment } from '../../environments/environment';
import { HttpClient } from '@angular/common/http';
import { User } from '../../../../bizmatch-server/src/models/db.model';
import { JwtToken, ListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
@Injectable({
providedIn: 'root'

View File

@ -1,5 +1,6 @@
import { INFO, ConsoleFormattedStream, createLogger as _createLogger, stdSerializers } from "browser-bunyan";
import { ListingCriteria } from "../../../../common-models/src/main.model";
import { ListingCriteria } from "../../../../bizmatch-server/src/models/main.model";
import { BusinessListing, CommercialPropertyListing } from "../../../../bizmatch-server/src/models/db.model";
export function createGenericObject<T>(): T {
// Ein leeres Objekt vom Typ T erstellen
@ -32,7 +33,10 @@ export function createGenericObject<T>(): T {
export function getCriteriaStateObject(){
const initialState = createGenericObject<ListingCriteria>();
initialState.listingsCategory='business';
const storedState = sessionStorage.getItem('criteria');
return storedState ? JSON.parse(storedState) : initialState;
}
export function getListingType(listing:BusinessListing|CommercialPropertyListing){
return listing.type<100?'business':'commercialProperty';
}

View File

@ -1,212 +0,0 @@
import { BusinessListing, CommercialPropertyListing } from "src/drizzle/schema.js";
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 interface Listing {
// id: string;
// userId: string;
// type: string; //enum
// title: string;
// description: string;
// city: string,
// state: string;//enum
// price?: number;
// favoritesForUser:Array<string>;
// hideImage?:boolean;
// draft?:boolean;
// created:Date;
// updated:Date;
// }
// export interface BusinessListing extends Listing {
// listingsCategory: 'business'; //enum
// realEstateIncluded?: boolean;
// leasedLocation?:boolean;
// franchiseResale?:boolean;
// salesRevenue?: number;
// cashFlow?: number;
// supportAndTraining?: string;
// employees?: number;
// established?: number;
// internalListingNumber?:number;
// reasonForSale?: string;
// brokerLicencing?: string;
// internals?: string;
// }
// export interface CommercialPropertyListing extends Listing {
// listingsCategory: 'commercialProperty'; //enum
// zipCode:number;
// county:string
// email?: string;
// website?: string;
// phoneNumber?: string;
// imageOrder?:ImageProperty[];
// }
// export interface UserBase {
// 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;
// }
// export interface User extends UserBase {
// licensedIn?:KeyValue[];
// }
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 interface ListingCriteria {
start:number,
length:number,
page:number,
pageCount:number,
type:string,
state:string,
minPrice:number,
maxPrice:number,
realEstateChecked:boolean,
title:string,
listingsCategory:'business'|'professionals_brokers'|'commercialProperty',
category:'professional|broker'
}
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;
}
export interface Sender {
name?: string;
email?: string;
phoneNumber?: string;
state?: string;
comments?: string;
}
export interface ImageProperty {
id:string;
code:string;
name:string;
}

View File

@ -41,5 +41,21 @@
"${workspaceFolder}/**/*.js"
]
},
{
"type": "node",
"request": "launch",
"name": "generateTypes",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/generateTypes.js",
"outFiles": [
"${workspaceFolder}/**/*.js"
],
"args": [
"--sourcefile","schema.ts",
"--outfile", "model.ts",
],
},
]
}

14
crawler/UserInterface.ts Normal file
View File

@ -0,0 +1,14 @@
export interface User {
id: string;
firstname: string;
lastname: string;
email: string;
phoneNumber?: string;
description?: string;
created_at: Date;
updated_at?: Date;
isActive: boolean;
score?: number;
balance?: number;
tags?: string[];
}

View File

@ -1,30 +1,30 @@
import yargs from 'yargs'
import fs from 'fs-extra';
import { hideBin } from 'yargs/helpers'
import { BusinessListing } from "../common-models/src/main.model"
// import yargs from 'yargs'
// import fs from 'fs-extra';
// import { hideBin } from 'yargs/helpers'
// import { BusinessListing } from "../common-models/src/main.model"
const argv = yargs(hideBin(process.argv)).argv
// const argv = yargs(hideBin(process.argv)).argv
if (!argv.userId){
console.log(' --userId [any valid userId]')
process.exit(1)
}
// if (!argv.userId){
// console.log(' --userId [any valid userId]')
// process.exit(1)
// }
(async () => {
console
const response = await fetch('http://localhost:3000/bizmatch/listings', {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
const listings:Array<BusinessListing> = await response.json();
for (const listing of listings) {
listing.userId=argv.userId;
listing.created=new Date()
listing.updated=new Date()
const response = await fetch(`http://localhost:3000/bizmatch/listings/${listing.id}`, {
method: 'PUT',
body: JSON.stringify(listing),
headers: { 'Content-Type': 'application/json' },
});
}
})();
// (async () => {
// console
// const response = await fetch('http://localhost:3000/bizmatch/listings', {
// method: 'GET',
// headers: { 'Content-Type': 'application/json' },
// })
// const listings:Array<BusinessListing> = await response.json();
// for (const listing of listings) {
// listing.userId=argv.userId;
// listing.created=new Date()
// listing.updated=new Date()
// const response = await fetch(`http://localhost:3000/bizmatch/listings/${listing.id}`, {
// method: 'PUT',
// body: JSON.stringify(listing),
// headers: { 'Content-Type': 'application/json' },
// });
// }
// })();

View File

@ -2698,5 +2698,161 @@
"description": "<p>Acquire this stunning waterfront restaurant and event venue located in the historic district of Annapolis, Maryland, offering breathtaking views of the Chesapeake Bay and Annapolis Harbor. The property features a beautifully restored building, multiple dining areas, and a large outdoor terrace, making it a highly sought-after destination for dining, weddings, and special events.</p><h3>Waterfront restaurant and event venue features:</h3><p>- 10,000 square feet of indoor and outdoor dining and event space<br>- Seating capacity for up to 200 guests<br>- Expansive waterfront terrace with unobstructed views of the Chesapeake Bay<br>- Fully equipped commercial kitchen and bar<br>- Elegantly appointed private dining rooms for intimate gatherings<br>- Prime location in the heart of Annapolis' historic district</p><p>Invest in this exceptional waterfront restaurant and event venue and capitalize on the strong demand for unique dining experiences and memorable event spaces in the charming and historically significant Annapolis market, a popular destination for tourists and locals alike.</p>",
"type": "100",
"imageOrder": []
},
{
"id": "a1b2c3d4-e5f6-7g8h-9i0j-1k2l3m4n5o6p",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Scenic Hill Country Ranch",
"state": "TX",
"hasImages": true,
"price": 7500000,
"city": "Fredericksburg",
"description": "<h2>Stunning Ranch in the Heart of Texas Hill Country</h2><p>Discover the beauty and tranquility of this breathtaking 500-acre ranch nestled in the picturesque Texas Hill Country. With its rolling hills, lush pastures, and crystal-clear streams, this property offers a perfect blend of natural beauty and recreational opportunities.</p><p>Key features include:</p><ul><li>Main residence: 4,500 sq ft luxury home with 5 bedrooms and 4.5 bathrooms</li><li>Guest house: Charming 2-bedroom, 2-bathroom cottage</li><li>Equestrian facilities: 10-stall barn, riding arena, and miles of riding trails</li><li>Hunting and fishing: Abundant wildlife and stocked ponds</li></ul><p>This exceptional ranch is a rare find and presents a unique opportunity for those seeking a luxury retreat or a profitable hunting and recreation property.</p>",
"type": "101",
"imageOrder": []
},
{
"id": "5e6f7g8h-9i0j-1k2l-3m4n-5o6p7q8r9s0t",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Thriving Gas Station and Convenience Store",
"state": "TX",
"hasImages": true,
"price": 1750000,
"city": "San Antonio",
"description": "<p>Well-established gas station and convenience store located in a high-traffic area of San Antonio. This profitable business offers a prime opportunity for investors seeking a stable income stream and growth potential.</p><h3>Property and Business Highlights:</h3><ul><li>Large 1-acre lot with ample parking</li><li>4,000 sq ft convenience store with modern fixtures</li><li>High-volume fuel sales and diverse in-store product mix</li><li>Long-term supplier contracts and loyal customer base</li></ul><p>Take advantage of this turn-key investment opportunity in San Antonio's robust retail market.</p>",
"type": "100",
"imageOrder": []
},
{
"id": "7g8h9i0j-1k2l-3m4n-5o6p-7q8r9s0t1u2v",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Full-Service Car Wash in High-Growth Austin Suburb",
"state": "TX",
"hasImages": true,
"price": 3500000,
"city": "Round Rock",
"description": "<p>Highly profitable, full-service car wash located in the rapidly growing Austin suburb of Round Rock. This well-maintained facility has a strong reputation for quality service and boasts a loyal customer base in a high-income residential area.</p><h3>Business and Property Highlights:</h3><ul><li>1.5-acre site with spacious parking and attractive landscaping</li><li>4 self-serve bays and 2 automatic wash tunnels</li><li>Modern equipment and computerized payment systems</li><li>Consistent year-over-year revenue growth and high margins</li></ul><p>Capitalize on the booming Austin metro market with this established, high-performing car wash business.</p>",
"type": "100",
"imageOrder": []
},
{
"id": "9i0j1k2l-3m4n-5o6p-7q8r-9s0t1u2v3w4x",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Historic Church Building in Downtown San Antonio",
"state": "TX",
"hasImages": true,
"price": 2200000,
"city": "San Antonio",
"description": "<p>Unique opportunity to acquire a beautifully preserved historic church building in the heart of downtown San Antonio. This iconic property offers a blend of old-world charm and modern amenities, making it an ideal space for a variety of commercial or community uses.</p><h3>Property Highlights:</h3><ul><li>10,000 sq ft of versatile interior space with soaring ceilings</li><li>Meticulously maintained original architectural features</li><li>Newly updated electrical, plumbing, and HVAC systems</li><li>Prime location near popular attractions and public transportation</li></ul><p>Embrace the rich history and endless potential of this one-of-a-kind property in San Antonio's vibrant downtown district.</p>",
"type": "106",
"imageOrder": []
},
{
"id": "0j1k2l3m-4n5o-6p7q-8r9s-0t1u2v3w4x5y",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Established Bar and Live Music Venue in Austin",
"state": "TX",
"hasImages": true,
"price": 1800000,
"city": "Austin",
"description": "<h2>Iconic Austin Bar and Music Hotspot</h2><p>Own a piece of Austin's legendary live music scene with this well-known bar and music venue. Located in the heart of the city's entertainment district, this business has a loyal following and a reputation for showcasing top talent.</p><p>Property and business features:</p><ul><li>6,000 sq ft building with a spacious stage and multiple bars</li><li>Fully equipped kitchen and outdoor patio area</li><li>Established relationships with booking agents and promoters</li><li>Consistent revenue and growth potential in Austin's thriving nightlife scene</li></ul><p>Take center stage in Austin's vibrant music community with this turnkey business opportunity.</p>",
"type": "100",
"imageOrder": []
},
{
"id": "1k2l3m4n-5o6p-7q8r-9s0t-1u2v3w4x5y6z",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Private Airport and Aviation Community",
"state": "TX",
"hasImages": true,
"price": 15000000,
"city": "Fredericksburg",
"description": "<p>Rare opportunity to acquire a private airport and aviation community nestled in the scenic Texas Hill Country. This unique property features a well-maintained runway, hangar facilities, and a gated residential airpark.</p><h3>Property Highlights:</h3><ul><li>4,500' x 75' paved and lighted runway</li><li>50 acres of land with room for expansion</li><li>20 aircraft hangars and a private fuel farm</li><li>Gated community with 15 aviation-themed homes</li></ul><p>Elevate your investment portfolio with this one-of-a-kind aviation property in the heart of Texas.</p>",
"type": "101",
"imageOrder": []
},
{
"id": "0t1u2v3w-4x5y-6z7a-8b9c-0d1e2f3g4h5i",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Scenic Hill Country RV Resort",
"state": "TX",
"hasImages": true,
"price": 6500000,
"city": "New Braunfels",
"description": "<h2>Premium RV Resort in the Heart of Texas Hill Country</h2><p>Discover the beauty and tranquility of the Texas Hill Country at this top-rated RV resort in New Braunfels. Nestled along the banks of the Guadalupe River, this immaculately maintained property offers guests a perfect blend of natural beauty, modern amenities, and endless recreational opportunities.</p><p>Resort features and highlights:</p><ul><li>150 spacious RV sites with full hookups and concrete pads</li><li>Sparkling swimming pool, hot tub, and sun deck area</li><li>Private river access with designated swimming and fishing spots</li><li>Clubhouse with game room, fitness center, and meeting spaces</li><li>Close proximity to popular attractions like Schlitterbahn Water Park and Gruene Historic District</li></ul><p>This profitable RV resort presents a unique investment opportunity in one of Texas' most sought-after vacation destinations. With strong occupancy rates, a loyal customer base, and multiple revenue streams, this turn-key property is poised for continued growth and success.</p><p>Capitalize on the booming Texas tourism market with this exceptional RV resort in the picturesque Hill Country region.</p>",
"type": "101",
"imageOrder": []
},
{
"id": "3m4n5o6p-7q8r-9s0t-1u2v-3w4x5y6z7a8b",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Turnkey Industrial Manufacturing Facility",
"state": "TX",
"hasImages": true,
"price": 9750000,
"city": "Fort Worth",
"description": "<p>Fully equipped industrial manufacturing facility located in Fort Worth's thriving Alliance commercial district. This modern property offers a turnkey solution for businesses seeking to expand or relocate their operations to the Dallas-Fort Worth Metroplex.</p><h3>Property Highlights:</h3><ul><li>150,000 sq ft of manufacturing and warehouse space</li><li>State-of-the-art production equipment and utility systems</li><li>Abundant power, water, and natural gas capacity</li><li>Easy access to I-35W, I-820, and Alliance Airport</li></ul><p>Unlock your company's growth potential with this move-in-ready manufacturing facility in one of the nation's top industrial markets.</p>",
"type": "102",
"imageOrder": []
},
{
"id": "6p7q8r9s-0t1u-2v3w-4x5y-6z7a8b9c0d1e",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Full-Service Auto Dealership and Service Center",
"state": "TX",
"hasImages": true,
"price": 5500000,
"city": "San Marcos",
"description": "<p>Well-established, full-service auto dealership and service center located in the rapidly growing city of San Marcos. This successful business benefits from a prime location along the heavily trafficked I-35 corridor between Austin and San Antonio.</p><h3>Property and Business Highlights:</h3><ul><li>5-acre site with modern showroom and service facilities</li><li>Franchise rights for a major domestic auto brand</li><li>Loyal customer base and strong revenue growth</li><li>Highly trained sales and service staff in place</li></ul><p>Take the wheel of this profitable auto dealership in one of Texas' fastest-growing markets.</p>",
"type": "100",
"imageOrder": []
},
{
"id": "7q8r9s0t-1u2v-3w4x-5y6z-7a8b9c0d1e2f",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Multi-Tenant Medical Office Complex",
"state": "TX",
"hasImages": true,
"price": 8750000,
"city": "Plano",
"description": "<p>Newly constructed, multi-tenant medical office complex in the heart of Plano's booming Legacy West district. This state-of-the-art facility offers a prime location and modern amenities, making it an attractive choice for a wide range of healthcare providers.</p><h3>Property Features and Highlights:</h3><ul><li>80,000 sq ft of customizable medical office space</li><li>Abundant parking and easy access from Dallas North Tollway</li><li>On-site surgical center and diagnostic imaging suites</li><li>Close proximity to major hospitals and residential areas</li></ul><p>Capitalize on the growing demand for high-quality medical office space in one of the Dallas area's most affluent and dynamic communities.</p>",
"type": "103",
"imageOrder": []
},
{
"id": "8r9s0t1u-2v3w-4x5y-6z7a-8b9c0d1e2f3g",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Upscale Boutique Motel in Trendy East Austin",
"state": "TX",
"hasImages": true,
"price": 4200000,
"city": "Austin",
"description": "<h2>Chic Motel in the Heart of Austin's Creative Hub</h2><p>Unique opportunity to acquire a beautifully renovated, upscale boutique motel in Austin's vibrant East Side neighborhood. This trendy property offers a perfect blend of retro charm and modern amenities, attracting a hip, creative clientele.</p><p>Property features and highlights:</p><ul><li>25 stylishly appointed guest rooms with premium finishes</li><li>Instagrammable pool area and cozy outdoor lounge spaces</li><li>Artisanal coffee bar and curated retail shop</li><li>Walking distance to popular restaurants, bars, and music venues</li></ul><p>Tap into Austin's booming tourism market with this one-of-a-kind boutique motel in the city's trendiest neighborhood.</p>",
"type": "100",
"imageOrder": []
},
{
"id": "9s0t1u2v-3w4x-5y6z-7a8b-9c0d1e2f3g4h",
"userId": "",
"listingsCategory": "commercialProperty",
"title": "Warehouse and Showroom Space in Houston's Design District",
"state": "TX",
"hasImages": true,
"price": 3750000,
"city": "Houston",
"description": "<p>Expansive warehouse and showroom space located in the heart of Houston's thriving Design District. This versatile property offers a prime location and flexible layout, making it ideal for a variety of wholesale, distribution, and creative business uses.</p><h3>Property Highlights:</h3><ul><li>30,000 sq ft of warehouse and showroom space</li><li>High ceilings, ample natural light, and modern finishes</li><li>Loading docks and easy access to I-10 and I-45</li><li>Surrounded by premier design showrooms and art galleries</li></ul><p>Showcase your business in one of Houston's most creative and dynamic commercial districts.</p>",
"type": "102",
"imageOrder": []
}
]

View File

@ -1,13 +0,0 @@
import fs from 'fs-extra';
(async () => {
const listings = await fs.readJson('./users.json');
//listings.forEach(element => {
for (const listing of listings) {
const response = await fetch('http://localhost:3000/bizmatch/user', {
method: 'POST',
body: JSON.stringify(listing),
headers: { 'Content-Type': 'application/json' },
});
}
})();

View File

@ -1,6 +1,6 @@
// import puppeteer, { Browser, ElementHandle, Page } from 'puppeteer-core';
import puppeteer, { Browser, ElementHandle, Page } from 'puppeteer';
import { BusinessListing } from "../common-models/src/main.model"
import currency from 'currency.js';
import fs from 'fs-extra'
@ -51,7 +51,7 @@ async function getParentElementText(elementHandle: ElementHandle<Element> | null
return textContent?(textContent.length<2?textContent.join():textContent):null
}
async function extractListingData(page: Page): Promise<BusinessListing | null> {
async function extractListingData(page: Page): Promise<any | null> {
const labels = {
summaryLabel: 'Summary',
descriptionLabel: 'Description',

View File

@ -3,7 +3,6 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
@ -11,6 +10,9 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/parser": "^7.24.4",
"@babel/traverse": "^7.24.1",
"commander": "^12.0.0",
"typescript": "^5.4.5"
},
"dependencies": {

View File

@ -3,7 +3,7 @@ const { Pool } = pkg;
import fsextra from 'fs-extra';
const { fstat, readFileSync, writeJsonSync } = fsextra;
import { v4 as uuidv4 } from 'uuid';
import { CommercialPropertyListing, User } from '../common-models/src/main.model';
// PostgreSQL Verbindungskonfiguration
const pool = new Pool({
user: 'bizmatch',
@ -70,7 +70,7 @@ async function importBusinesses() {
async function importUser() {
const filePath = './data/broker.json'
const data: string = readFileSync(filePath, 'utf8');
const jsonData: User[] = JSON.parse(data); // Erwartet ein Array von Objekten
const jsonData: any[] = JSON.parse(data); // Erwartet ein Array von Objekten
await pool.query('drop table if exists users');
await pool.query(`CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
@ -96,7 +96,7 @@ async function importUser() {
async function importCommercials() {
const filePath = './data/commercials.json'
const data: string = readFileSync(filePath, 'utf8');
const jsonData: CommercialPropertyListing[]|any = JSON.parse(data); // Erwartet ein Array von Objekten
const jsonData: any[]|any = JSON.parse(data); // Erwartet ein Array von Objekten
await pool.query('drop table if exists commercials');
await pool.query(`CREATE TABLE commercials (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),

View File

@ -1,80 +1,81 @@
import { pgTable, timestamp, integer, jsonb, uuid } from 'drizzle-orm/pg-core';
import { InferModel } from 'drizzle-orm';
import { sql } from 'drizzle-orm';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
// import { pgTable, timestamp, integer, jsonb, uuid } from 'drizzle-orm/pg-core';
// import { InferModel } from 'drizzle-orm';
// import { sql } from 'drizzle-orm';
// import { drizzle } from 'drizzle-orm/node-postgres';
// import { Pool } from 'pg';
// Definiere den benutzerdefinierten Typ für das JSON-Objekt
type BusinessData = {
id?: string;
firstname: string;
lastname: string;
email: string;
phoneNumber: string;
companyLocation: string;
hasCompanyLogo: boolean;
hasProfile: boolean;
};
// // Definiere den benutzerdefinierten Typ für das JSON-Objekt
// type BusinessData = {
// id?: string;
// firstname: string;
// lastname: string;
// email: string;
// phoneNumber: string;
// companyLocation: string;
// hasCompanyLogo: boolean;
// hasProfile: boolean;
// };
// Definiere die Tabelle "businesses"
const businesses = pgTable('businesses', {
id: uuid('id').primaryKey(),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('last_visit'),
data: jsonb('data'),
});
// // Definiere die Tabelle "businesses"
// const businesses = pgTable('businesses', {
// id: uuid('id').primaryKey(),
// created: timestamp('created'),
// updated: timestamp('updated'),
// visits: integer('visits'),
// lastVisit: timestamp('last_visit'),
// data: jsonb('data'),
// });
// Definiere den Typ für das Modell
type Business = InferModel<typeof businesses, 'select'>;
// // Definiere den Typ für das Modell
// type Business = InferModel<typeof businesses, 'select'>;
// Erstelle eine Verbindung zur Datenbank
const pool = new Pool({
// Konfiguriere die Verbindungsoptionen
});
// // Erstelle eine Verbindung zur Datenbank
// const pool = new Pool({
// // Konfiguriere die Verbindungsoptionen
// });
const db = drizzle(pool);
// const db = drizzle(pool);
// Beispiel für das Einfügen eines neuen Datensatzes
const insertBusiness = async () => {
const businessData: BusinessData = {
firstname: 'Robert',
lastname: 'Jackson',
email: 'robert.jackson@texasbizbrokers.com',
phoneNumber: '(214) 555-7890',
companyLocation: 'Dallas - TX',
hasCompanyLogo: true,
hasProfile: true,
};
// // Beispiel für das Einfügen eines neuen Datensatzes
// const insertBusiness = async () => {
// const businessData: BusinessData = {
// firstname: 'Robert',
// lastname: 'Jackson',
// email: 'robert.jackson@texasbizbrokers.com',
// phoneNumber: '(214) 555-7890',
// companyLocation: 'Dallas - TX',
// hasCompanyLogo: true,
// hasProfile: true,
// };
const [insertedBusiness] = await db
.with({
new_business: sql<{ generated_id: string; created: Date; updated: Date; visits: number; last_visit: Date; data: BusinessData }>`(${(qb) => {
return qb
.select({
generated_id: sql`uuid_generate_v4()`,
created: sql`NOW()`,
updated: sql`NOW()`,
visits: sql`0`,
last_visit: sql`NOW()`,
data: sql`jsonb_set(${JSON.stringify(businessData)}::jsonb, '{id}', to_jsonb(uuid_generate_v4()))`,
});
}})`
})
.insert(businesses)
.values((eb) => ({
id: eb.generated_id,
created: eb.created,
updated: eb.updated,
visits: eb.visits,
lastVisit: eb.last_visit,
data: sql`jsonb_set(${eb.data}::jsonb, '{id}', to_jsonb(${eb.generated_id}))`,
}))
.returning({ generatedId: businesses.id, jsonData: businesses.data });
// const [insertedBusiness] = await db
// .$with('sq')
// .as({
// new_business: sql<{ generated_id: string; created: Date; updated: Date; visits: number; last_visit: Date; data: BusinessData }>`(${(qb) => {
// return qb
// .select({
// generated_id: sql`uuid_generate_v4()`,
// created: sql`NOW()`,
// updated: sql`NOW()`,
// visits: sql`0`,
// last_visit: sql`NOW()`,
// data: sql`jsonb_set(${JSON.stringify(businessData)}::jsonb, '{id}', to_jsonb(uuid_generate_v4()))`,
// });
// }})`
// })
// .insert(businesses)
// .values((eb) => ({
// id: eb.generated_id,
// created: eb.created,
// updated: eb.updated,
// visits: eb.visits,
// lastVisit: eb.last_visit,
// data: sql`jsonb_set(${eb.data}::jsonb, '{id}', to_jsonb(${eb.generated_id}))`,
// }))
// .returning({ generatedId: businesses.id, jsonData: businesses.data });
console.log('Generated ID:', insertedBusiness.generatedId);
console.log('JSON Data:', insertedBusiness.jsonData);
};
// console.log('Generated ID:', insertedBusiness.generatedId);
// console.log('JSON Data:', insertedBusiness.jsonData);
// };
insertBusiness();
// insertBusiness();

77
crawler/schema.ts Normal file
View File

@ -0,0 +1,77 @@
import { integer, serial, text, pgTable, timestamp, jsonb, varchar, char, numeric, boolean, uuid, real, doublePrecision } from 'drizzle-orm/pg-core';
import { InferInsertModel, InferModel, InferModelFromColumns, InferSelectModel, relations, sql } from 'drizzle-orm';
export const PG_CONNECTION = 'PG_CONNECTION';
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(),
});
export const businesses = pgTable('businesses', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('userId').references(()=>users.id),
type: varchar('type', { length: 255 }),
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: varchar('type', { length: 255 }),
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(),
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:30}).array(),
imagePath:varchar('imagePath',{length:30}).array(),
created: timestamp('created'),
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
});

View File

@ -25,9 +25,9 @@
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ESNext", /* Specify what module code is generated. */
// "module": "Node16", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "moduleResolution": "Node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */

View File

@ -1,7 +1,7 @@
import yargs from 'yargs'
import fs from 'fs-extra';
import { hideBin } from 'yargs/helpers'
import { BusinessListing } from "../common-models/src/main.model"
//const argv = yargs(hideBin(process.argv)).argv
// if (!argv.userId){
@ -19,7 +19,7 @@ import { BusinessListing } from "../common-models/src/main.model"
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
const listings:Array<BusinessListing> = await response.json();
const listings:Array<any> = await response.json();
for (const listing of listings) {
const option = selectOptions.locations.find(l=>l.name.toLowerCase()===listing.state.toLowerCase());
if (option){