Fehlerbehebung & Start Vector Search

This commit is contained in:
Andreas Knuth 2024-07-11 17:09:35 +02:00
parent 7bd5e1aaf8
commit b4644ea295
20 changed files with 381 additions and 245 deletions

View File

@ -36,7 +36,7 @@
"@nestjs/serve-static": "^4.0.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.30.8",
"drizzle-orm": "^0.32.0",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8",
"jwks-rsa": "^3.1.0",
@ -44,13 +44,13 @@
"nest-winston": "^1.9.4",
"nodemailer": "^6.9.10",
"nodemailer-smtp-transport": "^2.7.4",
"openai": "^4.52.6",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"pg": "^8.11.5",
"redis": "^4.6.13",
"redis-om": "^0.4.3",
"pgvector": "^0.2.0",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"sharp": "^0.33.2",
@ -77,7 +77,7 @@
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"commander": "^12.0.0",
"drizzle-kit": "^0.20.16",
"drizzle-kit": "^0.23.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",

View File

@ -2,6 +2,7 @@ import 'dotenv/config';
import { drizzle } from 'drizzle-orm/node-postgres';
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
import fs from 'fs-extra';
import OpenAI from 'openai';
import { join } from 'path';
import pkg from 'pg';
import { rimraf } from 'rimraf';
@ -11,6 +12,10 @@ import { emailToDirName } from 'src/models/main.model.js';
import * as schema from './schema.js';
const { Pool } = pkg;
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen
});
const connectionString = process.env.DATABASE_URL;
// const pool = new Pool({connectionString})
const client = new Pool({ connectionString });
@ -124,6 +129,14 @@ for (const commercial of commercialJsonData) {
//End
await client.end();
async function createEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: text,
});
return response.data[0].embedding;
}
function getRandomItem<T>(arr: T[]): T {
if (arr.length === 0) {
throw new Error('The array is empty.');

View File

@ -1,6 +1,5 @@
import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar, vector } from 'drizzle-orm/pg-core';
import { AreasServed, LicensedIn } from 'src/models/db.model';
export const PG_CONNECTION = 'PG_CONNECTION';
export const genderEnum = pgEnum('gender', ['male', 'female']);
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'professional']);
@ -58,6 +57,9 @@ export const businesses = pgTable('businesses', {
updated: timestamp('updated'),
visits: integer('visits'),
lastVisit: timestamp('lastVisit'),
// Neue Spalte für das OpenAI Embedding
embedding: vector('embedding', { dimensions: 1536 }),
// embedding: sql`vector(1536)`,
});
export const commercials = pgTable('commercials', {

View File

@ -30,3 +30,4 @@
</div>
</div>
}
<app-message-container></app-message-container>

View File

@ -10,13 +10,14 @@ import { ListingCriteria } from '../../../bizmatch-server/src/models/main.model'
import build from '../build';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component';
import { LoadingService } from './services/loading.service';
import { UserService } from './services/user.service';
import { createDefaultListingCriteria } from './utils/utils';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent],
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent],
providers: [],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',

View File

@ -41,19 +41,21 @@
</div>
<ul class="py-2" aria-labelledby="user-menu-button">
<li>
<a routerLink="/account" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
<a routerLink="/account" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Account</a>
</li>
<li>
<a routerLink="/createBusinessListing" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Create Listing</a>
<a routerLink="/createBusinessListing" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
>Create Listing</a
>
</li>
<li>
<a routerLink="/myListings" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Listings</a>
<a routerLink="/myListings" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">My Listings</a>
</li>
<li>
<a routerLink="/emailUs" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">EMail Us</a>
<a routerLink="/emailUs" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">EMail Us</a>
</li>
<li>
<a routerLink="/logout" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
<a routerLink="/logout" (click)="closeDropdown()" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white">Logout</a>
</li>
</ul>
</div>
@ -93,6 +95,7 @@
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/businessListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
aria-current="page"
(click)="closeMenus()"
>Businesses</a
>
</li>
@ -102,6 +105,7 @@
routerLink="/commercialPropertyListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/commercialPropertyListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
(click)="closeMenus()"
>Properties</a
>
</li>
@ -111,6 +115,7 @@
routerLink="/brokerListings"
[ngClass]="{ 'bg-blue-700 text-white md:text-blue-700 md:bg-transparent md:dark:text-blue-500': isActive('/brokerListings') }"
class="block py-2 px-3 rounded hover:bg-gray-100 md:hover:bg-transparent md:hover:text-blue-700 md:p-0 dark:text-white md:dark:hover:text-blue-500 dark:hover:bg-gray-700 dark:hover:text-white md:dark:hover:bg-transparent dark:border-gray-700"
(click)="closeMenus()"
>Professionals</a
>
</li>

