Rework of major pages

This commit is contained in:
Andreas Knuth 2024-04-24 14:31:32 +02:00
parent 9e03620be7
commit 4230867608
17 changed files with 995 additions and 837 deletions

View File

@ -22,19 +22,19 @@ export class CommercialPropertyListingsController {
// return this.listingsService.findByUserId(userid,commercials);
// }
@Post('search')
find(@Body() criteria: ListingCriteria): any {
return this.listingsService.findListingsByCriteria(criteria,commercials);
async find(@Body() criteria: ListingCriteria): Promise<any> {
return await this.listingsService.findListingsByCriteria(criteria,commercials);
}
@Post()
create(@Body() listing: any){
async create(@Body() listing: any){
this.logger.info(`Save Listing`);
this.listingsService.createListing(listing,commercials)
return await this.listingsService.createListing(listing,commercials)
}
@Put()
update(@Body() listing: any){
async update(@Body() listing: any){
this.logger.info(`Save Listing`);
this.listingsService.updateListing(listing.id,listing,commercials)
return await this.listingsService.updateListing(listing.id,listing,commercials)
}
@Delete(':id')
deleteById(@Param('id') id:string){

View File

@ -71,12 +71,16 @@ export class ListingsService {
}
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();
data.created=new Date()
data.updated=new Date()
data.visits=0;
data.lastVisit=null
const [createdListing] = await this.conn.insert(table).values(data).returning();
return createdListing as BusinessListing | CommercialPropertyListing;
}
async updateListing(id: string, data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
data.updated=new Date();
const [updateListing] = await this.conn.update(table).set(data).where(eq(table.id, id)).returning();
return updateListing as BusinessListing | CommercialPropertyListing;
}
@ -107,95 +111,5 @@ export class ListingsService {
listing.imageOrder.push(imagename);
await this.updateListing(listing.id, listing, commercials)
}
// async getCommercialPropertyListingById(id: string): Promise<CommercialPropertyListing>{
// return await this.commercialPropertyListingRepository.fetch(id) as unknown as CommercialPropertyListing;
// }
// async getBusinessListingById(id: string) {
// return await this.businessListingRepository.fetch(id)
// }
// async getBusinessListingByUserId(userid:string){
// return await this.businessListingRepository.search().where('userId').equals(userid).return.all()
// }
// async deleteBusinessListing(id: string){
// return await this.businessListingRepository.remove(id);
// }
// async deleteCommercialPropertyListing(id: string){
// return await this.commercialPropertyListingRepository.remove(id);
// }
// async getAllBusinessListings(start?: number, end?: number) {
// return await this.businessListingRepository.search().return.all()
// }
// async getAllCommercialListings(start?: number, end?: number) {
// return await this.commercialPropertyListingRepository.search().return.all()
// }
// async findBusinessListings(criteria:ListingCriteria): Promise<any> {
// // let listings = await this.getAllBusinessListings();
// // return this.find(criteria,listings);
// const from=criteria.start?criteria.start:0
// const size=criteria.length?criteria.length:24
// this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`);
// const result = await this.redis.ft.search('business:index','*',{LIMIT:{from,size}});
// this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`);
// return result
// }
// async findCommercialPropertyListings(criteria:ListingCriteria): Promise<any> {
// let listings = await this.getAllCommercialListings();
// return this.find(criteria,listings);
// }
// async deleteAllBusinessListings(){
// const ids = await this.getIdsForRepo(this.schemaNameBusiness.name);
// this.businessListingRepository.remove(ids);
// }
// async deleteAllcommercialListings(){
// const ids = await this.getIdsForRepo(this.schemaNameCommercial.name);
// this.commercialPropertyListingRepository.remove(ids);
// }
// async getIdsForRepo(repoName:string, maxcount=100000){
// let cursor = 0;
// let ids = [];
// do {
// const reply = await this.redis.scan(cursor, {
// MATCH: `${repoName}:*`,
// COUNT: maxcount
// });
// cursor = reply.cursor;
// // Extrahiere die ID aus jedem Schlüssel und füge sie zur Liste hinzu
// ids = ids.concat(reply.keys.map(key => key.split(':')[1]).filter(id=>id!='index'));
// } while (cursor !== 0);
// return ids;
// }
// async find(criteria:ListingCriteria, listings: any[]): Promise<any> {
// listings=listings.filter(l=>l.listingsCategory===criteria.listingsCategory);
// if (convertStringToNullUndefined(criteria.type)){
// console.log(criteria.type);
// listings=listings.filter(l=>l.type===criteria.type);
// }
// if (convertStringToNullUndefined(criteria.state)){
// console.log(criteria.state);
// listings=listings.filter(l=>l.state===criteria.state);
// }
// if (convertStringToNullUndefined(criteria.minPrice)){
// console.log(criteria.minPrice);
// listings=listings.filter(l=>l.price>=Number(criteria.minPrice));
// }
// if (convertStringToNullUndefined(criteria.maxPrice)){
// console.log(criteria.maxPrice);
// listings=listings.filter(l=>l.price<=Number(criteria.maxPrice));
// }
// if (convertStringToNullUndefined(criteria.realEstateChecked)){
// console.log(criteria.realEstateChecked);
// listings=listings.filter(l=>l.realEstateIncluded);
// }
// if (convertStringToNullUndefined(criteria.category)){
// console.log(criteria.category);
// listings=listings.filter(l=>l.category===criteria.category);
// }
// return listings
// }
}

View File

@ -36,6 +36,8 @@ export const routes: Routes = [
path: 'home',
component: HomeComponent,
},
// #########
// Listings Details
{
path: 'details-business-listing/:id',
component: DetailsBusinessListingComponent,
@ -44,15 +46,21 @@ export const routes: Routes = [
path: 'details-commercial-property-listing/:id',
component: DetailsCommercialPropertyListingComponent,
},
// #########
// User Details
{
path: 'details-user/:id',
component: DetailsUserComponent,
},
// #########
// User edit
{
path: 'account',
component: AccountComponent,
canActivate: [authGuard],
},
// #########
// Create, Update Listings
{
path: 'editBusinessListing/:id',
component: EditBusinessListingComponent,
@ -73,26 +81,36 @@ export const routes: Routes = [
component: EditCommercialPropertyListingComponent,
canActivate: [authGuard],
},
// #########
// My Listings
{
path: 'myListings',
component: MyListingComponent,
canActivate: [authGuard],
},
// #########
// My Favorites
{
path: 'myFavorites',
component: FavoritesComponent,
canActivate: [authGuard],
},
// #########
// EMAil Us
{
path: 'emailUs',
component: EmailUsComponent,
canActivate: [authGuard],
},
// #########
// Logout
{
path: 'logout',
component: LogoutComponent,
canActivate: [authGuard],
},
// #########
// Pricing
{
path: 'pricing',
component: PricingComponent,

View File

@ -46,7 +46,7 @@ export class HeaderComponent {
{
label: 'Create Listing',
icon: 'pi pi-plus-circle',
routerLink: '/createListing',
routerLink: '/createBusinessListing',
visible: this.isUserLogedIn(),
},
{
@ -90,13 +90,13 @@ export class HeaderComponent {
fragment: '',
},
{
label: 'Professionals/Brokers Directory',
routerLink: '/brokerListings',
label: 'Commercial Property',
routerLink: '/commercialPropertyListings',
fragment: '',
},
{
label: 'Commercial Property',
routerLink: '/commercialPropertyListings',
label: 'Professionals/Brokers Directory',
routerLink: '/brokerListings',
fragment: '',
},
];

View File

@ -95,7 +95,7 @@
<div class="text-900 w-full md:w-10">
<div class="grid mt-0 mr-0">
@for (listing of userListings; track listing) {
<div class="col-12 md:col-6 cursor-pointer" [routerLink]="['/details-listing/business', listing.id]">
<div class="col-12 md:col-6 cursor-pointer" [routerLink]="['/details-business-listing', listing.id]">
<div class="p-3 border-1 surface-border border-round surface-card">
<div class="text-900 mb-2">
<span [class]="selectOptions.getBgColorType(listing.type)" class="inline-flex border-circle align-items-center justify-content-center mr-3" style="width: 38px; height: 38px">

View File

@ -1,42 +1,62 @@
<div class="surface-ground ">
<div class="p-fluid flex flex-column lg:flex-row">
<ul class="list-none m-0 p-0 flex flex-row lg:flex-column justify-content-evenly md:justify-content-between lg:justify-content-start mb-5 lg:pr-8 lg:mb-0">
<li>
<a routerLink="/account" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline" >
<i class="pi pi-user md:mr-2"></i>
<span class="font-medium hidden md:block">Account</span>
</a>
</li>
<li>
<a routerLink="/createListing" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<i class="pi pi-plus-circle md:mr-2"></i>
<span class="font-medium hidden md:block">Create Listing</span>
</a>
</li>
<li>
<a routerLink="/myListings" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<i class="pi pi-list md:mr-2"></i>
<span class="font-medium hidden md:block">My Listings</span>
</a>
</li>
<li>
<a routerLink="/myFavorites" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<i class="pi pi-star md:mr-2"></i>
<span class="font-medium hidden md:block">My Favorites</span>
</a>
</li>
<li>
<a routerLink="/emailUs" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<fa-icon [icon]="faEnvelope" class="mr-2 flex"></fa-icon>
<span class="font-medium hidden md:block">Email Us</span>
</a>
</li>
<li>
<a (click)="userService.logout()" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<fa-icon [icon]="faRightFromBracket" class="mr-2 flex"></fa-icon>
<span class="font-medium hidden md:block">Logout</span>
</a>
</li>
</ul>
</div>
</div>
<div class="surface-ground">
<div class="p-fluid flex flex-column lg:flex-row">
<ul class="list-none m-0 p-0 flex flex-row lg:flex-column justify-content-evenly md:justify-content-between lg:justify-content-start mb-5 lg:pr-8 lg:mb-0">
<li>
<a routerLink="/account" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<i class="pi pi-user md:mr-2"></i>
<span class="font-medium hidden md:block">Account</span>
</a>
</li>
<li>
<a
routerLink="/createBusinessListing"
routerLinkActive="text-blue-500"
pRipple
class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"
>
<i class="pi pi-plus-circle md:mr-2"></i>
<span class="font-medium hidden md:block">Create Listing</span>
</a>
</li>
<li>
<a
routerLink="/myListings"
routerLinkActive="text-blue-500"
pRipple
class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"
>
<i class="pi pi-list md:mr-2"></i>
<span class="font-medium hidden md:block">My Listings</span>
</a>
</li>
<li>
<a
routerLink="/myFavorites"
routerLinkActive="text-blue-500"
pRipple
class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"
>
<i class="pi pi-star md:mr-2"></i>
<span class="font-medium hidden md:block">My Favorites</span>
</a>
</li>
<li>
<a routerLink="/emailUs" routerLinkActive="text-blue-500" pRipple class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline">
<fa-icon [icon]="faEnvelope" class="mr-2 flex"></fa-icon>
<span class="font-medium hidden md:block">Email Us</span>
</a>
</li>
<li>
<a
(click)="userService.logout()"
routerLinkActive="text-blue-500"
pRipple
class="flex align-items-center cursor-pointer p-3 border-round text-800 hover:surface-200 transition-duration-150 transition-colors no-underline"
>
<fa-icon [icon]="faRightFromBracket" class="mr-2 flex"></fa-icon>
<span class="font-medium hidden md:block">Logout</span>
</a>
</li>
</ul>
</div>
</div>

View File

@ -1,155 +1,160 @@
<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 class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<div *ngIf="listing" class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">{{ mode === 'create' ? 'New' : 'Edit' }} Listing</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Listing category</label>
<p-dropdown
id="listingCategory"
[options]="selectOptions?.listingCategories"
[ngModel]="listingsCategory"
optionLabel="name"
optionValue="value"
(ngModelChange)="changeListingCategory($event)"
placeholder="Listing category"
[disabled]="mode === 'edit'"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="mb-4">
<label for="email" class="block font-medium text-900 mb-2">Title of Listing</label>
<input id="email" type="text" pInputText [(ngModel)]="listing.title" />
</div>
<div>
<div class="mb-4">
<label for="description" class="block font-medium text-900 mb-2">Description</label>
<!-- <textarea id="description" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.description"></textarea> -->
<p-editor [(ngModel)]="listing.description" [style]="{ height: '320px' }" [modules]="editorModules">
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
<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 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

@ -1,98 +1,92 @@
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 { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { createGenericObject, getListingType, routeListingWithState } from '../../../utils/utils';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { ConfirmationService, MessageService } from 'primeng/api';
import { CarouselModule } from 'primeng/carousel';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { EditorModule } from 'primeng/editor';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { LoadingService } from '../../../services/loading.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { 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',
selector: 'business-listing',
standalone: true,
imports: [SharedModule, ArrayToStringPipe, InputNumberModule, CarouselModule,
DialogModule, AngularCropperjsModule, FileUploadModule, EditorModule, DynamicDialogModule, DragDropModule,
ConfirmDialogModule, MixedCdkDragDropModule],
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'
styleUrl: './edit-business-listing.component.scss',
})
export class EditBusinessListingComponent {
@ViewChild(FileUpload) public fileUpload: FileUpload;
listingsCategory = 'commercialProperty'
listingsCategory = 'business';
category: string;
location: string;
mode: 'edit' | 'create';
separator: '\n\n'
listing: BusinessListing
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[]
propertyImages: ImageProperty[];
responsiveOptions = [
{
breakpoint: '1199px',
numVisible: 1,
numScroll: 1
numScroll: 1,
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1
numScroll: 1,
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1
}
numScroll: 1,
},
];
config = { aspectRatio: 16 / 9 }
editorModules = TOOLBAR_OPTIONS
config = { aspectRatio: 16 / 9 };
editorModules = TOOLBAR_OPTIONS;
dialogRef: DynamicDialogRef | undefined;
draggedImage: ImageProperty
draggedImage: ImageProperty;
faTrash = faTrash;
constructor(public selectOptions: SelectOptionsService,
data: CommercialPropertyListing;
constructor(
public selectOptions: SelectOptionsService,
private router: Router,
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
@ -102,106 +96,48 @@ export class EditBusinessListingComponent {
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
private confirmationService: ConfirmationService,
private route: ActivatedRoute,
) {
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
this.mode = event.url === '/createListing' ? 'create' : 'edit';
this.mode = event.url === '/createBusinessListing' ? 'create' : 'edit';
}
});
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
this.data = this.router.getCurrentNavigation().extras.state['data'];
}
});
}
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.listing.listingsCategory = 'business';
this.listing.userId = await this.userService.getId();
this.listing.title = this.data?.title;
this.listing.description = this.data?.description;
}
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
}
async save() {
sessionStorage.removeItem('uuid')
await this.listingsService.save(this.listing, getListingType(this.listing));
this.listing = await this.listingsService.save(this.listing, getListingType(this.listing));
this.router.navigate(['editBusinessListing', this.listing.id]);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 });
}
suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state))
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();
}
})
changeListingCategory(value: 'business' | 'commercialProperty') {
routeListingWithState(this.router, value, this.listing);
}
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,110 +1,126 @@
<div class="surface-ground px-4 py-8 md:px-6 lg:px-8">
<div class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<div *ngIf="listing" class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">{{mode==='create'?'New':'Edit'}} Listing</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Listing category</label>
<p-dropdown id="listingCategory" [options]="selectOptions?.listingCategories"
[(ngModel)]="listingsCategory" optionLabel="name" optionValue="value"
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 class="p-fluid flex flex-column lg:flex-row">
<menu-account></menu-account>
<p-toast></p-toast>
<div *ngIf="listing" class="surface-card p-5 shadow-2 border-round flex-auto">
<div class="text-900 font-semibold text-lg mt-3">{{ mode === 'create' ? 'New' : 'Edit' }} Listing</div>
<p-divider></p-divider>
<div class="flex gap-5 flex-column-reverse md:flex-row">
<div class="flex-auto p-fluid">
<div class="mb-4">
<label for="listingCategory" class="block font-medium text-900 mb-2">Listing category</label>
<p-dropdown
id="listingCategory"
[options]="selectOptions?.listingCategories"
[ngModel]="listingsCategory"
optionLabel="name"
optionValue="value"
(ngModelChange)="changeListingCategory($event)"
placeholder="Listing category"
[disabled]="mode === 'edit'"
[style]="{ width: '100%' }"
></p-dropdown>
</div>
<div class="mb-4">
<label for="email" class="block font-medium text-900 mb-2">Title of Listing</label>
<input id="email" type="text" pInputText [(ngModel)]="listing.title" />
</div>
<div>
<div class="mb-4">
<label for="description" class="block font-medium text-900 mb-2">Description</label>
<!-- <textarea id="description" type="text" pInputTextarea rows="5" [autoResize]="true" [(ngModel)]="listing.description"></textarea> -->
<p-editor [(ngModel)]="listing.description" [style]="{ height: '320px' }" [modules]="editorModules">
<ng-template pTemplate="header"></ng-template>
</p-editor>
</div>
<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 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>
<span class="font-light text-sm text-900 mb-2">(Pictures can be uploaded once the listing is posted initially)</span>
<!-- <img [src]="propertyPictureUrl" (error)="setImageToFallback($event)" class="image"/> -->
<p-fileUpload
mode="basic"
chooseLabel="Upload"
[customUpload]="true"
name="file"
accept="image/*"
[maxFileSize]="maxFileSize"
(onSelect)="select($event)"
styleClass="p-button-outlined p-button-plain p-button-rounded mt-4"
[disabled]="true"
>
</p-fileUpload>
</div>
</div>
</div>
@if (propertyImages?.length>0){
<div class="p-2 border-1 surface-border border-round mb-4 image-container" cdkDropListGroup mixedCdkDragDrop (dropped)="onDrop($event)" cdkDropListOrientation="horizontal">
@for (image of propertyImages; track image) {
<span cdkDropList mixedCdkDropList>
<div cdkDrag mixedCdkDragSizeHelper class="image-wrap">
<img src="{{ 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

@ -1,98 +1,96 @@
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 { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { createGenericObject, getListingType, routeListingWithState } from '../../../utils/utils';
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { HttpEventType } from '@angular/common/http';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { ConfirmationService, MessageService } from 'primeng/api';
import { CarouselModule } from 'primeng/carousel';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { EditorModule } from 'primeng/editor';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { LoadingService } from '../../../services/loading.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { 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',
selector: 'commercial-property-listing',
standalone: true,
imports: [SharedModule, ArrayToStringPipe, InputNumberModule, CarouselModule,
DialogModule, AngularCropperjsModule, FileUploadModule, EditorModule, DynamicDialogModule, DragDropModule,
ConfirmDialogModule, MixedCdkDragDropModule],
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'
styleUrl: './edit-commercial-property-listing.component.scss',
})
export class EditCommercialPropertyListingComponent {
@ViewChild(FileUpload) public fileUpload: FileUpload;
listingsCategory = 'commercialProperty'
listingsCategory = 'commercialProperty';
category: string;
location: string;
mode: 'edit' | 'create';
separator: '\n\n'
listing: CommercialPropertyListing
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[]
propertyImages: ImageProperty[];
responsiveOptions = [
{
breakpoint: '1199px',
numVisible: 1,
numScroll: 1
numScroll: 1,
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1
numScroll: 1,
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1
}
numScroll: 1,
},
];
config = { aspectRatio: 16 / 9 }
editorModules = TOOLBAR_OPTIONS
config = { aspectRatio: 16 / 9 };
editorModules = TOOLBAR_OPTIONS;
dialogRef: DynamicDialogRef | undefined;
draggedImage: ImageProperty
draggedImage: ImageProperty;
faTrash = faTrash;
constructor(public selectOptions: SelectOptionsService,
suggestions: string[] | undefined;
data: BusinessListing;
userId: string;
constructor(
public selectOptions: SelectOptionsService,
private router: Router,
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
@ -102,40 +100,43 @@ export class EditCommercialPropertyListingComponent {
private imageService: ImageService,
private loadingService: LoadingService,
public dialogService: DialogService,
private confirmationService: ConfirmationService) {
this.user = this.userService.getUser();
private confirmationService: ConfirmationService,
private route: ActivatedRoute,
) {
// 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';
this.mode = event.url === '/createCommercialPropertyListing' ? 'create' : 'edit';
}
});
this.route.data.subscribe(async () => {
if (this.router.getCurrentNavigation().extras.state) {
this.data = this.router.getCurrentNavigation().extras.state['data'];
}
});
}
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.listing.userId = await this.userService.getId();
this.listing.title = this.data?.title;
this.listing.description = this.data?.description;
}
this.uploadUrl = `${environment.apiBaseUrl}/bizmatch/image/uploadPropertyPicture/${this.listing.id}`;
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id)
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
}
async save() {
sessionStorage.removeItem('uuid')
await this.listingsService.save(this.listing, getListingType(this.listing));
this.listing = await this.listingsService.save(this.listing, getListingType(this.listing));
this.router.navigate(['editCommercialPropertyListing', this.listing.id]);
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing changes have been persisted', life: 3000 });
}
suggestions: string[] | undefined;
async search(event: AutoCompleteCompleteEvent) {
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state))
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
this.suggestions = result.map(r => r.city).slice(0, 5);
}
@ -145,7 +146,7 @@ export class EditCommercialPropertyListingComponent {
data: {
imageUrl: imageUrl,
fileUpload: this.fileUpload,
ratioVariable: false
ratioVariable: false,
},
header: 'Edit Image',
width: '50vw',
@ -155,24 +156,27 @@ export class EditCommercialPropertyListingComponent {
closable: false,
breakpoints: {
'960px': '75vw',
'640px': '90vw'
'640px': '90vw',
},
});
this.dialogRef.onClose.subscribe(cropper => {
if (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));
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) {
@ -181,27 +185,28 @@ export class EditCommercialPropertyListingComponent {
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",
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)
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
},
reject: () => {
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
console.log('deny')
}
console.log('deny');
},
});
}
onDrop(event: { previousIndex: number; currentIndex: number }) {
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages)
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages);
}
changeListingCategory(value: 'business' | 'commercialProperty') {
routeListingWithState(this.router, value, this.listing);
}
}

