history service, mail template improved, general listing entry

This commit is contained in:
Andreas Knuth 2024-05-14 11:53:20 -05:00
parent aff55c5433
commit d2e5562602
17 changed files with 261 additions and 49 deletions

View File

@ -34,6 +34,8 @@ export class MailService {
iname: mailInfo.sender.name,
phone: mailInfo.sender.phoneNumber,
email: mailInfo.sender.email,
id: mailInfo.listing.id,
url: 'http://localhost:4200',
},
});
return user;

View File

@ -1,12 +1,104 @@
<p>Dear {{ name }},</p>
<p>You got an inquiry regarding your '{{title}}'' listing</p>
<p>
Buyers Information
</p>
<p> Contact Name: {{iname}}</p>
<p> Contact Phone: {{phone}}</p>
<p> Contact Mail: {{email}}</p>
<p> Contact Name: {{iname}}</p>
<p> Comments: {{inquiry}}</p>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notification: New Buyer Lead</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f8f8f8;
}
.container {
width: 80%;
margin: auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
font-size: 24px;
font-weight: bold;
text-align: center;
margin-top: 20px;
color: #333333;
}
.subheader {
text-align: center;
margin-bottom: 20px;
color: #555555;
}
.section {
margin-bottom: 20px;
}
.section-title {
color: #1E90FF;
font-weight: bold;
border-bottom: 2px solid #1E90FF;
padding-bottom: 5px;
margin-bottom: 10px;
}
.info {
margin-bottom: 10px;
padding: 10px;
}
.info:nth-child(even) {
background-color: #f0f0f0;
}
.info-label {
font-weight: bold;
color: #333333;
}
.info-value {
margin-left: 10px;
color: #555555;
}
</style>
</head>
<body>
<div class="container">
<div class="header">Notification: New buyer lead from the Bizmatch Network</div>
<div class="subheader">Dear {{name}},</div>
<p>You've received a message regarding your "{{title}}" listing.</p>
<p>Internal Listing Number: {{internalListingNumber}}</p>
<div class="section">
<div class="section-title">Buyer Information</div>
<div class="info">
<span class="info-label">Contact Name:</span>
<span class="info-value">{{iname}}</span>
</div>
<div class="info">
<span class="info-label">Contact Email:</span>
<span class="info-value">{{email}}</span>
</div>
<div class="info">
<span class="info-label">Contact Phone:</span>
<span class="info-value">{{phone}}</span>
</div>
<div class="info">
<span class="info-label">Comments:</span>
<span class="info-value">{{inquiry}}</span>
</div>
</div>
<div class="section">
<div class="section-title">Listing Information</div>
<div class="info">
<span class="info-label">Headline:</span>
<span class="info-value">{{title}}</span>
</div>
<div class="info">
<span class="info-label">Listing ID:</span>
<span class="info-value"><a href="{{url}}/listing/{{id}}">{{id}}</a></span>
</div>
{{#if internalListingNumber}}
<div class="info">
<span class="info-label">Ref ID:</span>
<span class="info-value">{{internalListingNumber}}</span>
</div>
{{/if}}
</div>
</div>
</body>
</html>

View File

@ -1,6 +1,8 @@
import { Routes } from '@angular/router';
import { LogoutComponent } from './components/logout/logout.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { authGuard } from './guards/auth.guard';
import { ListingCategoryGuard } from './guards/listing-category.guard';
import { DetailsBusinessListingComponent } from './pages/details/details-business-listing/details-business-listing.component';
import { DetailsCommercialPropertyListingComponent } from './pages/details/details-commercial-property-listing/details-commercial-property-listing.component';
import { DetailsUserComponent } from './pages/details/details-user/details-user.component';
@ -46,6 +48,11 @@ export const routes: Routes = [
path: 'details-commercial-property-listing/:id',
component: DetailsCommercialPropertyListingComponent,
},
{
path: 'listing/:id',
canActivate: [ListingCategoryGuard],
component: NotFoundComponent, // Dummy-Komponente, wird nie angezeigt, da der Guard weiterleitet
},
// #########
// User Details
{

View File

@ -0,0 +1 @@
<p>not-found works!</p>

View File

@ -0,0 +1,8 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-not-found',
standalone: true,
template: '<h2>Page not found</h2>',
})
export class NotFoundComponent {}

View File

@ -0,0 +1,35 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class ListingCategoryGuard implements CanActivate {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> {
const id = route.paramMap.get('id');
const url = `${this.apiBaseUrl}/bizmatch/listings/undefined/${id}`;
return this.http.get<any>(url).pipe(
map(response => {
const category = response.listingsCategory;
if (category === 'business') {
return this.router.createUrlTree([`/details-business-listing/${id}`]);
} else if (category === 'commercialProperty') {
return this.router.createUrlTree([`/details-commercial-property-listing/${id}`]);
} else {
return this.router.createUrlTree(['/not-found']);
}
}),
catchError(() => {
return of(this.router.createUrlTree(['/not-found']));
}),
);
}
}

View File

@ -4,7 +4,9 @@
<div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{ listing?.title }}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
@if(historyService.canGoBack()){
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="historyService.goBack()"></p-button>
}
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
@if(listing){
@ -12,8 +14,8 @@
<div class="col-12 md:col-6">
<ul class="list-none p-0 m-0 border-top-1 border-300">
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium flex align-self-start">Description</div>
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div>
<div class="text-500 w-full md:w-2 font-medium flex">Description</div>
<div class="text-900 w-full md:w-10 line-height-3 flex flex-column" [innerHTML]="description"></div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Category</div>
@ -45,6 +47,14 @@
<div class="text-500 w-full md:w-2 font-medium">Employees</div>
<div class="text-900 w-full md:w-10">{{ listing.employees }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Support & Training</div>
<div class="text-900 w-full md:w-10">{{ listing.supportAndTraining }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-2 font-medium">Reason for Sale</div>
<div class="text-900 w-full md:w-10">{{ listing.reasonForSale }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium">Broker licensing</div>
<div class="text-900 w-full md:w-10">{{ listing.brokerLicencing }}</div>

View File

@ -1,7 +1,6 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import onChange from 'on-change';
import { MessageService } from 'primeng/api';
import { GalleriaModule } from 'primeng/galleria';
@ -9,6 +8,7 @@ import { lastValueFrom } from 'rxjs';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ListingsService } from '../../../services/listings.service';
import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';
@ -49,7 +49,9 @@ export class DetailsBusinessListingComponent {
mailinfo: MailInfo;
environment = environment;
user: User;
listingUser: User;
description: SafeHtml;
private history: string[] = [];
constructor(
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
@ -59,8 +61,13 @@ export class DetailsBusinessListingComponent {
private mailService: MailService,
private messageService: MessageService,
private sanitizer: DomSanitizer,
private location: Location,
public historyService: HistoryService,
) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.history.push(event.urlAfterRedirects);
}
});
this.mailinfo = { sender: {}, userId: '', email: '' };
this.userService.getUserObservable().subscribe(user => {
this.user = user;
@ -70,16 +77,15 @@ export class DetailsBusinessListingComponent {
async ngOnInit() {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'business'));
this.listingUser = await this.userService.getById(this.listing.userId);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
}
back() {
this.location.back();
}
isAdmin() {
return this.userService.hasAdminRole();
}
async mail() {
this.mailinfo.email = this.user.email;
this.mailinfo.email = this.listingUser.email;
this.mailinfo.userId = this.listing.userId;
this.mailinfo.listing = this.listing;
await this.mailService.mail(this.mailinfo);

View File

@ -4,7 +4,9 @@
<div class="flex justify-content-between align-items-center align-content-center mb-2">
<div class="font-medium text-3xl text-900 mb-3">{{ listing?.title }}</div>
<!-- <button pButton pRipple type="button" label="Go back to listings" icon="pi pi-user-plus" class="mr-3 p-button-rounded"></button> -->
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
@if(historyService.canGoBack()){
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="historyService.goBack()"></p-button>
}
</div>
<!-- <div class="text-500 mb-5">Egestas sed tempus urna et pharetra pharetra massa massa ultricies.</div> -->
@if(listing){
@ -12,7 +14,7 @@
<div class="col-12 md:col-6">
<ul class="list-none p-0 m-0 border-top-1 border-300">
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-2 font-medium flex align-self-start">Description</div>
<div class="text-500 w-full md:w-2 font-medium flex">Description</div>
<div class="text-900 w-full md:w-10 line-height-3" [innerHTML]="description"></div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">

View File

@ -1,4 +1,3 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
@ -9,6 +8,7 @@ import { lastValueFrom } from 'rxjs';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ListingsService } from '../../../services/listings.service';
import { MailService } from '../../../services/mail.service';
import { SelectOptionsService } from '../../../services/select-options.service';
@ -50,6 +50,7 @@ export class DetailsCommercialPropertyListingComponent {
propertyImages: string[] = [];
environment = environment;
user: User;
listingUser: User;
description: SafeHtml;
constructor(
private activatedRoute: ActivatedRoute,
@ -60,7 +61,7 @@ export class DetailsCommercialPropertyListingComponent {
private mailService: MailService,
private messageService: MessageService,
private sanitizer: DomSanitizer,
private location: Location,
public historyService: HistoryService,
) {
this.mailinfo = { sender: {}, userId: '', email: '' };
this.userService.getUserObservable().subscribe(user => {
@ -72,16 +73,14 @@ export class DetailsCommercialPropertyListingComponent {
async ngOnInit() {
this.listing = await lastValueFrom(this.listingsService.getListingById(this.id, 'commercialProperty'));
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
this.listingUser = await this.userService.getById(this.listing.userId);
this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description);
}
back() {
this.location.back();
}
isAdmin() {
return this.userService.hasAdminRole();
}
async mail() {
this.mailinfo.email = this.user.email;
this.mailinfo.email = this.listingUser.email;
this.mailinfo.userId = this.listing.userId;
this.mailinfo.listing = this.listing;
await this.mailService.mail(this.mailinfo);

View File

@ -2,10 +2,6 @@
<div class="px-6 py-5">
@if (user){
<div class="surface-card p-4 shadow-2 border-round">
<!-- <div class="flex justify-content-between align-items-center align-content-center">
<div class="font-medium text-3xl text-900 mb-3">{{listing?.title}}</div>
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
</div> -->
<div class="surface-section px-6 pt-5">
<div class="flex align-items-start flex-column lg:flex-row lg:justify-content-between">
<div class="flex align-items-start flex-column md:flex-row">
@ -44,7 +40,9 @@
</div>
</div>
</div>
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="back()"></p-button>
@if(historyService.canGoBack()){
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="historyService.goBack()"></p-button>
}
</div>
<p class="mt-2 text-700 line-height-3 text-l font-semibold">{{ user.description }}</p>
</div>
@ -123,7 +121,7 @@
<div class="p-3 border-1 surface-border border-round surface-card">
<div class="text-900 mb-2 flex align-items-center">
@if (listing.imageOrder?.length>0){
<img src="pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}&_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" />
<img src="pictures/property/{{ listing.imagePath }}/{{ listing.imageOrder[0] }}?_ts={{ ts }}" class="mr-3" style="width: 45px; height: 45px" />
} @else {
<img src="assets/images/placeholder_properties.jpg" class="mr-3" style="width: 45px; height: 45px" />
}

View File

@ -1,4 +1,3 @@
import { Location } from '@angular/common';
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
@ -8,6 +7,7 @@ import { Observable } from 'rxjs';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { HistoryService } from '../../../services/history.service';
import { ImageService } from '../../../services/image.service';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
@ -42,7 +42,7 @@ export class DetailsUserComponent {
public selectOptions: SelectOptionsService,
private sanitizer: DomSanitizer,
private imageService: ImageService,
private location: Location,
public historyService: HistoryService,
) {}
async ngOnInit() {
@ -56,9 +56,7 @@ export class DetailsUserComponent {
this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview);
this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices);
}
back() {
this.location.back();
}
isAdmin() {
return this.userService.hasAdminRole();
}

View File

@ -28,7 +28,6 @@
<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>
@ -36,13 +35,26 @@
</div>
<div class="mb-4">
<label for="type" class="block font-medium text-900 mb-2">Type of business</label>
<p-dropdown id="type" [options]="typesOfBusiness" [(ngModel)]="listing.type" optionLabel="name" optionValue="value" [showClear]="true" placeholder="Type of business" [style]="{ width: '100%' }"></p-dropdown>
<p-dropdown
id="type"
[filter]="true"
filterBy="name"
[options]="typesOfBusiness"
[(ngModel)]="listing.type"
optionLabel="name"
optionValue="value"
[showClear]="true"
placeholder="Type of business"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="listingCategory" class="block font-medium text-900 mb-2">State</label>
<p-dropdown
id="listingCategory"
[filter]="true"
filterBy="name"
[options]="selectOptions?.states"
[(ngModel)]="listing.state"
optionLabel="name"
@ -82,11 +94,11 @@
<div class="grid">
<div class="mb-4 col-12 md:col-6">
<label for="established" class="block font-medium text-900 mb-2">Years Established Since</label>
<p-inputNumber mode="decimal" inputId="established" [(ngModel)]="listing.established"></p-inputNumber>
<p-inputNumber mode="decimal" inputId="established" [(ngModel)]="listing.established" [useGrouping]="false"></p-inputNumber>
</div>
<div class="mb-4 col-12 md:col-6">
<label for="employees" class="block font-medium text-900 mb-2">Employees</label>
<p-inputNumber mode="decimal" inputId="employees" [(ngModel)]="listing.employees"></p-inputNumber>
<p-inputNumber mode="decimal" inputId="employees" [(ngModel)]="listing.employees" [useGrouping]="false"></p-inputNumber>
</div>
</div>
<div class="grid">
@ -119,7 +131,7 @@
</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>
<p-inputNumber mode="decimal" inputId="internalListingNumber" type="text" [(ngModel)]="listing.internalListingNumber"></p-inputNumber>
<p-inputNumber mode="decimal" inputId="internalListingNumber" type="text" [(ngModel)]="listing.internalListingNumber" [useGrouping]="false"></p-inputNumber>
</div>
</div>
<div class="mb-4">

View File

@ -38,6 +38,8 @@
<label for="type" class="block font-medium text-900 mb-2">Property Category</label>
<p-dropdown
id="type"
[filter]="true"
filterBy="name"
[options]="typesOfCommercialProperty"
[(ngModel)]="listing.type"
optionLabel="name"
@ -50,7 +52,18 @@
<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>
<p-dropdown
[filter]="true"
filterBy="name"
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>

View File

@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
@Injectable({
providedIn: 'root',
})
export class HistoryService {
private history: string[] = [];
constructor(private router: Router) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.history.push(event.urlAfterRedirects);
}
});
}
public getHistory(): string[] {
return this.history;
}
public canGoBack(): boolean {
return this.history.length > 1;
}
public goBack(): void {
if (this.canGoBack()) {
this.history.pop();
this.router.navigateByUrl(this.history[this.history.length - 1]);
}
}
}

View File

@ -12,9 +12,6 @@ export class ListingsService {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {}
// getAllListings():Observable<ListingType[]>{
// return this.http.get<ListingType[]>(`${this.apiBaseUrl}/bizmatch/business-listings`);
// }
async getListings(criteria: ListingCriteria, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty'): Promise<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray> {
const result = await lastValueFrom(this.http.post<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/search`, criteria));
return result;