switch to bootstrap italia through AI

This commit is contained in:
2026-07-17 09:53:46 +02:00
parent 99a64f1157
commit 4cd22396d9
16 changed files with 345 additions and 672 deletions
@@ -1,12 +1,10 @@
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;
@@ -14,7 +12,6 @@ interface ContrattoResult {
idRicevuta: string;
}
/** State aggiuntivo per gestire l'autocomplete */
interface RecuperoPropostaState {
searchText: string;
searchResults: ContrattoResult[];
@@ -26,7 +23,6 @@ interface RecuperoPropostaState {
}
export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsComponentAbstract {
/** Stato locale per l'autocomplete (separato dal formData gestito dall'abstract) */
private autocompleteState: RecuperoPropostaState = {
searchText: '',
searchResults: [],
@@ -37,20 +33,17 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
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',
idContratto: 'COL0010',
titoloContratto: 'COL0020',
idDomanda: 'COL0030',
idRicevuta: 'COL0040',
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) {
@@ -59,7 +52,6 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
}
override componentDidMount(): void {
// Listener per chiudere il dropdown al click fuori dall'autocomplete
document.addEventListener('mousedown', this.handleClickOutside);
}
@@ -71,19 +63,12 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
}
private handleClickOutside = (event: MouseEvent): void => {
if (
this.autocompleteRef.current &&
!this.autocompleteRef.current.contains(event.target as Node)
) {
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 = '';
@@ -99,9 +84,9 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Authorization': `Basic ${credentials}`,
Authorization: `Basic ${credentials}`,
'X-Api-Key': config.apiKey,
'Accept': 'application/json',
Accept: 'application/json',
},
});
@@ -121,21 +106,15 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
this.autocompleteState.searchResults = [];
this.autocompleteState.isDropdownOpen = false;
this.autocompleteState.isLoading = false;
this.autocompleteState.errorMessage =
error instanceof Error ? error.message : 'Errore durante la ricerca';
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 ?? '';
private handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const text = event.target.value;
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;
@@ -146,7 +125,6 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
return;
}
// Debounce della ricerca
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
@@ -163,10 +141,7 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
}
};
/**
* Gestisce la navigazione da tastiera nel dropdown (frecce, Invio, Escape).
*/
private handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
private handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
const { searchResults, isDropdownOpen, highlightedIndex } = this.autocompleteState;
if (!isDropdownOpen || searchResults.length === 0) return;
@@ -174,10 +149,7 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
this.autocompleteState.highlightedIndex = Math.min(
highlightedIndex + 1,
searchResults.length - 1
);
this.autocompleteState.highlightedIndex = Math.min(highlightedIndex + 1, searchResults.length - 1);
this.forceUpdate();
break;
@@ -202,9 +174,6 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
}
};
/**
* 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}`;
@@ -212,7 +181,6 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
this.autocompleteState.searchResults = [];
this.autocompleteState.highlightedIndex = -1;
// Aggiorna il formData con i valori selezionati
this.setState({
formData: {
...this.state.formData,
@@ -221,15 +189,9 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
[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;
@@ -244,42 +206,41 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
});
}
/**
* 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
<div className="position-relative" ref={this.autocompleteRef}>
<input
id="autocomplete-search"
name="autocomplete-search"
className="form-control"
type="text"
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 className="autocompleteDropdown mt-2 p-3 text-muted">
<span className="me-2">
<span className="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
</span>
Ricerca in corso...
</div>
)}
{!isLoading && isDropdownOpen && searchResults.length > 0 && (
<div className="autocomplete-dropdown">
<div className="autocompleteDropdown mt-2">
{searchResults.map((contract, index) => (
<div
key={`${contract.idContratto}_${index}`}
className={`autocomplete-dropdown-item${index === highlightedIndex ? ' highlighted' : ''}`}
onMouseDown={(e) => {
e.preventDefault(); // Previene blur del TextField
className={`autocompleteItem${index === highlightedIndex ? ' isHighlighted' : ''}`}
onMouseDown={(event) => {
event.preventDefault();
this.selectContract(contract);
}}
onMouseEnter={() => {
@@ -287,41 +248,31 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
this.forceUpdate();
}}
>
<span className="contract-id">[{contract.idContratto}]</span>{' '}
<span className="contract-title">{contract.titoloContratto}</span>
<span className="fw-semibold">[{contract.idContratto}]</span>{' '}
<span>{contract.titoloContratto}</span>
</div>
))}
</div>
)}
{!isLoading && isDropdownOpen && searchResults.length === 0 && (
<div className="autocomplete-dropdown">
<div className="autocomplete-no-results">Nessun risultato trovato</div>
</div>
<div className="autocompleteDropdown mt-2 p-3 text-muted">Nessun risultato trovato</div>
)}
{errorMessage && (
<div className="autocomplete-error">{errorMessage}</div>
<div className="alert alert-danger mt-2">{errorMessage}</div>
)}
</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 className="card mt-3">
<div className="card-body">
<div className="row g-2">
<div className="col-12 col-md-6"><strong>ID Contratto:</strong> {this.autocompleteState.selectedContract.idContratto}</div>
<div className="col-12 col-md-6"><strong>Titolo:</strong> {this.autocompleteState.selectedContract.titoloContratto}</div>
<div className="col-12 col-md-6"><strong>ID Domanda:</strong> {this.autocompleteState.selectedContract.idDomanda}</div>
<div className="col-12 col-md-6"><strong>ID Ricevuta:</strong> {this.autocompleteState.selectedContract.idRicevuta}</div>
</div>
</div>
</div>
)}
@@ -329,16 +280,11 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
);
return new ElixFormsElement(
<FluentUI.Label htmlFor="autocomplete-search">Cerca Contratto</FluentUI.Label>,
<label className="form-label fw-semibold" htmlFor="autocomplete-search">Cerca Contratto</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] ?? '';
@@ -349,8 +295,6 @@ export default class RecuperoPropostaCctDaContrattiComponent extends ElixFormsCo
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} />