View File

@ -0,0 +1,216 @@
<!-- <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>
<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>
<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>
<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>
<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>
<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-inputSwitch inputId="draft" [(ngModel)]="listing.draft"></p-inputSwitch>
<span class="ml-2 text-900 absolute translate-y-5">Draft Mode (Will not be shown as public listing)</span>
</div>
</div>
}
<div>
@if (mode==='create'){
<button pButton pRipple label="Post Listing" class="w-auto" (click)="save()"></button>
} @else {
<button pButton pRipple label="Update Listing" class="w-auto" (click)="save()"></button>
}
</div>
</div>
</div>
</div>
</div>
</div>
<p-toast></p-toast>
<p-confirmDialog></p-confirmDialog> -->

View File

@ -1,60 +1,55 @@
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 { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { createGenericObject } from '../../../utils/utils';
import { DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { HttpEventType } from '@angular/common/http';
import { faTrash } from '@fortawesome/free-solid-svg-icons';
import { AngularCropperjsModule } from 'angular-cropperjs';
import { MixedCdkDragDropModule } from 'angular-mixed-cdk-drag-drop';
import { ConfirmationService, MessageService } from 'primeng/api';
import { CarouselModule } from 'primeng/carousel';
import { ConfirmDialogModule } from 'primeng/confirmdialog';
import { DialogModule } from 'primeng/dialog';
import { DialogService, DynamicDialogModule, DynamicDialogRef } from 'primeng/dynamicdialog';
import { EditorModule } from 'primeng/editor';
import { FileUpload, FileUploadModule } from 'primeng/fileupload';
import { v4 as uuidv4 } from 'uuid';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { AutoCompleteCompleteEvent, ImageProperty, ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment';
import { ImageCropperComponent } from '../../../components/image-cropper/image-cropper.component';
import { InputNumberModule } from '../../../components/inputnumber/inputnumber.component';
import { ArrayToStringPipe } from '../../../pipes/array-to-string.pipe';
import { GeoService } from '../../../services/geo.service';
import { ImageService } from '../../../services/image.service';
import { LoadingService } from '../../../services/loading.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { 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],
imports: [
SharedModule,
ArrayToStringPipe,
InputNumberModule,
CarouselModule,
DialogModule,
AngularCropperjsModule,
FileUploadModule,
EditorModule,
DynamicDialogModule,
DragDropModule,
ConfirmDialogModule,
MixedCdkDragDropModule,
],
providers: [MessageService, DialogService, ConfirmationService],
templateUrl: './edit-listing.component.html',
styleUrl: './edit-listing.component.scss'
styleUrl: './edit-listing.component.scss',
})
export class EditListingComponent {
@ViewChild(FileUpload) public fileUpload: FileUpload;
@ -62,37 +57,38 @@ export class EditListingComponent {
category: string;
location: string;
mode: 'edit' | 'create';
separator: '\n\n'
listing: ListingType
separator: '\n\n';
listing: ListingType;
private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined;
user: User;
maxFileSize = 3000000;
uploadUrl: string;
environment = environment;
propertyImages: ImageProperty[]
propertyImages: ImageProperty[];
responsiveOptions = [
{
breakpoint: '1199px',
numVisible: 1,
numScroll: 1
numScroll: 1,
},
{
breakpoint: '991px',
numVisible: 2,
numScroll: 1
numScroll: 1,
},
{
breakpoint: '767px',
numVisible: 1,
numScroll: 1
}
numScroll: 1,
},
];
config = { aspectRatio: 16 / 9 }
editorModules = TOOLBAR_OPTIONS
config = { aspectRatio: 16 / 9 };
editorModules = TOOLBAR_OPTIONS;
dialogRef: DynamicDialogRef | undefined;
draggedImage: ImageProperty
draggedImage: ImageProperty;
faTrash = faTrash;
constructor(public selectOptions: SelectOptionsService,
constructor(
public selectOptions: SelectOptionsService,
private router: Router,
private activatedRoute: ActivatedRoute,
private listingsService: ListingsService,
@ -102,7 +98,8 @@ export class EditListingComponent {
private imageService: ImageService,
private loadingService: LoadingService,
public dialogService: DialogService,
private confirmationService: ConfirmationService) {
private confirmationService: ConfirmationService,
) {
this.user = this.userService.getUser();
// Abonniere Router-Events, um den aktiven Link zu ermitteln
this.router.events.subscribe(event => {
@ -110,7 +107,6 @@ export class EditListingComponent {
this.mode = event.url === '/createListing' ? 'create' : 'edit';
}
});
}
async ngOnInit() {
if (this.mode === 'edit') {
@ -119,23 +115,23 @@ export class EditListingComponent {
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.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)
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
}
async save() {
sessionStorage.removeItem('uuid')
await this.listingsService.save(this.listing, getListingType(this.listing));
sessionStorage.removeItem('uuid');
// await this.listingsService.save(this.listing, this.listing.listingsCategory);
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))
const result = await lastValueFrom(this.geoService.findCitiesStartingWith(event.query, this.listing.state));
this.suggestions = result.map(r => r.city).slice(0, 5);
}
@ -145,7 +141,7 @@ export class EditListingComponent {
data: {
imageUrl: imageUrl,
fileUpload: this.fileUpload,
ratioVariable: false
ratioVariable: false,
},
header: 'Edit Image',
width: '50vw',
@ -155,24 +151,27 @@ export class EditListingComponent {
closable: false,
breakpoints: {
'960px': '75vw',
'640px': '90vw'
'640px': '90vw',
},
});
this.dialogRef.onClose.subscribe(cropper => {
if (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));
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) {
@ -181,27 +180,24 @@ export class EditListingComponent {
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",
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)
this.propertyImages = await this.listingsService.getPropertyImages(this.listing.id);
},
reject: () => {
// this.messageService.add({ severity: 'error', summary: 'Rejected', detail: 'You have rejected' });
console.log('deny')
}
console.log('deny');
},
});
}
onDrop(event: { previousIndex: number; currentIndex: number }) {
moveItemInArray(this.propertyImages, event.previousIndex, event.currentIndex);
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages)
this.listingsService.changeImageOrder(this.listing.id, this.propertyImages);
}
}

