361 lines
13 KiB
TypeScript
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} />
|
|
</>
|
|
);
|
|
}
|
|
}
|