diff --git a/bizmatch/angular.json b/bizmatch/angular.json index d10bf7d..02ebf47 100644 --- a/bizmatch/angular.json +++ b/bizmatch/angular.json @@ -32,7 +32,8 @@ ], "styles": [ "src/styles.scss", - "node_modules/quill/dist/quill.snow.css" + "node_modules/quill/dist/quill.snow.css", + "node_modules/leaflet/dist/leaflet.css" ] }, "configurations": { @@ -81,7 +82,9 @@ } }, "defaultConfiguration": "development", - "options": {"proxyConfig": "proxy.conf.json"} + "options": { + "proxyConfig": "proxy.conf.json" + } }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", @@ -102,7 +105,12 @@ "src/assets", "cropped-Favicon-32x32.png", "cropped-Favicon-180x180.png", - "cropped-Favicon-191x192.png" + "cropped-Favicon-191x192.png", + { + "glob": "**/*", + "input": "./node_modules/leaflet/dist/images", + "output": "assets/" + } ], "styles": [ "src/styles.scss" @@ -116,4 +124,4 @@ "cli": { "analytics": false } -} +} \ No newline at end of file diff --git a/bizmatch/package.json b/bizmatch/package.json index fa4d491..5cf23a8 100644 --- a/bizmatch/package.json +++ b/bizmatch/package.json @@ -23,6 +23,7 @@ "@angular/platform-browser-dynamic": "^18.1.3", "@angular/platform-server": "^18.1.3", "@angular/router": "^18.1.3", + "@bluehalo/ngx-leaflet": "^18.0.2", "@fortawesome/angular-fontawesome": "^0.15.0", "@fortawesome/fontawesome-free": "^6.5.2", "@fortawesome/fontawesome-svg-core": "^6.5.2", @@ -33,6 +34,7 @@ "@ngneat/until-destroy": "^10.0.0", "@stripe/stripe-js": "^4.3.0", "@types/cropperjs": "^1.3.0", + "@types/leaflet": "^1.9.12", "@types/uuid": "^10.0.0", "browser-bunyan": "^1.8.0", "dayjs": "^1.11.11", @@ -41,6 +43,7 @@ "jwt-decode": "^4.0.0", "keycloak-angular": "^16.0.1", "keycloak-js": "^25.0.1", + "leaflet": "^1.9.4", "memoize-one": "^6.0.0", "ng-gallery": "^11.0.0", "ngx-currency": "^18.0.0", diff --git a/bizmatch/src/app/pages/details/base-details.component.ts b/bizmatch/src/app/pages/details/base-details.component.ts new file mode 100644 index 0000000..3a46fcf --- /dev/null +++ b/bizmatch/src/app/pages/details/base-details.component.ts @@ -0,0 +1,94 @@ +import { Component } from '@angular/core'; +import { Control, DomEvent, DomUtil, icon, Icon, latLng, Layer, Map, MapOptions, Marker, tileLayer } from 'leaflet'; +import { BusinessListing, CommercialPropertyListing } from '../../../../../bizmatch-server/src/models/db.model'; +@Component({ + selector: 'app-base-details', + template: ``, + standalone: true, + imports: [], +}) +export abstract class BaseDetailsComponent { + // Leaflet-Map-Einstellungen + mapOptions: MapOptions; + mapLayers: Layer[] = []; + mapCenter: any; + mapZoom: number = 13; // Standardzoomlevel + protected listing: BusinessListing | CommercialPropertyListing; + constructor() { + this.mapOptions = { + layers: [ + tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }), + ], + zoom: this.mapZoom, + center: latLng(0, 0), // Platzhalter, wird später gesetzt + }; + } + protected configureMap() { + const latitude = this.listing.location.latitude; + const longitude = this.listing.location.longitude; + + if (latitude && longitude) { + this.mapCenter = latLng(latitude, longitude); + this.mapLayers = [ + tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors', + }), + new Marker([latitude, longitude], { + icon: icon({ + ...Icon.Default.prototype.options, + iconUrl: 'assets/leaflet/marker-icon.png', + iconRetinaUrl: 'assets/leaflet/marker-icon-2x.png', + shadowUrl: 'assets/leaflet/marker-shadow.png', + }), + }), + ]; + this.mapOptions = { + ...this.mapOptions, + center: this.mapCenter, + zoom: this.mapZoom, + }; + } + } + onMapReady(map: Map) { + if (this.listing.location.street) { + const addressControl = new Control({ position: 'topright' }); + + addressControl.onAdd = () => { + const container = DomUtil.create('div', 'address-control bg-white p-2 rounded shadow'); + const address = `${this.listing.location.housenumber ? this.listing.location.housenumber : ''} ${this.listing.location.street}, ${ + this.listing.location.name ? this.listing.location.name : this.listing.location.county + }, ${this.listing.location.state}`; + container.innerHTML = ` + ${address}
+ View larger map + `; + + // Verhindere, dass die Karte durch das Klicken des Links bewegt wird + DomEvent.disableClickPropagation(container); + + // Füge einen Event Listener für den Link hinzu + const link = container.querySelector('#view-full-map') as HTMLElement; + if (link) { + DomEvent.on(link, 'click', (e: Event) => { + e.preventDefault(); + this.openFullMap(); + }); + } + + return container; + }; + + addressControl.addTo(map); + } + } + openFullMap() { + const latitude = this.listing.location.latitude; + const longitude = this.listing.location.longitude; + const address = `${this.listing.location.housenumber} ${this.listing.location.street}, ${this.listing.location.name ? this.listing.location.name : this.listing.location.county}, ${this.listing.location.state}`; + const url = `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=15/${latitude}/${longitude}`; + + window.open(url, '_blank'); + } +} diff --git a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.html b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.html index 77f0cd2..33c5d66 100644 --- a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.html +++ b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.html @@ -56,6 +56,12 @@ + +
+

Location Map

+ +
+
diff --git a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts index aaa0856..e396e29 100644 --- a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { LeafletModule } from '@bluehalo/ngx-leaflet'; import { KeycloakService } from 'keycloak-angular'; import { ShareButton } from 'ngx-sharebuttons/button'; import { lastValueFrom } from 'rxjs'; @@ -22,15 +23,18 @@ import { SelectOptionsService } from '../../../services/select-options.service'; import { UserService } from '../../../services/user.service'; import { SharedModule } from '../../../shared/shared/shared.module'; import { createMailInfo, map2User } from '../../../utils/utils'; +// Import für Leaflet +// Benannte Importe für Leaflet +import { BaseDetailsComponent } from '../base-details.component'; @Component({ selector: 'app-details-business-listing', standalone: true, - imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent], + imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent, LeafletModule], providers: [], templateUrl: './details-business-listing.component.html', styleUrl: '../details.scss', }) -export class DetailsBusinessListingComponent { +export class DetailsBusinessListingComponent extends BaseDetailsComponent { // listings: Array; responsiveOptions = [ { @@ -50,7 +54,7 @@ export class DetailsBusinessListingComponent { }, ]; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; - listing: BusinessListing; + override listing: BusinessListing; mailinfo: MailInfo; environment = environment; keycloakUser: KeycloakUser; @@ -60,6 +64,7 @@ export class DetailsBusinessListingComponent { private history: string[] = []; ts = new Date().getTime(); env = environment; + constructor( private activatedRoute: ActivatedRoute, private listingsService: ListingsService, @@ -76,12 +81,14 @@ export class DetailsBusinessListingComponent { public emailService: EMailService, private geoService: GeoService, ) { + super(); this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { this.history.push(event.urlAfterRedirects); } }); this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl }; + // Initialisiere die Map-Optionen } async ngOnInit() { @@ -96,11 +103,15 @@ export class DetailsBusinessListingComponent { this.auditService.createEvent(this.listing.id, 'view', this.user?.email); this.listingUser = await this.userService.getByMail(this.listing.email); this.description = this.sanitizer.bypassSecurityTrustHtml(this.listing.description); + if (this.listing.location.street) { + this.configureMap(); + } } catch (error) { this.auditService.log({ severity: 'error', text: error.error.message }); this.router.navigate(['notfound']); } } + ngOnDestroy() { this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten } @@ -139,7 +150,7 @@ export class DetailsBusinessListingComponent { } const result = [ { label: 'Category', value: this.selectOptions.getBusiness(this.listing.type) }, - { label: 'Located in', value: `${this.listing.location.name}, ${this.selectOptions.getState(this.listing.location.state)}` }, + { label: 'Located in', value: `${this.listing.location.name ? this.listing.location.name : this.listing.location.county}, ${this.selectOptions.getState(this.listing.location.state)}` }, { label: 'Asking Price', value: `$${this.listing.price?.toLocaleString()}` }, { label: 'Sales revenue', value: `$${this.listing.salesRevenue?.toLocaleString()}` }, { label: 'Cash flow', value: `$${this.listing.cashFlow?.toLocaleString()}` }, diff --git a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html index b126409..fcb9046 100644 --- a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html +++ b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html @@ -52,6 +52,12 @@ + +
+

Location Map

+ +
+
diff --git a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts index 31db425..b5fa0bd 100644 --- a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts @@ -1,6 +1,7 @@ import { Component, NgZone } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; +import { LeafletModule } from '@bluehalo/ngx-leaflet'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { KeycloakService } from 'keycloak-angular'; import { GalleryModule, ImageItem } from 'ng-gallery'; @@ -24,16 +25,17 @@ import { SelectOptionsService } from '../../../services/select-options.service'; import { UserService } from '../../../services/user.service'; import { SharedModule } from '../../../shared/shared/shared.module'; import { createMailInfo, map2User } from '../../../utils/utils'; +import { BaseDetailsComponent } from '../base-details.component'; @Component({ selector: 'app-details-commercial-property-listing', standalone: true, - imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent, GalleryModule], + imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent, GalleryModule, LeafletModule], providers: [], templateUrl: './details-commercial-property-listing.component.html', styleUrl: '../details.scss', }) -export class DetailsCommercialPropertyListingComponent { +export class DetailsCommercialPropertyListingComponent extends BaseDetailsComponent { responsiveOptions = [ { breakpoint: '1199px', @@ -52,7 +54,7 @@ export class DetailsCommercialPropertyListingComponent { }, ]; private id: string | undefined = this.activatedRoute.snapshot.params['id'] as string | undefined; - listing: CommercialPropertyListing; + override listing: CommercialPropertyListing; criteria: CommercialPropertyListingCriteria; mailinfo: MailInfo; environment = environment; @@ -83,9 +85,8 @@ export class DetailsCommercialPropertyListingComponent { private auditService: AuditService, private emailService: EMailService, ) { + super(); this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl }; - - // this.criteria = onChange(getCriteriaStateObject('commercialProperty'), getSessionStorageHandler); } async ngOnInit() { @@ -106,7 +107,7 @@ export class DetailsCommercialPropertyListingComponent { this.propertyDetails = [ { label: 'Property Category', value: this.selectOptions.getCommercialProperty(this.listing.type) }, { label: 'Located in', value: this.selectOptions.getState(this.listing.location.state) }, - { label: 'City', value: this.listing.location.name }, + { label: this.listing.location.name ? 'City' : 'County', value: this.listing.location.name ? this.listing.location.name : this.listing.location.county }, { label: 'Asking Price:', value: `$${this.listing.price?.toLocaleString()}` }, ]; if (this.listing.draft) { @@ -116,6 +117,9 @@ export class DetailsCommercialPropertyListingComponent { const imageURL = `${this.env.imageBaseUrl}/pictures/property/${this.listing.imagePath}/${this.listing.serialId}/${image}`; this.images.push(new ImageItem({ src: imageURL, thumb: imageURL })); }); + if (this.listing.location.street) { + this.configureMap(); + } } catch (error) { this.auditService.log({ severity: 'error', text: error.error.message }); this.router.navigate(['notfound']); diff --git a/bizmatch/src/app/pages/details/details.scss b/bizmatch/src/app/pages/details/details.scss index 3273eaf..1ed42e5 100644 --- a/bizmatch/src/app/pages/details/details.scss +++ b/bizmatch/src/app/pages/details/details.scss @@ -77,3 +77,23 @@ button.share { top: 10px; } } +/* details.scss */ + +/* Stil für das Adress-Info-Feld */ +.address-control { + background: rgba(255, 255, 255, 0.9); + padding: 10px; + border-radius: 5px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); + font-size: 14px; + line-height: 1.4; +} + +.address-control a { + color: #007bff; + text-decoration: none; +} + +.address-control a:hover { + text-decoration: underline; +} diff --git a/bizmatch/src/app/pages/listings/business-listings/business-listings.component.html b/bizmatch/src/app/pages/listings/business-listings/business-listings.component.html index 67937a6..eec18e7 100644 --- a/bizmatch/src/app/pages/listings/business-listings/business-listings.component.html +++ b/bizmatch/src/app/pages/listings/business-listings/business-listings.component.html @@ -33,7 +33,7 @@

Sales revenue: {{ listing.salesRevenue | currency : 'USD' : 'symbol' : '1.0-0' }}

Net profit: {{ listing.cashFlow | currency : 'USD' : 'symbol' : '1.0-0' }}

-

Location: {{ listing.location.name }}

+

Location: {{ listing.location.name ? listing.location.name : listing.location.county }}

Established: {{ listing.established }}

Company logo diff --git a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html index 071305b..20b871b 100644 --- a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html +++ b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html @@ -23,7 +23,7 @@ Draft } -

{{ listing.location.name }}

+

{{ listing.location.name ? listing.location.name : listing.location.county }}

{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}