import { CommonModule } from '@angular/common'; import { Component, ViewChild } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators, } from '@angular/forms'; import { GoogleMap, GoogleMapsModule, MapInfoWindow, MapMarker, } from '@angular/google-maps'; import { StrutturePubblicheControllerFindManyStrutture$Params } from '@api/fn/strutture-pubbliche/strutture-pubbliche-controller-find-many-strutture'; import { CittaRes, LuogoRes, ProvinciaRes, RegioneRes, StatoRes, StrutturePubblicheResDto, TipoStruttura, } from '@api/models'; import { LuoghiService } from '@core/services'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faCity, faGlobeEurope, faHospital, faMapLocationDot, } from '@fortawesome/free-solid-svg-icons'; import { RxState } from '@rx-angular/state'; import { MessageService, ToastMessageOptions } from 'primeng/api'; import { AutoCompleteModule } from 'primeng/autocomplete'; import { ButtonModule } from 'primeng/button'; import { DividerModule } from 'primeng/divider'; import { DropdownModule } from 'primeng/dropdown'; import { IconFieldModule } from 'primeng/iconfield'; import { InputIconModule } from 'primeng/inputicon'; import { InputTextModule } from 'primeng/inputtext'; import { MultiSelectModule } from 'primeng/multiselect'; import { RatingModule } from 'primeng/rating'; import { SelectModule } from 'primeng/select'; import { Table, TableModule } from 'primeng/table'; import { ToastModule } from 'primeng/toast'; import { ToggleButtonModule } from 'primeng/togglebutton'; import { catchError, EMPTY, endWith, filter, finalize, map, Observable, of, startWith, switchMap, tap, } from 'rxjs'; import { TableColumn } from '../../shared'; import { StrutturePubblicheService } from './strutture-pubbliche.service'; const cercaStruttureFormGroupFunc = () => { return new FormGroup({ tipologiaStruttura: new FormControl(null), stato: new FormControl(null, { validators: [Validators.required], }), regione: new FormControl(null, { validators: [Validators.required], }), provincia: new FormControl(null), citta: new FormControl(null), indirizzo: new FormControl(null), }); }; export type cercaStruttureForm = ReturnType; export type cercaStruttureFormValue = cercaStruttureForm['value']; export interface CercaStruttureComponentState { strutture: StrutturePubblicheResDto[]; tipologieStrutture: TipoStruttura[]; stati: Array; regioni: Array; province: Array; citta: Array; filteredLuoghiEsteso: Array; cercaStruttureFormSubmitted: boolean; tipologieStruttureAreLoading: boolean; struttureAreLoading: boolean; statiAreLoading: boolean; regioniAreLoading: boolean; provinceAreLoading: boolean; cittaAreLoading: boolean; luoghiAreLoading: boolean; isSearching: boolean; } type State = CercaStruttureComponentState; @Component({ selector: 'app-strutture-pubbliche', imports: [ CommonModule, FormsModule, ReactiveFormsModule, ButtonModule, DividerModule, MultiSelectModule, ToggleButtonModule, GoogleMapsModule, AutoCompleteModule, FontAwesomeModule, InputTextModule, DropdownModule, ToastModule, TableModule, IconFieldModule, InputIconModule, RatingModule, SelectModule, ], providers: [RxState, LuoghiService], templateUrl: './strutture-pubbliche.component.html', styleUrl: './strutture-pubbliche.component.scss', }) export class StrutturePubblicheComponent { model$: Observable; messages: ToastMessageOptions[] = []; @ViewChild('gmap', { static: false }) map!: GoogleMap; @ViewChild(MapInfoWindow) infoWindow!: MapInfoWindow; options: google.maps.MapOptions; markerOptions: google.maps.MarkerOptions[] = []; indirizzoGmaps: string = 'indirizoooo'; struttureTable!: Table; calcolaIndirizzoDa: string | null | undefined; @ViewChild('struttureTable') set table(table: Table) { if (!table || this.struttureTable === table) return; this.struttureTable = table; } cercaStruttureForm = cercaStruttureFormGroupFunc(); faCity = faCity; faGlobeEurope = faGlobeEurope; faMapLocationDot = faMapLocationDot; faHospital = faHospital; struttureConCoords: StrutturePubblicheResDto[] = []; cols: TableColumn[] = [ { header: 'Tipologia', field: 'struttura.struttureTipiStrutture', }, { header: 'Strutture', field: 'struttura.nome', sortField: 'struttura.nome', }, { header: 'Indirizzo', field: 'indirizzo', }, { header: 'Posizione', field: 'posizione', }, ]; constructor( private strutturePubblicheService: StrutturePubblicheService, private luoghiService: LuoghiService, public state: RxState, private messageService: MessageService, ) { this.model$ = this.state.select(); this.state.set({ strutture: undefined, stati: [], regioni: [], province: [], citta: [], tipologieStrutture: [], filteredLuoghiEsteso: [], cercaStruttureFormSubmitted: false, tipologieStruttureAreLoading: false, struttureAreLoading: false, statiAreLoading: false, regioniAreLoading: false, provinceAreLoading: false, cittaAreLoading: false, luoghiAreLoading: false, isSearching: false, }); const fetchTipologieStrutture$ = of(EMPTY).pipe( switchMap(() => this.strutturePubblicheService.getTipologieStrutture().pipe( catchError((err) => { this.messageService.add({ severity: 'error', life: 5000, summary: 'Attenzione!', detail: 'Impossibile recuperare i tipi struttura in questo momento, riprova più tardi', }); return of([] as TipoStruttura[]); }), map((res) => ({ tipologieStrutture: res, })), startWith({ tipologieStruttureAreLoading: true }), endWith({ tipologieStruttureAreLoading: false }), ), ), ); this.state.connect(fetchTipologieStrutture$); const fetchStati$ = this.strutturePubblicheService.getStati().pipe( tap(() => this.state.set({ statiAreLoading: true })), map((stati) => ({ stati })), tap((stati) => { if (stati) this.cercaStruttureForm.get('stato')?.setValue(stati.stati[0]); }), catchError(() => { this.messageService.add({ severity: 'error', life: 5000, summary: 'Attenzione!', detail: 'Impossibile recuperare gli stati in questo momento, riprova più tardi', }); return of({ stati: [] as StatoRes[] }); }), finalize(() => this.state.set({ statiAreLoading: false })), ); this.state.connect(fetchStati$); const fetchRegioni$ = this.cercaStruttureForm.controls.stato.valueChanges.pipe( tap(() => this.state.set({ regioniAreLoading: true })), tap((stato) => { const isITA = stato?.codiceStato === 'ITA'; if (!isITA) { // Reset completo this.resetLocation('stato'); // Disable cascata this.cercaStruttureForm.get('regione')?.disable(); this.cercaStruttureForm.get('provincia')?.disable(); this.cercaStruttureForm.get('citta')?.disable(); // Rimuovo il required da regione (non serve più) this.cercaStruttureForm.get('regione')?.clearValidators(); this.cercaStruttureForm.get('regione')?.updateValueAndValidity(); return; } // Se ITA → abilita tutto e rimetti validatori this.cercaStruttureForm.get('regione')?.enable(); this.cercaStruttureForm.get('provincia')?.enable(); this.cercaStruttureForm.get('citta')?.enable(); this.cercaStruttureForm .get('regione') ?.setValidators([Validators.required]); this.cercaStruttureForm.get('regione')?.updateValueAndValidity(); }), filter((stato) => !!stato), switchMap((stato) => this.strutturePubblicheService.getRegioni(stato.codiceStato).pipe( map((regioni) => ({ regioni })), catchError(() => of({ regioni: [] as RegioneRes[] })), finalize(() => this.state.set({ regioniAreLoading: false })), ), ), ); this.state.connect(fetchRegioni$); const fetchProvince$ = this.cercaStruttureForm.controls.regione.valueChanges.pipe( tap(() => this.state.set({ provinceAreLoading: true })), tap(() => this.resetLocation('regione')), filter((regione) => !!regione), switchMap((regione) => this.strutturePubblicheService .getProvince(regione.codiceRegione) .pipe( map((province) => ({ province })), catchError(() => of({ province: [] as ProvinciaRes[] })), finalize(() => this.state.set({ provinceAreLoading: false })), ), ), ); this.state.connect(fetchProvince$); const fetchCitta$ = this.cercaStruttureForm.controls.provincia.valueChanges.pipe( tap(() => this.state.set({ cittaAreLoading: true })), tap(() => this.resetLocation('provincia')), filter((provincia) => !!provincia), switchMap((provincia) => this.strutturePubblicheService .getCitta(provincia.siglaProvincia) .pipe( map((citta) => ({ citta })), catchError(() => of({ citta: [] as CittaRes[] })), finalize(() => this.state.set({ cittaAreLoading: false })), ), ), ); this.state.connect(fetchCitta$); this.options = { center: { lat: 45.4627123, lng: 9.1075213 }, zoom: 8, // mapId: 'c18657fa9d1be788abd38482', }; } fetchLuogo( event: { originalEvent: Event; query: string }, flagAttivo?: number, ) { if (!event.query || event.query.length < 3) { this.state.set({ filteredLuoghiEsteso: [] }); return; } const fetchCities$ = this.strutturePubblicheService .getLuoghiEsteso(event.query, flagAttivo) .pipe( catchError((err) => { return of(null); }), map((res) => { const mapped = res?.rows.map((r) => ({ ...r, dataKey: r.tipo + ' → ' + r.luogo, })) ?? []; // ⭐ ORDINAMENTO EURISTICO LOCALE const sorted = mapped.sort((a, b) => { const rankA = this.luoghiService.rankLuogo(a, event.query); const rankB = this.luoghiService.rankLuogo(b, event.query); if (rankA !== rankB) return rankA - rankB; return a.luogo!.localeCompare(b.luogo!); }); return { filteredLuoghiEsteso: sorted }; }), ); this.state.connect(fetchCities$); } private resetLocation(level: 'stato' | 'regione' | 'provincia') { if (level === 'stato') { this.cercaStruttureForm.get('regione')?.reset(); this.cercaStruttureForm.get('provincia')?.reset(); this.cercaStruttureForm.get('citta')?.reset(); } if (level === 'regione') { this.cercaStruttureForm.get('provincia')?.reset(); this.cercaStruttureForm.get('citta')?.reset(); } if (level === 'provincia') { this.cercaStruttureForm.get('citta')?.reset(); } } getStrutture() { this.state.set({ cercaStruttureFormSubmitted: true, }); if (this.cercaStruttureForm.invalid) { return; } const _form = this.cercaStruttureForm.value; this.cercaStruttureForm.disable({ emitEvent: false }); const params: StrutturePubblicheControllerFindManyStrutture$Params = { indirizzo: _form.indirizzo, tipoStruttura: _form.tipologiaStruttura, codiceStato: _form.stato!.codiceStato, regione: _form.regione?.regione, siglaProvincia: _form.provincia?.siglaProvincia, comune: _form.citta?.comune, }; this.calcolaIndirizzoDa = _form.indirizzo; const strutture$ = this.strutturePubblicheService.getStrutture(params).pipe( catchError((err) => { this.messageService.add({ severity: 'error', life: 5000, summary: 'Attenzione!', detail: err.status === 400 ? 'Il luogo è obbligatorio' : 'Impossibile recuperare la lista delle strutture in questo momento, riprova più tardi', }); return of([] as StrutturePubblicheResDto[]); }), tap((res) => { this.cercaStruttureForm.enable({ emitEvent: false }); if (!res) return; this.gMapMarkers(res, false); }), map((res) => ({ strutture: res })), startWith({ struttureAreLoading: true, isSearching: true }), endWith({ struttureAreLoading: false, isSearching: false }), ); this.state.connect(strutture$); } openInfoWindow(marker: MapMarker, index: number) { if (this.struttureConCoords.length === index) { // se è home icon (il push è stato fatto alla fine, quindi sarà l'ultimo nella lista) const contentStringHome = `

${this.indirizzoGmaps}

`; this.infoWindow.infoWindow?.setContent(contentStringHome); this.infoWindow.open(marker); } else { const info = this.struttureConCoords[index]; const contentString = `
${''.repeat( info.struttura.stelline.length, )}

${ info.struttura.nome }

${info.struttura.indirizzo}${ info.struttura.numeroCivico ? ', ' + info.struttura.numeroCivico : '' }

${info.struttura.cap} ${info.struttura.codiceLuogo.comune} ${ info.struttura.codiceLuogo.provincia ? '(' + info.struttura.codiceLuogo.siglaProvincia + ')' : '' }

`; this.infoWindow.infoWindow?.setHeaderDisabled(true); this.infoWindow.infoWindow?.setContent(contentString); this.infoWindow.open(marker); this.infoWindow.infoWindow?.addListener('domready', () => { const el = document.getElementById(`close_${info?.struttura.id}`); if (el) { el.addEventListener('click', () => { this.infoWindow.infoWindow?.close(); }); } }); } } gMapMarkers(items: StrutturePubblicheResDto[], filter: boolean) { // Filtra solo strutture con coordinate valide const itemsWithCoords = items.filter((x) => { const lat = this.parseCoord(x.struttura.latitudine); const lng = this.parseCoord(x.struttura.longitudine); return lat !== null && lng !== null; }); this.struttureConCoords = itemsWithCoords; if (!itemsWithCoords.length) { this.markerOptions = []; return; } // Calcola centro mappa const firstItem = itemsWithCoords[0]; const centerLat = this.parseCoord(firstItem.struttura.latitudine) ?? 45.472520078847595; const centerLng = this.parseCoord(firstItem.struttura.longitudine) ?? 9.194890732834459; this.options = { center: { lat: centerLat, lng: centerLng }, zoom: itemsWithCoords.length ? 13 : 11, // mapId: 'c18657fa9d1be788abd38482', }; // Marker strutture this.markerOptions = itemsWithCoords.map((x, i) => ({ position: { lat: this.parseCoord(x.struttura.latitudine)!, lng: this.parseCoord(x.struttura.longitudine)!, }, title: x.struttura.nome, draggable: false, icon: !filter && i < 6 ? 'https://maps.google.com/mapfiles/kml/paddle/blu-stars.png' : undefined, })); // Marker "home" if (firstItem.startingLocation) { this.markerOptions.push({ position: firstItem.startingLocation, title: this.indirizzoGmaps, icon: { url: 'https://maps.google.com/mapfiles/kml/pal3/icon56.png', scaledSize: new google.maps.Size(50, 50), }, }); } // Crea i marker cluster dopo che la mappa è pronta // setTimeout(() => { // if (!this.map?.googleMap) return; // const markers = this.markerOptions // .filter((opt) => !!opt.position) // .map((opt) => { // return new google.maps.marker.AdvancedMarkerElement({ // position: opt.position!, // title: opt.title, // map: this.map.googleMap!, // }); // }); // new MarkerClusterer({ // map: this.map.googleMap, // markers, // }); // }); } private parseCoord(value?: string | null): number | null { if (!value) return null; const n = Number(value.replace(',', '.')); return Number.isFinite(n) ? n : null; } externalLink(url: string) { window.open('//' + url, '_blank'); } applyFilterGlobal($event: Event, stringVal: string) { this.struttureTable.filterGlobal( ($event.target as HTMLInputElement).value, stringVal, ); } calculateMapsLink(struttura: StrutturePubblicheResDto, open = true): string { // https://www.google.com/maps/dir/?api=1&origin=45.476333943968626,9.17169124076061&destination=45.4692422,9.16562&travelmode=driving // https://www.google.com/maps/dir/?api=1&destination=45.4692422,9.16562&travelmode=driving const baseUrl = 'https://www.google.com/maps/dir/?api=1'; const travelMode = 'driving'; const destinationMode: 'latlng' | 'nameAddress' = 'nameAddress'; let url = ((destinationMode: 'latlng' | 'nameAddress') => { switch (destinationMode) { case 'nameAddress': { let address = `${struttura.struttura.indirizzo}${ struttura.struttura.numeroCivico ? ', ' + struttura.struttura.numeroCivico : '' }`; address += ` ${struttura.struttura.cap} ${struttura.struttura.codiceLuogo.comune}(${struttura.struttura.codiceLuogo.siglaProvincia})`; // address += `${struttura.struttura.codiceLuogo.provincia ? '(' + struttura.struttura.codiceLuogo.siglaProvincia + ')' : ''}`; const destination = encodeURIComponent( `${struttura.struttura.nome}, ${address}`, ); return `${baseUrl}&destination=${destination}`; } case 'latlng': return `${baseUrl}&destination=${struttura.struttura.latitudine},${struttura.struttura.longitudine}`; } })(destinationMode); if (this.calcolaIndirizzoDa) { url = `${url}&origin=${this.calcolaIndirizzoDa}`; } const luogo = this.cercaStruttureForm.controls.citta.value; if (luogo) { url = `${url}, ${luogo.comune}`; } url = `${url}&travelmode=${travelMode}`; if (open) window.open(url, '_blank'); return url; } // checkLuogo() { // //Hack: rimosso dall'autocomplete forceSelection perchè non funzionava come previsto, selezionando ad esempio "Milano" (comune), selezionava "Milano" (comune). // const luogo = this.cercaStruttureForm.controls['luogo'].value; // if (typeof luogo === 'string') { // this.cercaStruttureForm.controls['luogo'].setValue(null); // } // } }