View File

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

View File

@ -1,52 +1,53 @@
import { ChangeDetectorRef, Component } from '@angular/core';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
import dataListings from '../../../../assets/data/listings.json';
import { SharedModule } from '../../../shared/shared/shared.module';
import { UserService } from '../../../services/user.service';
import { ListingsService } from '../../../services/listings.service';
import { lastValueFrom } from 'rxjs';
import { SelectOptionsService } from '../../../services/select-options.service';
import { ConfirmationService, MessageService } from 'primeng/api';
import { User } from '../../../../../../bizmatch-server/src/models/db.model';
import { ListingType } from '../../../../../../bizmatch-server/src/models/main.model';
import { getListingType } from '../../../utils/utils';
import { ListingsService } from '../../../services/listings.service';
import { SelectOptionsService } from '../../../services/select-options.service';
import { UserService } from '../../../services/user.service';
import { SharedModule } from '../../../shared/shared/shared.module';
import { getListingType, isBusinessListing, isCommercialPropertyListing } from '../../../utils/utils';
import { MenuAccountComponent } from '../../menu-account/menu-account.component';
@Component({
selector: 'app-my-listing',
standalone: true,
imports: [MenuAccountComponent, SharedModule],
providers:[ConfirmationService,MessageService],
providers: [ConfirmationService, MessageService],
templateUrl: './my-listing.component.html',
styleUrl: './my-listing.component.scss'
styleUrl: './my-listing.component.scss',
})
export class MyListingComponent {
user: User;
listings: Array<ListingType> =[]//dataListings as unknown as Array<BusinessListing>;
myListings: Array<ListingType>
constructor(public userService: UserService,private listingsService:ListingsService, private cdRef:ChangeDetectorRef,public selectOptions:SelectOptionsService,private confirmationService: ConfirmationService,private messageService: MessageService){
this.user=this.userService.getUser();
}
async ngOnInit(){
// this.listings=await lastValueFrom(this.listingsService.getAllListings());
this.myListings=this.listings.filter(l=>l.userId===this.user.id);
listings: Array<ListingType> = []; //dataListings as unknown as Array<BusinessListing>;
myListings: Array<ListingType>;
userId: string;
isBusinessListing = isBusinessListing;
isCommercialPropertyListing = isCommercialPropertyListing;
constructor(
public userService: UserService,
private listingsService: ListingsService,
private cdRef: ChangeDetectorRef,
public selectOptions: SelectOptionsService,
private confirmationService: ConfirmationService,
private messageService: MessageService,
) {}
async ngOnInit() {
this.userId = await this.userService.getId();
this.myListings = await this.listingsService.getListingByUserId(this.userId);
}
async deleteListing(listing:ListingType){
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);
async deleteListing(listing: ListingType) {
await this.listingsService.deleteListing(listing.id, getListingType(listing));
this.myListings = await this.listingsService.getListingByUserId(this.userId);
}
confirm(event: Event,listing:ListingType) {
confirm(event: Event, listing: ListingType) {
this.confirmationService.confirm({
target: event.target as EventTarget,
message: 'Are you sure you want to delet this listing?',
icon: 'pi pi-exclamation-triangle',
accept: () => {
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been deleted', life: 3000 });
this.deleteListing(listing);
}
target: event.target as EventTarget,
message: 'Are you sure you want to delet this listing?',
icon: 'pi pi-exclamation-triangle',
accept: () => {
this.messageService.add({ severity: 'info', summary: 'Confirmed', detail: 'Listing has been deleted', life: 3000 });
this.deleteListing(listing);
},
});
}
}
}

View File

@ -1,44 +1,45 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, lastValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
import onChange from 'on-change';
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';
import { ImageProperty, ListingCriteria, ListingType, ResponseBusinessListingArray } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment';
@Injectable({
providedIn: 'root'
providedIn: 'root',
})
export class ListingsService {
private apiBaseUrl = environment.apiBaseUrl;
constructor(private http: HttpClient) {
}
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<ListingType[]>{
const result = await lastValueFrom(this.http.post<ResponseBusinessListingArray>(`${this.apiBaseUrl}/bizmatch/listings/${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>{
getListingById(id: string, listingsCategory?: 'business' | 'commercialProperty'): Observable<ListingType> {
const result = this.http.get<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`);
return result;
}
getListingByUserId(userid:string):Promise<BusinessListing[]>{
getListingByUserId(userid: string): Promise<ListingType[]> {
return lastValueFrom(this.http.get<BusinessListing[]>(`${this.apiBaseUrl}/bizmatch/listings/business/user/${userid}`));
}
async save(listing:any,listingsCategory:'business'|'professionals_brokers'|'commercialProperty'){
await lastValueFrom(this.http.post<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}`,listing));
async save(listing: any, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
if (listing.id) {
return await lastValueFrom(this.http.put<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}`, listing));
} else {
return await lastValueFrom(this.http.post<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}`, listing));
}
}
async deleteListing(id:string,listingsCategory:'business'|'professionals_brokers'|'commercialProperty'){
async deleteListing(id: string, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty') {
await lastValueFrom(this.http.delete<ListingType>(`${this.apiBaseUrl}/bizmatch/listings/${listingsCategory}/${id}`));
}
async getPropertyImages(id:string):Promise<ImageProperty[]>{
async getPropertyImages(id: string): Promise<ImageProperty[]> {
return await lastValueFrom(this.http.get<ImageProperty[]>(`${this.apiBaseUrl}/bizmatch/image/${id}`));
}
async changeImageOrder(id:string, propertyImages: ImageProperty[]):Promise<ImageProperty[]>{
return await lastValueFrom(this.http.put<ImageProperty[]>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/imageOrder/${id}`,propertyImages));
async changeImageOrder(id: string, propertyImages: ImageProperty[]): Promise<ImageProperty[]> {
return await lastValueFrom(this.http.put<ImageProperty[]>(`${this.apiBaseUrl}/bizmatch/listings/commercialProperty/imageOrder/${id}`, propertyImages));
}
}

