import React, { type JSX } from 'react'; import * as FluentUI from '@fluentui/react'; import ElixFormsComponentAbstract from '@common/src/ElixFormsComponentAbstract'; import type { IElixFormsComponentCustomFormFieldFactory } from '@common/src/IElixFormsComponentCustomFormFieldFactory'; import { ElixFormsElement } from '@common/src/ElixFormsElement'; import { QueryParamHelper } from '@common/src/QueryParamHelper'; import config from './config.json'; /** Singolo risultato restituito dall'API esterna */ interface ContrattoResult { idContratto: string; titoloContratto: string; idDomanda: string; idRicevuta: string; } /** State aggiuntivo per gestire l'autocomplete */ interface RecuperoPropostaState { searchText: string; searchResults: ContrattoResult[]; isLoading: boolean; isDropdownOpen: boolean; selectedContract: ContrattoResult | null; highlightedIndex: number; errorMessage: string; } export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsComponentAbstract { /** Stato locale per l'autocomplete (separato dal formData gestito dall'abstract) */ private autocompleteState: RecuperoPropostaState = { searchText: '', searchResults: [], isLoading: false, isDropdownOpen: false, selectedContract: null, highlightedIndex: -1, errorMessage: '', }; /** Timer per il debounce della ricerca */ private debounceTimer: ReturnType | null = null; /** Ref al container per gestire il click-outside */ private autocompleteRef: React.RefObject = React.createRef(); private static readonly formFieldKeys = { idContratto: 'COL0010', titoloContratto: 'COL0020', idDomanda: 'COL0030', idRicevuta: 'COL0040', } as const; /** Codice fiscale letto dalla query string (COL0009), usato solo per la chiamata API */ private codiceFiscale: string; constructor(props: any) { super(props); this.codiceFiscale = QueryParamHelper.getDecodedTextFromQuery('COL0009') ?? ''; } override componentDidMount(): void { // Listener per chiudere il dropdown al click fuori dall'autocomplete document.addEventListener('mousedown', this.handleClickOutside); } override componentWillUnmount(): void { document.removeEventListener('mousedown', this.handleClickOutside); if (this.debounceTimer) { clearTimeout(this.debounceTimer); } } private handleClickOutside = (event: MouseEvent): void => { if ( this.autocompleteRef.current && !this.autocompleteRef.current.contains(event.target as Node) ) { this.autocompleteState.isDropdownOpen = false; this.forceUpdate(); } }; /** * Chiama l'API esterna per ottenere i contratti corrispondenti al testo di ricerca. * Autenticazione: Basic Auth (username:password) + header X-Api-Key. */ private async fetchContratti(searchTerm: string): Promise { this.autocompleteState.isLoading = true; this.autocompleteState.errorMessage = ''; this.forceUpdate(); try { const url = new URL(config.apiUrl); url.searchParams.set('_where', `{ "or": [ { "idContratto": { "contains": "${searchTerm}" } }, { "titoloContratto": { "contains": "${searchTerm}" } } ] }` ); //url.searchParams.set('cod_fis', this.codiceFiscale); //url.searchParams.set('term', searchTerm); const credentials = btoa(`${config.apiUsername}:${config.apiPassword}`); const response = await fetch(url.toString(), { method: 'GET', headers: { 'Authorization': `Basic ${credentials}`, 'X-Api-Key': config.apiKey, 'Accept': 'application/json', }, }); if (!response.ok) { throw new Error(`Errore API: ${response.status} ${response.statusText}`); } const data: ContrattoResult[] = await response.json(); this.autocompleteState.searchResults = data; this.autocompleteState.isDropdownOpen = data.length > 0; this.autocompleteState.highlightedIndex = -1; this.autocompleteState.isLoading = false; this.forceUpdate(); } catch (error) { console.error('Errore durante la ricerca contratti:', error); this.autocompleteState.searchResults = []; this.autocompleteState.isDropdownOpen = false; this.autocompleteState.isLoading = false; this.autocompleteState.errorMessage = error instanceof Error ? error.message : 'Errore durante la ricerca'; this.forceUpdate(); } } /** * Gestisce il cambio testo nell'input di ricerca. * Applica debounce e chiama l'API al raggiungimento del minimo di caratteri. */ private handleSearchChange = (_event: React.FormEvent, newValue?: string): void => { const text = newValue ?? ''; this.autocompleteState.searchText = text; // Se l'utente cancella il testo, resetta anche la selezione if (text.length === 0) { this.autocompleteState.searchResults = []; this.autocompleteState.isDropdownOpen = false; this.autocompleteState.highlightedIndex = -1; this.autocompleteState.errorMessage = ''; this.clearSelection(); this.forceUpdate(); return; } // Debounce della ricerca if (this.debounceTimer) { clearTimeout(this.debounceTimer); } if (text.length >= config.minCharsForSearch) { this.debounceTimer = setTimeout(() => { this.fetchContratti(text); }, 300); } else { this.autocompleteState.searchResults = []; this.autocompleteState.isDropdownOpen = false; this.autocompleteState.highlightedIndex = -1; this.forceUpdate(); } }; /** * Gestisce la navigazione da tastiera nel dropdown (frecce, Invio, Escape). */ private handleKeyDown = (event: React.KeyboardEvent): void => { const { searchResults, isDropdownOpen, highlightedIndex } = this.autocompleteState; if (!isDropdownOpen || searchResults.length === 0) return; switch (event.key) { case 'ArrowDown': event.preventDefault(); this.autocompleteState.highlightedIndex = Math.min( highlightedIndex + 1, searchResults.length - 1 ); this.forceUpdate(); break; case 'ArrowUp': event.preventDefault(); this.autocompleteState.highlightedIndex = Math.max(highlightedIndex - 1, 0); this.forceUpdate(); break; case 'Enter': event.preventDefault(); if (highlightedIndex >= 0 && highlightedIndex < searchResults.length) { this.selectContract(searchResults[highlightedIndex]); } break; case 'Escape': event.preventDefault(); this.autocompleteState.isDropdownOpen = false; this.forceUpdate(); break; } }; /** * Seleziona un contratto dal dropdown e popola i campi hidden del form. */ private selectContract(contract: ContrattoResult): void { this.autocompleteState.selectedContract = contract; this.autocompleteState.searchText = `[${contract.idContratto}] ${contract.titoloContratto}`; this.autocompleteState.isDropdownOpen = false; this.autocompleteState.searchResults = []; this.autocompleteState.highlightedIndex = -1; // Aggiorna il formData con i valori selezionati this.setState({ formData: { ...this.state.formData, [RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idContratto]: contract.idContratto, [RecuperoPropostaCctDaContrattiComponent.formFieldKeys.titoloContratto]: contract.titoloContratto, [RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idDomanda]: contract.idDomanda, [RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idRicevuta]: contract.idRicevuta, }, }, () => { console.log('Contratto selezionato:', contract); console.log('Form data aggiornato:', this.state.formData); }); } /** * Resetta la selezione e svuota i campi hidden. */ private clearSelection(): void { this.autocompleteState.selectedContract = null; this.setState({ formData: { ...this.state.formData, [RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idContratto]: '', [RecuperoPropostaCctDaContrattiComponent.formFieldKeys.titoloContratto]: '', [RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idDomanda]: '', [RecuperoPropostaCctDaContrattiComponent.formFieldKeys.idRicevuta]: '', }, }); } /** * Renderizza il componente autocomplete completo (label + input + dropdown). */ private renderAutocomplete(): JSX.Element { const { searchResults, isLoading, isDropdownOpen, highlightedIndex, errorMessage, searchText } = this.autocompleteState; const autocompleteInput = (
{isLoading && (
)} {!isLoading && isDropdownOpen && searchResults.length > 0 && (
{searchResults.map((contract, index) => (
{ e.preventDefault(); // Previene blur del TextField this.selectContract(contract); }} onMouseEnter={() => { this.autocompleteState.highlightedIndex = index; this.forceUpdate(); }} > [{contract.idContratto}]{' '} {contract.titoloContratto}
))}
)} {!isLoading && isDropdownOpen && searchResults.length === 0 && (
Nessun risultato trovato
)} {errorMessage && (
{errorMessage}
)} {this.autocompleteState.selectedContract && (
ID Contratto: {this.autocompleteState.selectedContract.idContratto}
Titolo: {this.autocompleteState.selectedContract.titoloContratto}
ID Domanda: {this.autocompleteState.selectedContract.idDomanda}
ID Ricevuta: {this.autocompleteState.selectedContract.idRicevuta}
)}
); return new ElixFormsElement( Cerca Contratto, autocompleteInput ).render(); } /** * Override del metodo factory per definire i campi custom del form. * - Autocomplete per la ricerca contratti * - 4 campi hidden (COL0010, COL0020, COL0030, COL0040) per i dati da rimandare a elixForms */ protected override createCustomFormFields(formFieldFactory: IElixFormsComponentCustomFormFieldFactory): JSX.Element { const fieldKeys = RecuperoPropostaCctDaContrattiComponent.formFieldKeys; const idContratto = this.state.formData[fieldKeys.idContratto] ?? ''; const titoloContratto = this.state.formData[fieldKeys.titoloContratto] ?? ''; const idDomanda = this.state.formData[fieldKeys.idDomanda] ?? ''; const idRicevuta = this.state.formData[fieldKeys.idRicevuta] ?? ''; return ( <> {this.renderAutocomplete()} {/* Campi hidden per i dati del contratto selezionato — verranno inviati nel POST a elixForms */} ); } }