View File

@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { faUserGear } from '@fortawesome/free-solid-svg-icons';
import { initFlowbite } from 'flowbite';
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
import { KeycloakService } from 'keycloak-angular';
import { Observable } from 'rxjs';
import { User } from '../../../../../bizmatch-server/src/models/db.model';
@ -60,4 +60,26 @@ export class HeaderComponent {
isActive(route: string): boolean {
return this.router.url === route;
}
closeDropdown() {
const dropdownButton = document.getElementById('user-menu-button');
const dropdownMenu = this.user ? document.getElementById('user-login') : document.getElementById('user-unknown');
if (dropdownButton && dropdownMenu) {
const dropdown = new Dropdown(dropdownMenu, dropdownButton);
dropdown.hide();
}
}
closeMobileMenu() {
const targetElement = document.getElementById('navbar-user');
const triggerElement = document.querySelector('[data-collapse-toggle="navbar-user"]');
if (targetElement instanceof HTMLElement && triggerElement instanceof HTMLElement) {
const collapse = new Collapse(targetElement, triggerElement);
collapse.collapse();
}
}
closeMenus() {
this.closeDropdown();
this.closeMobileMenu();
}
}

View File

@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MessageComponent } from './message.component';
import { Message, MessageService } from './message.service';
@Component({
selector: 'app-message-container',
standalone: true,
imports: [CommonModule, MessageComponent],
template: `
<div class="fixed top-5 right-5 z-50 flex flex-col items-end">
<app-message *ngFor="let message of messages" [message]="message" (close)="removeMessage(message)"> </app-message>
</div>
`,
})
export class MessageContainerComponent implements OnInit {
messages: Message[] = [];
constructor(private messageService: MessageService) {}
ngOnInit(): void {
this.messageService.messages$.subscribe(messages => {
this.messages = messages;
});
}
removeMessage(message: Message): void {
this.messageService.removeMessage(message);
}
}

View File

@ -1,32 +1,25 @@
import { Component } from '@angular/core';
import { AsyncPipe, NgIf } from '@angular/common';
import { MessageService } from './message.service';
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Message } from './message.service';
@Component({
selector: 'app-message',
standalone: true,
imports: [AsyncPipe, NgIf],
imports: [CommonModule],
template: `
<div
*ngIf="messageService.modalVisible$ | async"
id="toast-success"
class="fixed top-[0.5rem] right-[1rem] flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-slate-200 rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
role="alert"
>
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
<div [ngClass]="getClasses()" role="alert">
<div [ngClass]="getIconClasses()">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z" />
<path [attr.d]="getIconPath()" />
</svg>
<span class="sr-only">Check icon</span>
<span class="sr-only">{{ getSrText() }}</span>
</div>
<div class="ms-3 text-sm font-normal">{{ messageService.message$ | async }}</div>
<div class="ms-3 text-sm font-normal">{{ message.text }}</div>
<button
type="button"
(click)="onClose()"
class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700"
data-dismiss-target="#toast-success"
aria-label="Close"
(click)="messageService.reject()"
>
<span class="sr-only">Close</span>
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
@ -37,5 +30,59 @@ import { MessageService } from './message.service';
`,
})
export class MessageComponent {
constructor(public messageService: MessageService) {}
@Input() message!: Message;
@Output() close = new EventEmitter<void>();
onClose(): void {
this.close.emit();
}
getClasses(): string {
return `flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800 ${this.getSeverityClasses()}`;
}
getIconClasses(): string {
const baseClasses = 'inline-flex items-center justify-center flex-shrink-0 w-8 h-8 rounded-lg';
switch (this.message.severity) {
case 'success':
return `${baseClasses} text-green-500 bg-green-100 dark:bg-green-800 dark:text-green-200`;
case 'danger':
return `${baseClasses} text-red-500 bg-red-100 dark:bg-red-800 dark:text-red-200`;
case 'warning':
return `${baseClasses} text-orange-500 bg-orange-100 dark:bg-orange-700 dark:text-orange-200`;
}
}
getIconPath(): string {
switch (this.message.severity) {
case 'success':
return 'M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z';
case 'danger':
return 'M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z';
case 'warning':
return 'M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z';
}
}
getSrText(): string {
switch (this.message.severity) {
case 'success':
return 'Check icon';
case 'danger':
return 'Error icon';
case 'warning':
return 'Warning icon';
}
}
private getSeverityClasses(): string {
switch (this.message.severity) {
case 'success':
return 'border-green-500';
case 'danger':
return 'border-red-500';
case 'warning':
return 'border-orange-500';
}
}
}

