Browser-Strutture-Musa/src/app/modules/public/strutture-pubbliche/strutture-pubbliche.component.ts

626 lines
20 KiB
TypeScript

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<string | null>(null),
stato: new FormControl<StatoRes | null>(null, {
validators: [Validators.required],
}),
regione: new FormControl<RegioneRes | null>(null, {
validators: [Validators.required],
}),
provincia: new FormControl<ProvinciaRes | null>(null),
citta: new FormControl<CittaRes | null>(null),
indirizzo: new FormControl<string | null>(null),
});
};
export type cercaStruttureForm = ReturnType<typeof cercaStruttureFormGroupFunc>;
export type cercaStruttureFormValue = cercaStruttureForm['value'];
export interface CercaStruttureComponentState {
strutture: StrutturePubblicheResDto[];
tipologieStrutture: TipoStruttura[];
stati: Array<StatoRes>;
regioni: Array<RegioneRes>;
province: Array<ProvinciaRes>;
citta: Array<CittaRes>;
filteredLuoghiEsteso: Array<LuogoRes & { dataKey: string }>;
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<CercaStruttureComponentState>;
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<State>,
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 = `<div>
<h1 style="margin-top: 0px; margin-bottom: 0px; font-size:16px;">${this.indirizzoGmaps}</h1>
</div>`;
this.infoWindow.infoWindow?.setContent(contentStringHome);
this.infoWindow.open(marker);
} else {
const info = this.struttureConCoords[index];
const contentString = `
<div>
<i class="hover:text-gray-500 absolute top-[3px] right-[3px] pi pi-times" id="close_${info.struttura.id}"></i>
<div>
<span class="text-primary" style="font-size: 20px;font-weight: bold;">${'<i class="text-primary pi pi-star-fill"></i>'.repeat(
info.struttura.stelline.length,
)}</span>
<h1 class="text-primary" style="margin-top: 0px; margin-bottom: 0px; font-size:16px;">${
info.struttura.nome
}</h1>
</div>
<div>
<p class="m-t-0 m-b-0"">
${info.struttura.indirizzo}${
info.struttura.numeroCivico
? ', ' + info.struttura.numeroCivico
: ''
}
</p>
<p class="m-t-0 m-b-0"">
${info.struttura.cap} ${info.struttura.codiceLuogo.comune} ${
info.struttura.codiceLuogo.provincia
? '(' + info.struttura.codiceLuogo.siglaProvincia + ')'
: ''
}
</p>
</div>
<div class="mt-2">
<a class="hover:underline text-primary-700" target="_blank" href="${this.calculateMapsLink(info, false)}"><i class="pi pi-map-marker"></i>Apri su Google Maps</a>
</div>
</div>
`;
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);
// }
// }
}