import { CommonModule } from '@angular/common'; import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators, } from '@angular/forms'; import { Subject, Subscription } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, filter, switchMap, } from 'rxjs/operators'; import { of } from 'rxjs'; import { BAN_LIMITE_SUGGESTIONS, BAN_MIN_CARACTERES, BANService, } from '../../core/services/ban.service'; import { AdresseNommeeService } from '../../core/services/adresse-nommee.service'; import { NotificationService } from '../../core/services/notification.service'; import { AdresseNommee, BANFeature, SaveAdresseNommee } from '../../core/models'; /** Délai d'inactivité avant interrogation de la BAN (Exigence 15.3). */ const BAN_DEBOUNCE_MS = 300; /** * Carnet d'adresses nommées (Exigence 15). * * Liste triée par dénomination, formulaire de création / modification avec * autocomplétion BAN (≥ 3 car., debounce 300 ms, max 5 suggestions), * suppression avec confirmation, messages de confirmation via NotificationService. */ @Component({ selector: 'app-carnet-adresses', standalone: true, imports: [CommonModule, ReactiveFormsModule], templateUrl: './carnet-adresses.component.html', styleUrls: ['./carnet-adresses.component.scss'], }) export class CarnetAdressesComponent implements OnInit, OnDestroy { private readonly fb = inject(FormBuilder); private readonly service = inject(AdresseNommeeService); private readonly banService = inject(BANService); private readonly notifications = inject(NotificationService); /** Liste des adresses nommées, triée par dénomination. */ readonly adresses = signal([]); /** Chargement de la liste en cours. */ readonly chargement = signal(false); /** Enregistrement (create/update) en cours. */ readonly enregistrement = signal(false); /** Identifiant de l'adresse en cours d'édition (null = création). */ readonly editionId = signal(null); /** Ouverture de la popup de création / modification. */ readonly modaleOuverte = signal(false); /** Suggestions BAN courantes pour le champ adresse. */ readonly suggestions = signal([]); /** Coordonnées associées à l'adresse saisie (issues d'une sélection BAN). */ private latitude: number | null = null; private longitude: number | null = null; readonly form: FormGroup = this.fb.group({ denomination: ['', [Validators.required, Validators.maxLength(100)]], adresse: ['', [Validators.required, Validators.maxLength(255)]], }); private readonly adresseInput$ = new Subject(); private readonly subscriptions = new Subscription(); ngOnInit(): void { this.chargerAdresses(); this.configurerAutocompletion(); } ngOnDestroy(): void { this.subscriptions.unsubscribe(); } /** Recharge la liste des adresses nommées (triée par dénomination). */ chargerAdresses(): void { this.chargement.set(true); this.subscriptions.add( this.service .getAdressesNommees() .pipe( catchError(() => { this.notifications.error( "Impossible de charger le carnet d'adresses.", ); return of([] as AdresseNommee[]); }), ) .subscribe((adresses) => { this.adresses.set( [...adresses].sort((a, b) => a.denomination.localeCompare(b.denomination, 'fr', { sensitivity: 'base', }), ), ); this.chargement.set(false); }), ); } /** Indique si l'adresse nommée a des coordonnées géocodées. */ aCoordonnees(adresse: AdresseNommee): boolean { return adresse.latitude != null && adresse.longitude != null; } /** true en mode édition, false en mode création. */ get enEdition(): boolean { return this.editionId() != null; } /** Passe le formulaire en mode édition, pré-rempli avec l'adresse choisie. */ editer(adresse: AdresseNommee): void { this.editionId.set(adresse.id); this.latitude = adresse.latitude; this.longitude = adresse.longitude; this.suggestions.set([]); this.form.setValue({ denomination: adresse.denomination, adresse: adresse.adresse, }); this.modaleOuverte.set(true); } /** Ouvre la popup en mode création (formulaire vierge). */ ouvrirCreation(): void { this.editionId.set(null); this.latitude = null; this.longitude = null; this.suggestions.set([]); this.form.reset({ denomination: '', adresse: '' }); this.modaleOuverte.set(true); } /** Ferme la popup uniquement lors d'un clic sur le fond (pas sur le panneau). */ fermerSurOverlay(event: MouseEvent): void { if (event.target === event.currentTarget) { this.annuler(); } } /** Réinitialise le formulaire en mode création. */ annuler(): void { this.editionId.set(null); this.latitude = null; this.longitude = null; this.suggestions.set([]); this.form.reset({ denomination: '', adresse: '' }); this.modaleOuverte.set(false); } /** Réagit à la saisie du champ adresse pour alimenter l'autocomplétion BAN. */ onAdresseInput(valeur: string): void { // Toute saisie manuelle invalide les coordonnées d'une sélection précédente. this.latitude = null; this.longitude = null; this.adresseInput$.next(valeur ?? ''); } /** Applique une suggestion BAN : remplit le champ et conserve les coordonnées. */ choisirSuggestion(feature: BANFeature): void { const [lon, lat] = feature.geometry.coordinates; this.longitude = lon; this.latitude = lat; this.form.get('adresse')?.setValue(feature.properties.label); this.suggestions.set([]); } /** Crée ou met à jour l'adresse nommée. */ enregistrer(): void { if (this.form.invalid || this.enregistrement()) { this.form.markAllAsTouched(); return; } const dto: SaveAdresseNommee = { denomination: (this.form.value.denomination ?? '').trim(), adresse: (this.form.value.adresse ?? '').trim(), latitude: this.latitude, longitude: this.longitude, }; this.enregistrement.set(true); const editionId = this.editionId(); const requete$ = editionId != null ? this.service.update(editionId, dto) : this.service.create(dto); const enCreation = editionId == null; this.subscriptions.add( requete$.subscribe({ next: (adresse) => { this.enregistrement.set(false); this.notifications.success( enCreation ? `Adresse « ${adresse.denomination} » créée.` : `Adresse « ${adresse.denomination} » modifiée.`, ); if (!this.aCoordonnees(adresse)) { this.notifications.warning( `Les coordonnées de « ${adresse.denomination} » n'ont pas pu être obtenues (géocodage indisponible).`, ); } this.annuler(); this.chargerAdresses(); }, error: () => { this.enregistrement.set(false); this.notifications.error( enCreation ? "La création de l'adresse a échoué." : "La modification de l'adresse a échoué.", ); }, }), ); } /** Supprime une adresse nommée après confirmation. */ supprimer(adresse: AdresseNommee): void { const confirme = window.confirm( `Supprimer l'adresse « ${adresse.denomination} » du carnet ?`, ); if (!confirme) { return; } this.subscriptions.add( this.service.delete(adresse.id).subscribe({ next: () => { this.notifications.success( `Adresse « ${adresse.denomination} » supprimée.`, ); if (this.editionId() === adresse.id) { this.annuler(); } this.chargerAdresses(); }, error: () => { this.notifications.error("La suppression de l'adresse a échoué."); }, }), ); } private configurerAutocompletion(): void { this.subscriptions.add( this.adresseInput$ .pipe( debounceTime(BAN_DEBOUNCE_MS), distinctUntilChanged(), filter((terme) => (terme ?? '').trim().length >= BAN_MIN_CARACTERES), switchMap((terme) => this.banService.searchAdresses(terme).pipe(catchError(() => of([]))), ), ) .subscribe((features) => { this.suggestions.set(features.slice(0, BAN_LIMITE_SUGGESTIONS)); }), ); } }