Issue #55 View on map

This commit is contained in:
Andreas Knuth 2024-09-16 13:42:22 +02:00
parent 8595e70ceb
commit c00c2caccc
16 changed files with 187 additions and 16 deletions

View File

@ -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"

View File

@ -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",

View File

@ -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}<br/>
<a href="#" id="view-full-map">View larger map</a>
`;
// 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');
}
}

View File

@ -56,6 +56,12 @@
<share-button button="x" showText="true" (click)="createEvent('x')"></share-button>
<share-button button="linkedin" showText="true" (click)="createEvent('linkedin')"></share-button>
</div>
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
<div *ngIf="listing.location.street" class="mt-6">
<h2 class="text-lg font-semibold mb-2">Location Map</h2>
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
</div>
</div>
<!-- Right column -->

View File

@ -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<BusinessListing>;
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()}` },

View File

@ -52,6 +52,12 @@
<share-button button="x" showText="true" (click)="createEvent('x')"></share-button>
<share-button button="linkedin" showText="true" (click)="createEvent('linkedin')"></share-button>
</div>
<!-- Karte hinzufügen, wenn Straße vorhanden ist -->
<div *ngIf="listing.location.street" class="mt-6">
<h2 class="text-lg font-semibold mb-2">Location Map</h2>
<!-- <div style="height: 300px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom"></div> -->
<div style="height: 400px" leaflet [leafletOptions]="mapOptions" [leafletLayers]="mapLayers" [leafletCenter]="mapCenter" [leafletZoom]="mapZoom" (leafletMapReady)="onMapReady($event)"></div>
</div>
</div>
<div class="w-full lg:w-1/2 mt-6 lg:mt-0">

View File

@ -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']);

View File

@ -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;
}

View File

@ -33,7 +33,7 @@
</p>
<p class="text-sm text-gray-600 mb-2"><strong>Sales revenue:</strong> {{ listing.salesRevenue | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
<p class="text-sm text-gray-600 mb-2"><strong>Net profit:</strong> {{ listing.cashFlow | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
<p class="text-sm text-gray-600 mb-2"><strong>Location:</strong> {{ listing.location.name }}</p>
<p class="text-sm text-gray-600 mb-2"><strong>Location:</strong> {{ listing.location.name ? listing.location.name : listing.location.county }}</p>
<p class="text-sm text-gray-600 mb-4"><strong>Established:</strong> {{ listing.established }}</p>
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imageName }}.avif?_ts={{ ts }}" alt="Company logo" class="absolute bottom-[80px] right-[20px] h-[45px] w-auto" />

View File

@ -23,7 +23,7 @@
<span class="bg-red-100 text-red-800 text-sm font-medium me-2 ml-2 px-2.5 py-0.5 rounded dark:bg-red-900 dark:text-red-300">Draft</span>
}
</h3>
<p class="text-gray-600 mb-2">{{ listing.location.name }}</p>
<p class="text-gray-600 mb-2">{{ listing.location.name ? listing.location.name : listing.location.county }}</p>
<p class="text-xl font-bold mb-4">{{ listing.price | currency : 'USD' : 'symbol' : '1.0-0' }}</p>
<div class="flex-grow"></div>
<button [routerLink]="['/details-commercial-property-listing', listing.id]" class="bg-green-500 text-white px-4 py-2 rounded-full w-full hover:bg-green-600 transition duration-300 mt-auto">

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

View File

@ -11,6 +11,8 @@
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import 'ngx-sharebuttons/themes/default';
/* styles.scss */
@import 'leaflet/dist/leaflet.css';
:root {
--text-color-secondary: rgba(255, 255, 255);
--wrapper-width: 1491px;
@ -95,3 +97,20 @@ input::placeholder,
textarea::placeholder {
color: #999 !important;
}
/* Fix für Marker-Icons in Leaflet */
.leaflet-container {
height: 100%;
width: 100%;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
.leaflet-marker-icon {
/* Optional: Anpassen der Marker-Icon-Größe */
width: 25px;
height: 41px;
}