View File

@ -1,32 +1,30 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
export interface Message {
severity: 'success' | 'danger' | 'warning';
text: string;
duration: number;
}
@Injectable({
providedIn: 'root',
})
export class MessageService {
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
private messageSubject = new BehaviorSubject<string>('');
private resolvePromise!: (value: boolean) => void;
private messagesSubject = new BehaviorSubject<Message[]>([]);
messages$: Observable<Message[]> = this.messagesSubject.asObservable();
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
message$: Observable<string> = this.messageSubject.asObservable();
addMessage(message: Message): void {
const currentMessages = this.messagesSubject.value;
this.messagesSubject.next([...currentMessages, message]);
showMessage(message: string): Promise<boolean> {
this.messageSubject.next(message);
this.modalVisibleSubject.next(true);
return new Promise<boolean>(resolve => {
this.resolvePromise = resolve;
});
}
accept(): void {
this.modalVisibleSubject.next(false);
this.resolvePromise(true);
}
reject(): void {
this.modalVisibleSubject.next(false);
this.resolvePromise(false);
if (message.duration > 0) {
setTimeout(() => this.removeMessage(message), message.duration);
}
}
removeMessage(messageToRemove: Message): void {
const currentMessages = this.messagesSubject.value;
this.messagesSubject.next(currentMessages.filter(msg => msg !== messageToRemove));
}
}

View File

@ -200,10 +200,15 @@
<textarea id="message" name="message" [(ngModel)]="mailinfo.sender.comments" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div>
<div class="flex items-center justify-between">
<div class="text-sm">
Listing by <span class="font-semibold">Mia Hernandez</span>
<img src="https://placehold.co/30x30" alt="Realtor logo" class="inline-block ml-1 w-6 h-6" />
@if(listingUser){
<div class="flex items-center space-x-2">
<p>Listing by</p>
<a routerLink="/details-user/{{ listingUser.id }}" class="text-blue-600 dark:text-blue-500 hover:underline">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
@if(listingUser.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imagePath }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
}
</div>
}
<button (click)="mail()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Submit</button>
</div>
</form>

View File

@ -9,6 +9,7 @@
<input type="email" id="email" name="email" [(ngModel)]="user.email" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500" />
<p class="text-xs text-gray-500 mt-1">You can only modify your email by contacting us at support&#64;bizwatch.net</p>
</div>
@if (isProfessional){
<div class="flex flex-row items-center justify-around md:space-x-4">
<div class="flex h-full justify-between flex-col">
<p class="text-sm font-medium text-gray-700 mb-1">Company Logo</p>
@ -55,6 +56,7 @@
</button>
</div>
</div>
}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@ -75,14 +77,16 @@
<option *ngFor="let type of customerTypes" [value]="type">{{ type | titlecase }}</option>
</select>
</div>
@if (isProfessional){
<div>
<label for="customerSubType" class="block text-sm font-medium text-gray-700">Professional Type</label>
<select id="customerSubType" name="customerSubType" [(ngModel)]="user.customerSubType" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
<option *ngFor="let subType of customerSubTypes" [value]="subType">{{ subType | titlecase }}</option>
</select>
</div>
}
</div>
@if (isProfessional){
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="companyName" class="block text-sm font-medium text-gray-700">Company Name</label>
@ -178,7 +182,7 @@
<span class="text-sm text-gray-500 ml-2">[Add more licenses or remove existing ones.]</span>
</div>
</div>
}
<div class="flex justify-start">
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" (click)="updateProfile(user)">
Update Profile
@ -263,7 +267,6 @@
<!-- @if(showModal){ -->
<app-image-crop-and-upload [uploadParams]="uploadParams" (uploadFinished)="uploadFinished($event)"></app-image-crop-and-upload>
<app-confirmation></app-confirmation>
<app-message></app-message>
<!-- } -->
<!-- <div class="surface-ground px-4 py-8 md:px-6 lg:px-8">
<div class="p-fluid flex flex-column lg:flex-row">