View File

@ -75,7 +75,17 @@ export class UserService {
getUserObservable(): Observable<User> {
return this.user$;
}
async getId(): Promise<string> {
if (sessionStorage.getItem('USERID')) {
return sessionStorage.getItem('USERID');
} else {
const user = await this.getByMail(this.user.email);
sessionStorage.setItem('USERID', user.id);
return user.id;
}
}
logout() {
sessionStorage.removeItem('USERID');
this.keycloak.logout(window.location.origin + '/home');
}
async login(url: string) {

View File

@ -1,48 +1,56 @@
import { INFO, ConsoleFormattedStream, createLogger as _createLogger, stdSerializers } from "browser-bunyan";
import { ListingCriteria } from "../../../../bizmatch-server/src/models/main.model";
import { BusinessListing, CommercialPropertyListing } from "../../../../bizmatch-server/src/models/db.model";
import { Router } from '@angular/router';
import { ConsoleFormattedStream, INFO, createLogger as _createLogger, stdSerializers } from 'browser-bunyan';
import { BusinessListing, CommercialPropertyListing } from '../../../../bizmatch-server/src/models/db.model';
import { ListingCriteria } from '../../../../bizmatch-server/src/models/main.model';
export function createGenericObject<T>(): T {
// Ein leeres Objekt vom Typ T erstellen
const ergebnis: Partial<T> = {};
// Ein leeres Objekt vom Typ T erstellen
const ergebnis: Partial<T> = {};
// Für ein reales Interface funktioniert diese direkte Iteration nicht,
// da Interfaces zur Compile-Zeit entfernt werden. Stattdessen könnten Sie
// ein Dummy-Objekt oder spezifische Typtransformationen verwenden.
// Hier nur als Pseudocode dargestellt, um die Idee zu vermitteln:
for (const key in ergebnis) {
ergebnis[key] = null; // oder undefined, je nach Bedarf
}
return ergebnis as T;
// Für ein reales Interface funktioniert diese direkte Iteration nicht,
// da Interfaces zur Compile-Zeit entfernt werden. Stattdessen könnten Sie
// ein Dummy-Objekt oder spezifische Typtransformationen verwenden.
// Hier nur als Pseudocode dargestellt, um die Idee zu vermitteln:
for (const key in ergebnis) {
ergebnis[key] = null; // oder undefined, je nach Bedarf
}
export function createLogger(name:string, level: number = INFO, options:any = {}){
return _createLogger({
name,
streams:[{level, stream: new ConsoleFormattedStream()}],
serializers:stdSerializers,
src:true,
...options,
})
}
return ergebnis as T;
}
export const getSessionStorageHandler = function(path,value,previous,applyData){
sessionStorage.setItem('criteria',JSON.stringify(this));
}
export function createLogger(name: string, level: number = INFO, options: any = {}) {
return _createLogger({
name,
streams: [{ level, stream: new ConsoleFormattedStream() }],
serializers: stdSerializers,
src: true,
...options,
});
}
export function getCriteriaStateObject(){
const initialState = createGenericObject<ListingCriteria>();
const storedState = sessionStorage.getItem('criteria');
return storedState ? JSON.parse(storedState) : initialState;
}
export const getSessionStorageHandler = function (path, value, previous, applyData) {
sessionStorage.setItem('criteria', JSON.stringify(this));
};
export function getListingType(listing:BusinessListing|CommercialPropertyListing):'business'|'commercialProperty'{
return listing?.type<100?'business':'commercialProperty';
}
export function isBusinessListing(listing: BusinessListing | CommercialPropertyListing): listing is BusinessListing {
return listing?.type < 100;
}
export function isCommercialPropertyListing(listing: BusinessListing | CommercialPropertyListing): listing is CommercialPropertyListing {
return listing?.type >= 100;
export function getCriteriaStateObject() {
const initialState = createGenericObject<ListingCriteria>();
const storedState = sessionStorage.getItem('criteria');
return storedState ? JSON.parse(storedState) : initialState;
}
export function getListingType(listing: BusinessListing | CommercialPropertyListing): 'business' | 'commercialProperty' {
return listing?.type < 100 ? 'business' : 'commercialProperty';
}
export function isBusinessListing(listing: BusinessListing | CommercialPropertyListing): listing is BusinessListing {
return listing?.type < 100;
}
export function isCommercialPropertyListing(listing: BusinessListing | CommercialPropertyListing): listing is CommercialPropertyListing {
return listing?.type >= 100;
}
export function routeListingWithState(router: Router, value: string, data: any) {
if (value === 'business') {
router.navigate(['createBusinessListing'], { state: { data } });
} else {
router.navigate(['createCommercialPropertyListing'], { state: { data } });
}
}