Files
elixforms-web-pages/react/recupero-proposta-cct-da-contratti/src/RecuperoPropostaCctDaContrattiComponent.tsx
T

361 lines
13 KiB
TypeScript

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<typeof setTimeout> | null = null;
/** Ref al container per gestire il click-outside */
private autocompleteRef: React.RefObject<HTMLDivElement | null> = 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<void> {
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<HTMLInputElement | HTMLTextAreaElement>, 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<HTMLInputElement | HTMLTextAreaElement>): 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 = (
<div className="autocomplete-wrapper" ref={this.autocompleteRef}>
<FluentUI.TextField
id="autocomplete-search"
name="autocomplete-search"
placeholder="Digita almeno 3 caratteri per cercare..."
value={searchText}
onChange={this.handleSearchChange}
onKeyDown={this.handleKeyDown}
className="isiportalPartialAdminFormFieldSingleLineText"
autoComplete="off"
/>
{isLoading && (
<div className="autocomplete-dropdown">
<div className="autocomplete-loading">
<FluentUI.Spinner size={FluentUI.SpinnerSize.small} label="Ricerca in corso..." />
</div>
</div>
)}
{!isLoading && isDropdownOpen && searchResults.length > 0 && (
<div className="autocomplete-dropdown">
{searchResults.map((contract, index) => (
<div
key={`${contract.idContratto}_${index}`}
className={`autocomplete-dropdown-item${index === highlightedIndex ? ' highlighted' : ''}`}
onMouseDown={(e) => {
e.preventDefault(); // Previene blur del TextField
this.selectContract(contract);
}}
onMouseEnter={() => {
this.autocompleteState.highlightedIndex = index;
this.forceUpdate();
}}
>
<span className="contract-id">[{contract.idContratto}]</span>{' '}
<span className="contract-title">{contract.titoloContratto}</span>
</div>
))}
</div>
)}
{!isLoading && isDropdownOpen && searchResults.length === 0 && (
<div className="autocomplete-dropdown">
<div className="autocomplete-no-results">Nessun risultato trovato</div>
</div>
)}
{errorMessage && (
<div className="autocomplete-error">{errorMessage}</div>
)}
{this.autocompleteState.selectedContract && (
<div className="selected-contract-summary">
<div className="summary-row">
<span className="summary-label">ID Contratto:</span>
<span className="summary-value">{this.autocompleteState.selectedContract.idContratto}</span>
</div>
<div className="summary-row">
<span className="summary-label">Titolo:</span>
<span className="summary-value">{this.autocompleteState.selectedContract.titoloContratto}</span>
</div>
<div className="summary-row">
<span className="summary-label">ID Domanda:</span>
<span className="summary-value">{this.autocompleteState.selectedContract.idDomanda}</span>
</div>
<div className="summary-row">
<span className="summary-label">ID Ricevuta:</span>
<span className="summary-value">{this.autocompleteState.selectedContract.idRicevuta}</span>
</div>
</div>
)}
</div>
);
return new ElixFormsElement(
<FluentUI.Label htmlFor="autocomplete-search">Cerca Contratto</FluentUI.Label>,
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 */}
<input type="hidden" id="idContratto_hidden" name={fieldKeys.idContratto} value={idContratto} />
<input type="hidden" id="titoloContratto_hidden" name={fieldKeys.titoloContratto} value={titoloContratto} />
<input type="hidden" id="idDomanda_hidden" name={fieldKeys.idDomanda} value={idDomanda} />
<input type="hidden" id="idRicevuta_hidden" name={fieldKeys.idRicevuta} value={idRicevuta} />
</>
);
}
}