View File

@ -23,7 +23,7 @@ import { SharedService } from '../../../services/shared.service';
import { SubscriptionsService } from '../../../services/subscriptions.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { map2User } from '../../../utils/utils';
import { createDefaultUser, map2User } from '../../../utils/utils';
import { TOOLBAR_OPTIONS } from '../../utils/defaults';
@Component({
selector: 'app-account',
@ -34,13 +34,10 @@ import { TOOLBAR_OPTIONS } from '../../utils/defaults';
styleUrl: './account.component.scss',
})
export class AccountComponent {
// @ViewChild('companyUpload') public companyUpload: FileUpload;
// @ViewChild('profileUpload') public profileUpload: FileUpload;
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User;
subscriptions: Array<Subscription>;
userSubscriptions: Array<Subscription> = [];
maxFileSize = 15000000;
companyLogoUrl: string;
profileUrl: string;
type: 'company' | 'profile';
@ -101,8 +98,16 @@ export class AccountComponent {
printInvoice(invoice: Invoice) {}
async updateProfile(user: User) {
if (this.user.customerType === 'buyer') {
const id = this.user.id;
this.user = createDefaultUser(this.user.email, this.user.firstname, this.user.lastname);
this.user.customerType = 'buyer';
this.user.id = id;
this.imageService.deleteLogoImagesByMail(this.user.email);
this.imageService.deleteProfileImagesByMail(this.user.email);
}
await this.userService.save(this.user);
// this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Account changes have been persisted', life: 3000 });
this.messageService.addMessage({ severity: 'success', text: 'Account changes have been persisted', duration: 3000 });
}
onUploadCompanyLogo(event: any) {
@ -162,13 +167,18 @@ export class AccountComponent {
if (confirmed) {
if (type === 'profile') {
this.user.hasProfile = false;
await Promise.all([this.imageService.deleteProfileImagesById(this.user.email), this.userService.save(this.user)]);
await Promise.all([this.imageService.deleteProfileImagesByMail(this.user.email), this.userService.save(this.user)]);
} else {
this.user.hasCompanyLogo = false;
await Promise.all([this.imageService.deleteLogoImagesById(this.user.email), this.userService.save(this.user)]);
await Promise.all([this.imageService.deleteLogoImagesByMail(this.user.email), this.userService.save(this.user)]);
}
this.user = await this.userService.getById(this.user.id);
this.messageService.showMessage('Image deleted');
// this.messageService.showMessage('Image deleted');
this.messageService.addMessage({
severity: 'success',
text: 'Image deleted.',
duration: 3000, // 3 seconds
});
}
}
// select(event: any, type: 'company' | 'profile') {

View File

@ -1,3 +1,150 @@
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg shadow-md p-6">
<h1 class="text-2xl font-semibold mb-6">Edit Listing</h1>
@if(listing){
<form #listingForm="ngForm">
<div class="mb-4">
<label for="listingsCategory" class="block text-sm font-bold text-gray-700 mb-1">Listing category</label>
<ng-select [readonly]="mode === 'edit'" [items]="selectOptions?.listingCategories" bindLabel="name" bindValue="value" [(ngModel)]="listing.listingsCategory" name="listingsCategory"> </ng-select>
</div>
<div class="mb-4">
<label for="title" class="block text-sm font-bold text-gray-700 mb-1">Title of Listing</label>
<input type="text" id="title" [(ngModel)]="listing.title" name="title" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="mb-4">
<label for="description" class="block text-sm font-bold text-gray-700 mb-1">Description</label>
<quill-editor [(ngModel)]="listing.description" name="description" [modules]="quillModules"></quill-editor>
</div>
<div class="mb-4">
<label for="type" class="block text-sm font-bold text-gray-700 mb-1">Type of business</label>
<ng-select [items]="typesOfBusiness" bindLabel="name" bindValue="value" [(ngModel)]="listing.type" name="type"> </ng-select>
</div>
<div class="flex mb-4 space-x-4">
<div class="w-1/2">
<label for="state" class="block text-sm font-bold text-gray-700 mb-1">State</label>
<ng-select [items]="selectOptions?.states" bindLabel="name" bindValue="value" [(ngModel)]="listing.state" name="state"> </ng-select>
</div>
<div class="w-1/2">
<label for="city" class="block text-sm font-bold text-gray-700 mb-1">City</label>
<input type="text" id="city" [(ngModel)]="listing.city" name="city" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
</div>
<div class="flex mb-4 space-x-4">
<div class="w-1/2">
<label for="price" class="block text-sm font-bold text-gray-700 mb-1">Price</label>
<input
type="text"
id="price"
[(ngModel)]="listing.price"
name="price"
class="w-full p-2 border border-gray-300 rounded-md"
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
currencyMask
/>
</div>
<div class="w-1/2">
<label for="salesRevenue" class="block text-sm font-bold text-gray-700 mb-1">Sales Revenue</label>
<input
type="text"
id="salesRevenue"
[(ngModel)]="listing.salesRevenue"
name="salesRevenue"
class="w-full p-2 border border-gray-300 rounded-md"
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
currencyMask
/>
</div>
</div>
<div class="mb-4">
<label for="cashFlow" class="block text-sm font-bold text-gray-700 mb-1">Cash Flow</label>
<input
type="text"
id="cashFlow"
[(ngModel)]="listing.cashFlow"
name="cashFlow"
class="w-full p-2 border border-gray-300 rounded-md"
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
currencyMask
/>
</div>
<div class="flex mb-4 space-x-4">
<div class="w-1/2">
<label for="established" class="block text-sm font-bold text-gray-700 mb-1">Years Established Since</label>
<input type="number" id="established" [(ngModel)]="listing.established" name="established" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="w-1/2">
<label for="employees" class="block text-sm font-bold text-gray-700 mb-1">Employees</label>
<input type="number" id="employees" [(ngModel)]="listing.employees" name="employees" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
</div>
<div class="flex mb-4 space-x-4">
<div class="flex items-center">
<input type="checkbox" id="realEstateIncluded" [(ngModel)]="listing.realEstateIncluded" name="realEstateIncluded" class="mr-2" />
<label for="realEstateIncluded" class="text-sm font-bold text-gray-700">Real Estate Included</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="leasedLocation" [(ngModel)]="listing.leasedLocation" name="leasedLocation" class="mr-2" />
<label for="leasedLocation" class="text-sm font-bold text-gray-700">Leased Location</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="franchiseResale" [(ngModel)]="listing.franchiseResale" name="franchiseResale" class="mr-2" />
<label for="franchiseResale" class="text-sm font-bold text-gray-700">Franchise Re-Sale</label>
</div>
</div>
<div class="mb-4">
<label for="supportAndTraining" class="block text-sm font-bold text-gray-700 mb-1">Support & Training</label>
<input type="text" id="supportAndTraining" [(ngModel)]="listing.supportAndTraining" name="supportAndTraining" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="mb-4">
<label for="reasonForSale" class="block text-sm font-bold text-gray-700 mb-1">Reason for Sale</label>
<input type="text" id="reasonForSale" [(ngModel)]="listing.reasonForSale" name="reasonForSale" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="flex mb-4 space-x-4">
<div class="w-1/2">
<label for="brokerLicencing" class="block text-sm font-bold text-gray-700 mb-1">Broker Licensing</label>
<input type="text" id="brokerLicencing" [(ngModel)]="listing.brokerLicencing" name="brokerLicencing" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="w-1/2">
<label for="internalListingNumber" class="block text-sm font-bold text-gray-700 mb-1">Internal Listing Number</label>
<input type="number" id="internalListingNumber" [(ngModel)]="listing.internalListingNumber" name="internalListingNumber" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
</div>
<div class="mb-4">
<label for="internals" class="block text-sm font-bold text-gray-700 mb-1">Internal Notes (Will not be shown on the listing, for your records only.)</label>
<textarea id="internals" [(ngModel)]="listing.internals" name="internals" class="w-full p-2 border border-gray-300 rounded-md" rows="3"></textarea>
</div>
<div class="flex items-center mb-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" [(ngModel)]="listing.draft" name="draft" class="hidden" />
<div class="toggle-bg block w-12 h-6 rounded-full bg-gray-600 transition"></div>
</div>
<div class="ml-3 text-gray-700 font-medium">Draft Mode (Will not be shown as public listing)</div>
</label>
</div>
@if (mode==='create'){
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">Post Listing</button>
} @else {
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">Update Listing</button>
}
</form>
}
</div>
</div>
<!-- <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>
@ -156,153 +303,3 @@
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog> -->
<div class="container mx-auto p-4">
<div class="bg-white rounded-lg shadow-md p-6">
<h1 class="text-2xl font-semibold mb-6">Edit Listing</h1>
@if(listing){
<form #listingForm="ngForm">
<div class="mb-4">
<label for="listingsCategory" class="block text-sm font-bold text-gray-700 mb-1">Listing category</label>
<select id="listingsCategory" [(ngModel)]="listing.listingsCategory" name="listingsCategory" class="w-full p-2 border border-gray-300 rounded-md">
<option value="business">Business</option>
<option value="commercialProperty">Commercial Property</option>
</select>
</div>
<div class="mb-4">
<label for="title" class="block text-sm font-bold text-gray-700 mb-1">Title of Listing</label>
<input type="text" id="title" [(ngModel)]="listing.title" name="title" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="mb-4">
<label for="description" class="block text-sm font-bold text-gray-700 mb-1">Description</label>
<quill-editor [(ngModel)]="listing.description" name="description" [modules]="quillModules"></quill-editor>
</div>
<div class="mb-4">
<label for="type" class="block text-sm font-bold text-gray-700 mb-1">Type of business</label>
<ng-select [items]="typesOfBusiness" bindLabel="name" bindValue="value" [(ngModel)]="listing.type" name="type"> </ng-select>
</div>
<div class="flex mb-4 space-x-4">
<div class="w-1/2">
<label for="state" class="block text-sm font-bold text-gray-700 mb-1">State</label>
<ng-select [items]="selectOptions?.states" bindLabel="name" bindValue="value" [(ngModel)]="listing.state" name="state"> </ng-select>
</div>
<div class="w-1/2">
<label for="city" class="block text-sm font-bold text-gray-700 mb-1">City</label>
<input type="text" id="city" [(ngModel)]="listing.city" name="city" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
</div>
<div class="flex mb-4 space-x-4">
<div class="w-1/2">
<label for="price" class="block text-sm font-bold text-gray-700 mb-1">Price</label>
<input
type="text"
id="price"
[(ngModel)]="listing.price"
name="price"
class="w-full p-2 border border-gray-300 rounded-md"
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
currencyMask
/>
</div>
<div class="w-1/2">
<label for="salesRevenue" class="block text-sm font-bold text-gray-700 mb-1">Sales Revenue</label>
<input
type="text"
id="salesRevenue"
[(ngModel)]="listing.salesRevenue"
name="salesRevenue"
class="w-full p-2 border border-gray-300 rounded-md"
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
currencyMask
/>
</div>
</div>
<div class="mb-4">
<label for="cashFlow" class="block text-sm font-bold text-gray-700 mb-1">Cash Flow</label>
<input
type="text"
id="cashFlow"
[(ngModel)]="listing.cashFlow"
name="cashFlow"
class="w-full p-2 border border-gray-300 rounded-md"
[options]="{ prefix: '$', thousands: ',', decimal: '.', precision: 0, align: 'left' }"
currencyMask
/>
</div>
<div class="flex mb-4 space-x-4">
<div class="w-1/2">
<label for="established" class="block text-sm font-bold text-gray-700 mb-1">Years Established Since</label>
<input type="number" id="established" [(ngModel)]="listing.established" name="established" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="w-1/2">
<label for="employees" class="block text-sm font-bold text-gray-700 mb-1">Employees</label>
<input type="number" id="employees" [(ngModel)]="listing.employees" name="employees" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
</div>
<div class="flex mb-4 space-x-4">
<div class="flex items-center">
<input type="checkbox" id="realEstateIncluded" [(ngModel)]="listing.realEstateIncluded" name="realEstateIncluded" class="mr-2" />
<label for="realEstateIncluded" class="text-sm font-bold text-gray-700">Real Estate Included</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="leasedLocation" [(ngModel)]="listing.leasedLocation" name="leasedLocation" class="mr-2" />
<label for="leasedLocation" class="text-sm font-bold text-gray-700">Leased Location</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="franchiseResale" [(ngModel)]="listing.franchiseResale" name="franchiseResale" class="mr-2" />
<label for="franchiseResale" class="text-sm font-bold text-gray-700">Franchise Re-Sale</label>
</div>
</div>
<div class="mb-4">
<label for="supportAndTraining" class="block text-sm font-bold text-gray-700 mb-1">Support & Training</label>
<input type="text" id="supportAndTraining" [(ngModel)]="listing.supportAndTraining" name="supportAndTraining" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="mb-4">
<label for="reasonForSale" class="block text-sm font-bold text-gray-700 mb-1">Reason for Sale</label>
<input type="text" id="reasonForSale" [(ngModel)]="listing.reasonForSale" name="reasonForSale" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="flex mb-4 space-x-4">
<div class="w-1/2">
<label for="brokerLicencing" class="block text-sm font-bold text-gray-700 mb-1">Broker Licensing</label>
<input type="text" id="brokerLicencing" [(ngModel)]="listing.brokerLicencing" name="brokerLicencing" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
<div class="w-1/2">
<label for="internalListingNumber" class="block text-sm font-bold text-gray-700 mb-1">Internal Listing Number</label>
<input type="number" id="internalListingNumber" [(ngModel)]="listing.internalListingNumber" name="internalListingNumber" class="w-full p-2 border border-gray-300 rounded-md" />
</div>
</div>
<div class="mb-4">
<label for="internals" class="block text-sm font-bold text-gray-700 mb-1">Internal Notes (Will not be shown on the listing, for your records only.)</label>
<textarea id="internals" [(ngModel)]="listing.internals" name="internals" class="w-full p-2 border border-gray-300 rounded-md" rows="3"></textarea>
</div>
<div class="flex items-center mb-4">
<label class="flex items-center cursor-pointer">
<div class="relative">
<input type="checkbox" [(ngModel)]="listing.draft" name="draft" class="hidden" />
<div class="toggle-bg block w-12 h-6 rounded-full bg-gray-600 transition"></div>
</div>
<div class="ml-3 text-gray-700 font-medium">Draft Mode (Will not be shown as public listing)</div>
</label>
</div>
@if (mode==='create'){
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">Post Listing</button>
} @else {
<button (click)="save()" class="bg-blue-500 text-white px-4 py-2 rounded-md hover:bg-blue-600">Update Listing</button>
}
</form>
}
</div>
</div>

View File

@ -15,6 +15,7 @@ import { NgxCurrencyDirective } from 'ngx-currency';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { MessageService } from '../../../components/message/message.service';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
@ -39,25 +40,7 @@ export class EditBusinessListingComponent {
listing: BusinessListing;
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User;
maxFileSize = 3000000;
environment = environment;
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;
draggedImage: ImageProperty;
@ -76,7 +59,7 @@ export class EditBusinessListingComponent {
private geoService: GeoService,
private imageService: ImageService,
private loadingService: LoadingService,
private messageService: MessageService,
private route: ActivatedRoute,
private keycloakService: KeycloakService,
) {
@ -114,7 +97,7 @@ export class EditBusinessListingComponent {
async save() {
this.listing = await this.listingsService.save(this.listing, this.listing.listingsCategory);
this.router.navigate(['editBusinessListing', this.listing.id]);
// this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 });
this.messageService.addMessage({ severity: 'success', text: 'Listing changes have been persisted', duration: 3000 });
}
suggestions: string[] | undefined;

View File

@ -137,7 +137,7 @@ export class EditCommercialPropertyListingComponent {
async save() {
this.listing = await this.listingsService.save(this.listing, this.listing.listingsCategory);
this.router.navigate(['editCommercialPropertyListing', this.listing.id]);
// this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 });
this.messageService.addMessage({ severity: 'success', text: 'Listing changes have been persisted', duration: 3000 });
}
async search(event: AutoCompleteCompleteEvent) {
@ -167,10 +167,7 @@ export class EditCommercialPropertyListingComponent {
if (this.croppedImage) {
this.imageService.uploadImage(this.croppedImage, 'uploadPropertyPicture', this.listing.imagePath, this.listing.serialId).subscribe(
async () => {
//console.log('Upload successful', response);
//setTimeout(async () => {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
//}, 10);
this.closeModal();
},
error => {
@ -184,11 +181,10 @@ export class EditCommercialPropertyListingComponent {
const confirmed = await this.confirmationService.showConfirmation('Are you sure you want to delete this image?');
if (confirmed) {
this.listing.imageOrder = this.listing.imageOrder.filter(item => item !== imageName);
// await Promise.all([, ]);
await this.imageService.deleteListingImage(this.listing.imagePath, this.listing.serialId, imageName);
await this.listingsService.save(this.listing, 'commercialProperty');
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
this.messageService.showMessage('Image deleted');
this.messageService.addMessage({ severity: 'success', text: 'Image has been deleted', duration: 3000 });
} else {
console.log('deny');
}

View File

@ -88,7 +88,6 @@
</div>
</div>
<app-confirmation></app-confirmation>
<app-message></app-message>
<!-- <div class="surface-ground px-4 py-8 md:px-6 lg:px-8 h-full">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>

View File

@ -56,7 +56,7 @@ export class MyListingComponent {
async confirm(listing: ListingType) {
const confirmed = await this.confirmationService.showConfirmation(`Are you sure you want to delete this listing?`);
if (confirmed) {
this.messageService.showMessage('Listing has been deleted');
// this.messageService.showMessage('Listing has been deleted');
this.deleteListing(listing);
}
// this.confirmationService.confirm({

View File

@ -29,11 +29,11 @@ export class ImageService {
return await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/propertyPicture/${imagePath}/${serial}/${name}`));
}
async deleteLogoImagesById(email: string) {
async deleteLogoImagesByMail(email: string) {
const adjustedEmail = emailToDirName(email);
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/logo/${adjustedEmail}`));
}
async deleteProfileImagesById(email: string) {
async deleteProfileImagesByMail(email: string) {
const adjustedEmail = emailToDirName(email);
await lastValueFrom(this.http.delete<[]>(`${this.apiBaseUrl}/bizmatch/image/profile/${adjustedEmail}`));
}

View File

@ -1,9 +1,33 @@
import { Router } from '@angular/router';
import { ConsoleFormattedStream, INFO, createLogger as _createLogger, stdSerializers } from 'browser-bunyan';
import { jwtDecode } from 'jwt-decode';
import { BusinessListing, CommercialPropertyListing } from '../../../../bizmatch-server/src/models/db.model';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../bizmatch-server/src/models/db.model';
import { JwtToken, KeycloakUser, ListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
export function createDefaultUser(email: string, firstname: string, lastname: string): User {
return {
email,
firstname,
lastname,
phoneNumber: '',
description: '',
companyName: '',
companyOverview: '',
companyWebsite: '',
companyLocation: '',
offeredServices: '',
areasServed: [],
hasProfile: false,
hasCompanyLogo: false,
licensedIn: [],
gender: undefined,
customerType: undefined,
customerSubType: undefined,
created: new Date(),
updated: new Date(),
};
}
export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
return {
id: undefined,