2026-02-27 23:26:13 +01:00
import ' package:flutter/material.dart ' ;
import ' package:flutter/services.dart ' ;
import ' package:shared_preferences/shared_preferences.dart ' ;
import ' comp_8.dart ' ;
import ' global_data.dart ' ;
2026-04-28 16:00:03 +02:00
import ' services/profilo_service.dart ' ;
2026-02-27 23:26:13 +01:00
// Formattatore per forzare il maiuscolo mentre si scrive (Versione sicura per iOS/Android)
class UpperCaseTextFormatter extends TextInputFormatter {
@ override
TextEditingValue formatEditUpdate ( TextEditingValue oldValue , TextEditingValue newValue ) {
return TextEditingValue (
text: newValue . text . toUpperCase ( ) ,
selection: newValue . selection ,
composing: newValue . composing , // Mantiene intatta la memoria della tastiera!
) ;
}
}
class Comp6_7Screen extends StatefulWidget {
const Comp6_7Screen ( { super . key } ) ;
@ override
_Comp6_7ScreenState createState ( ) = > _Comp6_7ScreenState ( ) ;
}
class _Comp6_7ScreenState extends State < Comp6_7Screen > {
2026-04-28 20:20:45 +02:00
late TextEditingController _cognome , _nome , _cf , _indirizzo , _cap , _stato , _tel , _email ;
2026-02-27 23:26:13 +01:00
late TextEditingController _marca , _rimorchio , _targa , _statoImm , _statoImm2 ;
// Il tuo FocusNode originale
late FocusNode _cfFocusNode ;
final bool isB = GlobalData . latoCorrente = = ' B ' ;
bool _isReady = false ;
@ override
void initState ( ) {
super . initState ( ) ;
// Ripristino del tuo listener originale sull'uscita dal campo
_cfFocusNode = FocusNode ( ) ;
_cfFocusNode . addListener ( _onCfFocusChange ) ;
_forzaVerticaleEInizializza ( ) ;
}
// La tua funzione originale per l'uscita dal campo (Aggiornata per P.IVA)
void _onCfFocusChange ( ) {
if ( ! _cfFocusNode . hasFocus ) {
String cfInserito = _cf . text . trim ( ) . toUpperCase ( ) ;
if ( cfInserito . isNotEmpty ) {
bool isPIVA = RegExp ( r"^[0-9]{11}$" ) . hasMatch ( cfInserito ) ;
if ( ! isPIVA & & ! _isCodiceFiscaleValido ( cfInserito ) ) {
ScaffoldMessenger . of ( context ) . hideCurrentSnackBar ( ) ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( " Cod. Fiscale / P. IVA NON VALIDO (controlla i caratteri inseriti!). " ) ,
backgroundColor: Colors . orange ,
duration: Duration ( seconds: 3 ) ,
)
) ;
}
}
}
}
// La tua funzione originale per il controllo in tempo reale
void _controllaCfInTempoReale ( String value ) {
String cfScritto = value . trim ( ) . toUpperCase ( ) ;
if ( cfScritto . length = = 16 ) {
if ( ! _isCodiceFiscaleValido ( cfScritto ) ) {
// NON TOGLIAMO IL FOCUS QUI! Così l'incolla e l'autofill non si rompono.
ScaffoldMessenger . of ( context ) . hideCurrentSnackBar ( ) ;
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( " ⚠️ Codice Fiscale NON VALIDO (controlla i caratteri inseriti!). " ) ,
backgroundColor: Colors . orange ,
duration: Duration ( seconds: 3 ) ,
)
) ;
}
}
}
Future < void > _forzaVerticaleEInizializza ( ) async {
await SystemChrome . setPreferredOrientations ( [ DeviceOrientation . portraitUp ] ) ;
await Future . delayed ( const Duration ( milliseconds: 200 ) ) ;
_initControllers ( ) ;
if ( ! mounted ) return ;
setState ( ( ) = > _isReady = true ) ;
// Aspettiamo che il frame (la UI visiva) sia completamente disegnato per gestire i popup
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
_gestisciCatenaPopup ( ) ;
} ) ;
}
// --- SINCRONIZZAZIONE DEI POPUP ---
Future < void > _gestisciCatenaPopup ( ) async {
2026-04-28 16:00:03 +02:00
bool esiste = await ProfiloService . esisteProfilo ( ) ;
2026-02-27 23:26:13 +01:00
// 1. POPUP AUTOCOMPLETAMENTO LOCALE (se ci sono dati e i campi sono vuoti)
2026-04-28 16:00:03 +02:00
if ( esiste & & _cognome . text . trim ( ) . isEmpty ) {
await _mostraDialogoAutocompletamento ( ) ;
2026-02-27 23:26:13 +01:00
} else {
2026-04-28 16:00:03 +02:00
// 2. POPUP INFORMATIVO STANDARD (Appare sempre DOPO)
if ( mounted ) {
_mostraInfoPopup ( context ) ;
}
2026-02-27 23:26:13 +01:00
}
}
void _initControllers ( ) {
String pulisciBarre ( String valore , String patternBarre ) {
if ( valore . contains ( " / " ) ) return " " ;
return valore ;
}
if ( isB ) {
2026-05-02 12:00:02 +02:00
_cognome = TextEditingController ( text: GlobalData . cognomeContraenteB ) ;
_nome = TextEditingController ( text: GlobalData . nomeContraenteB ) ;
_cf = TextEditingController ( text: GlobalData . codiceFiscaleContraenteB ) ;
_indirizzo = TextEditingController ( text: GlobalData . indirizzoContraenteB ) ;
_cap = TextEditingController ( text: GlobalData . capContraenteB ) ;
_stato = TextEditingController ( text: GlobalData . statoContraenteB . isEmpty ? " ITALIA " : GlobalData . statoContraenteB ) ;
_tel = TextEditingController ( text: GlobalData . nTelefonoMailContraenteB ) ;
_email = TextEditingController ( text: GlobalData . emailContraenteB ) ;
_marca = TextEditingController ( text: GlobalData . marcaETipoB ) ;
_targa = TextEditingController ( text: GlobalData . targaB ) ;
_statoImm = TextEditingController ( text: GlobalData . statoImmatricolazioneB . isEmpty ? " ITALIA " : GlobalData . statoImmatricolazioneB ) ;
_rimorchio = TextEditingController ( text: pulisciBarre ( GlobalData . rimorchioB , " / " ) ) ;
_statoImm2 = TextEditingController ( text: pulisciBarre ( GlobalData . statoImmatricolazione2B , " / " ) ) ;
2026-02-27 23:26:13 +01:00
} else {
2026-05-02 12:00:02 +02:00
_cognome = TextEditingController ( text: GlobalData . cognomeContraenteA ) ;
_nome = TextEditingController ( text: GlobalData . nomeContraenteA ) ;
_cf = TextEditingController ( text: GlobalData . codiceFiscaleContraenteA ) ;
_indirizzo = TextEditingController ( text: GlobalData . indirizzoContraenteA ) ;
_cap = TextEditingController ( text: GlobalData . capContraenteA ) ;
_stato = TextEditingController ( text: GlobalData . statoContraenteA . isEmpty ? " ITALIA " : GlobalData . statoContraenteA ) ;
_tel = TextEditingController ( text: GlobalData . nTelefonoMailContraenteA ) ;
_email = TextEditingController ( text: GlobalData . emailContraenteA ) ;
_marca = TextEditingController ( text: GlobalData . marcaETipoA ) ;
_targa = TextEditingController ( text: GlobalData . targaA ) ;
_statoImm = TextEditingController ( text: GlobalData . statoImmatricolazioneA . isEmpty ? " ITALIA " : GlobalData . statoImmatricolazioneA ) ;
_rimorchio = TextEditingController ( text: pulisciBarre ( GlobalData . rimorchioA , " / " ) ) ;
_statoImm2 = TextEditingController ( text: pulisciBarre ( GlobalData . statoImmatricolazione2A , " / " ) ) ;
2026-02-27 23:26:13 +01:00
}
}
// --- POPUP: CHIEDE SE USARE I DATI SALVATI IN PRECEDENZA ---
2026-04-28 16:00:03 +02:00
Future < void > _mostraDialogoAutocompletamento ( ) async {
2026-02-27 23:26:13 +01:00
Color activeColor = isB ? Colors . amber . shade700 : Colors . blue . shade900 ;
bool ? usaDati = await showDialog < bool > (
context: context ,
barrierDismissible: false ,
builder: ( ctx ) = > AlertDialog (
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) ,
title: Row (
children: [
Icon ( Icons . contact_mail , color: activeColor , size: 28 ) ,
const SizedBox ( width: 10 ) ,
const Expanded ( child: Text ( " Dati Trovati " , style: TextStyle ( fontWeight: FontWeight . bold ) ) ) ,
] ,
) ,
2026-04-28 16:00:03 +02:00
content: const Text (
" Vuoi usare i dati salvati precedentemente? " ,
style: TextStyle ( fontSize: 16 )
2026-02-27 23:26:13 +01:00
) ,
actions: [
TextButton (
onPressed: ( ) = > Navigator . pop ( ctx , false ) ,
child: Text ( " NO, SCRIVO A MANO " , style: TextStyle ( color: Colors . grey [ 600 ] , fontWeight: FontWeight . bold ) ) ,
) ,
ElevatedButton (
style: ElevatedButton . styleFrom (
backgroundColor: activeColor ,
foregroundColor: isB ? Colors . black87 : Colors . white ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 8 ) ) ,
) ,
onPressed: ( ) = > Navigator . pop ( ctx , true ) ,
child: const Text ( " SÌ, USA QUESTI " , style: TextStyle ( fontWeight: FontWeight . bold ) ) ,
) ,
] ,
) ,
) ;
if ( usaDati = = true & & mounted ) {
2026-04-28 16:00:03 +02:00
await ProfiloService . caricaProfilo ( GlobalData . latoCorrente ) ;
2026-02-27 23:26:13 +01:00
setState ( ( ) {
2026-04-28 16:00:03 +02:00
_initControllers ( ) ;
2026-02-27 23:26:13 +01:00
if ( _cf . text . length = = 16 ) {
_controllaCfInTempoReale ( _cf . text ) ;
}
} ) ;
}
2026-04-28 16:00:03 +02:00
if ( mounted ) {
_mostraInfoPopup ( context ) ;
}
2026-02-27 23:26:13 +01:00
}
void _mostraInfoPopup ( BuildContext context ) {
Color activeColor = isB ? Colors . amber . shade700 : Colors . blue . shade900 ;
showGeneralDialog (
context: context ,
barrierDismissible: false ,
barrierLabel: " Popup " ,
2026-04-24 23:00:16 +02:00
barrierColor: Colors . black . withValues ( alpha: 0.5 ) ,
2026-02-27 23:26:13 +01:00
transitionDuration: const Duration ( milliseconds: 400 ) ,
pageBuilder: ( context , animation , secondaryAnimation ) {
return AlertDialog (
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 20 ) ) ,
title: Row (
children: [
Icon ( Icons . directions_car , color: activeColor , size: 28 ) ,
const SizedBox ( width: 10 ) ,
const Expanded ( child: Text ( " Assicurato e Veicolo " , style: TextStyle ( fontWeight: FontWeight . bold , fontSize: 18 ) ) ) ,
] ,
) ,
content: SingleChildScrollView (
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
mainAxisSize: MainAxisSize . min ,
children: [
Text ( " In questa sezione compilerai i dati relativi al Veicolo ${ GlobalData . latoCorrente } . " , style: const TextStyle ( fontSize: 15 ) ) ,
const SizedBox ( height: 16 ) ,
_buildPopupRow ( Icons . person , " Dati Anagrafici " , " Il Codice Fiscale o P. IVA non è obbligatorio, ma se inserito verrà verificato in automatico. " ) ,
const SizedBox ( height: 12 ) ,
_buildPopupRow ( Icons . numbers , " Targa e Mezzo " , " Inserisci la targa esatta (senza spazi) e la marca del veicolo. " ) ,
const SizedBox ( height: 12 ) ,
_buildPopupRow ( Icons . rv_hookup , " Rimorchio " , " Compila questa parte SOLO se il veicolo trainava un rimorchio al momento dell'incidente. " ) ,
] ,
) ,
) ,
actions: [
SizedBox (
width: double . infinity ,
child: ElevatedButton (
style: ElevatedButton . styleFrom (
backgroundColor: activeColor ,
foregroundColor: isB ? Colors . black87 : Colors . white ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 12 ) ) ,
padding: const EdgeInsets . symmetric ( vertical: 14 ) ,
) ,
onPressed: ( ) = > Navigator . pop ( context ) ,
child: const Text ( " HO CAPITO " , style: TextStyle ( fontSize: 16 , fontWeight: FontWeight . bold ) ) ,
) ,
) ,
] ,
) ;
} ,
transitionBuilder: ( context , animation , secondaryAnimation , child ) {
var curvePosizione = CurvedAnimation ( parent: animation , curve: Curves . easeOutBack , reverseCurve: Curves . easeInBack ) ;
var curveOpacita = CurvedAnimation ( parent: animation , curve: Curves . easeOut , reverseCurve: Curves . easeIn ) ;
return SlideTransition ( position: Tween < Offset > ( begin: const Offset ( 0.0 , 0.4 ) , end: Offset . zero ) . animate ( curvePosizione ) , child: FadeTransition ( opacity: curveOpacita , child: child ) ) ;
} ,
) ;
}
Widget _buildPopupRow ( IconData icon , String title , String desc ) {
return Row (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Icon ( icon , size: 24 , color: Colors . blueGrey ) ,
const SizedBox ( width: 12 ) ,
Expanded ( child: RichText ( text: TextSpan ( style: const TextStyle ( fontSize: 14 , color: Colors . black87 , height: 1.4 ) , children: [ TextSpan ( text: " $ title : " , style: const TextStyle ( fontWeight: FontWeight . bold ) ) , TextSpan ( text: desc ) ] ) ) ) ,
] ,
) ;
}
String _calcolaCodiceCognome ( String cognome ) {
cognome = cognome . toUpperCase ( ) . replaceAll ( RegExp ( r'[^A-Z]' ) , ' ' ) ;
if ( cognome . isEmpty ) return " XXX " ;
String consonanti = cognome . replaceAll ( RegExp ( r'[AEIOU]' ) , ' ' ) ;
String vocali = cognome . replaceAll ( RegExp ( r'[^AEIOU]' ) , ' ' ) ;
return ( consonanti + vocali + " XXX " ) . substring ( 0 , 3 ) ;
}
String _calcolaCodiceNome ( String nome ) {
nome = nome . toUpperCase ( ) . replaceAll ( RegExp ( r'[^A-Z]' ) , ' ' ) ;
if ( nome . isEmpty ) return " XXX " ;
String consonanti = nome . replaceAll ( RegExp ( r'[AEIOU]' ) , ' ' ) ;
String vocali = nome . replaceAll ( RegExp ( r'[^AEIOU]' ) , ' ' ) ;
if ( consonanti . length > = 4 ) {
return consonanti [ 0 ] + consonanti [ 2 ] + consonanti [ 3 ] ;
} else {
return ( consonanti + vocali + " XXX " ) . substring ( 0 , 3 ) ;
}
}
bool _isCodiceFiscaleValido ( String cf ) {
cf = cf . toUpperCase ( ) . trim ( ) ;
if ( cf . isEmpty ) return false ;
if ( ! RegExp ( r"^[A-Z0-9]{16}$" ) . hasMatch ( cf ) ) return false ;
final String setDispari = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ " ;
final String setPari = " 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ " ;
final List < int > valoriDispari = [ 1 , 0 , 5 , 7 , 9 , 13 , 15 , 17 , 19 , 21 , 1 , 0 , 5 , 7 , 9 , 13 , 15 , 17 , 19 , 21 , 2 , 4 , 18 , 20 , 11 , 3 , 6 , 8 , 12 , 14 , 16 , 10 , 22 , 25 , 24 , 23 ] ;
final List < int > valoriPari = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 , 21 , 22 , 23 , 24 , 25 ] ;
int somma = 0 ;
for ( int i = 0 ; i < 15 ; i + + ) {
String char = cf [ i ] ;
int val ;
if ( ( i + 1 ) % 2 ! = 0 ) {
int index = setDispari . indexOf ( char ) ;
if ( index = = - 1 ) return false ;
val = valoriDispari [ index ] ;
} else {
int index = setPari . indexOf ( char ) ;
if ( index = = - 1 ) return false ;
val = valoriPari [ index ] ;
}
somma + = val ;
}
int resto = somma % 26 ;
String carattereControlloCalcolato = String . fromCharCode ( 65 + resto ) ;
return carattereControlloCalcolato = = cf [ 15 ] ;
}
void _salvaTutto ( ) async {
FocusScope . of ( context ) . unfocus ( ) ;
// Rimosso _cf dalla lista dei campi obbligatori
if ( _cognome . text . trim ( ) . isEmpty | | _nome . text . trim ( ) . isEmpty | |
_indirizzo . text . trim ( ) . isEmpty | | _cap . text . trim ( ) . isEmpty | | _stato . text . trim ( ) . isEmpty | |
_tel . text . trim ( ) . isEmpty | | _marca . text . trim ( ) . isEmpty | | _targa . text . trim ( ) . isEmpty | |
_statoImm . text . trim ( ) . isEmpty ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( " Compila tutti i campi obbligatori! " ) , backgroundColor: Colors . red ) ) ;
return ;
}
2026-04-28 20:20:45 +02:00
// Validazione Telefono
String telefono = _tel . text . trim ( ) ;
if ( ! RegExp ( r'^\+?[0-9\s\-\.]+$' ) . hasMatch ( telefono ) ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( " Numero di telefono non valido. Usa solo numeri e il prefisso +. " ) , backgroundColor: Colors . orange ) ) ;
return ;
}
// Validazione Email (se inserita)
String email = _email . text . trim ( ) ;
if ( email . isNotEmpty & & ! RegExp ( r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+' ) . hasMatch ( email ) ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( const SnackBar ( content: Text ( " Formato email non valido! " ) , backgroundColor: Colors . orange ) ) ;
return ;
}
2026-02-27 23:26:13 +01:00
String cfInserito = _cf . text . trim ( ) . toUpperCase ( ) ;
// Controlliamo la validità SOLO se il campo non è vuoto
if ( cfInserito . isNotEmpty ) {
bool isPIVA = RegExp ( r"^[0-9]{11}$" ) . hasMatch ( cfInserito ) ;
bool isCF = _isCodiceFiscaleValido ( cfInserito ) ;
if ( ! isPIVA & & ! isCF ) {
ScaffoldMessenger . of ( context ) . showSnackBar (
const SnackBar (
content: Text ( " Cod. Fiscale / P. IVA NON VALIDO (Errato calcolo finale o formato). " ) ,
backgroundColor: Colors . orange ,
duration: Duration ( seconds: 3 ) ,
)
) ;
return ;
}
// Applichiamo il controllo incrociato con nome/cognome SOLO se è un Codice Fiscale
if ( isCF ) {
String cfCognome = cfInserito . substring ( 0 , 3 ) ;
String calcolatoCognome = _calcolaCodiceCognome ( _cognome . text ) ;
if ( cfCognome ! = calcolatoCognome ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar (
content: Text ( " Il CF non corrisponde al COGNOME inserito. \n Atteso: $ calcolatoCognome , Trovato: $ cfCognome " ) ,
backgroundColor: Colors . red ,
duration: const Duration ( seconds: 4 ) ,
) ) ;
return ;
}
String cfNome = cfInserito . substring ( 3 , 6 ) ;
String calcolatoNome = _calcolaCodiceNome ( _nome . text ) ;
if ( cfNome ! = calcolatoNome ) {
ScaffoldMessenger . of ( context ) . showSnackBar ( SnackBar (
content: Text ( " Il CF non corrisponde al NOME inserito. \n Atteso: $ calcolatoNome , Trovato: $ cfNome " ) ,
backgroundColor: Colors . red ,
duration: const Duration ( seconds: 4 ) ,
) ) ;
return ;
}
}
}
bool currentIsB = GlobalData . latoCorrente = = ' B ' ;
String rimorchioFinale = _rimorchio . text . trim ( ) ;
String statoImm2Finale = _statoImm2 . text . trim ( ) ;
if ( rimorchioFinale . isEmpty ) {
rimorchioFinale = " / / / / / / " ;
statoImm2Finale = " / / / / / / / / / / " ;
} else {
rimorchioFinale = rimorchioFinale . toUpperCase ( ) ;
statoImm2Finale = statoImm2Finale . isEmpty ? " ITALIA " : statoImm2Finale . toUpperCase ( ) ;
}
if ( currentIsB ) {
2026-05-02 12:00:02 +02:00
GlobalData . cognomeContraenteB = _cognome . text . trim ( ) . toUpperCase ( ) ;
GlobalData . nomeContraenteB = _nome . text . trim ( ) . toUpperCase ( ) ;
GlobalData . codiceFiscaleContraenteB = cfInserito ;
GlobalData . indirizzoContraenteB = _indirizzo . text . trim ( ) . toUpperCase ( ) ;
GlobalData . capContraenteB = _cap . text . trim ( ) ;
GlobalData . statoContraenteB = _stato . text . trim ( ) . toUpperCase ( ) ;
GlobalData . nTelefonoMailContraenteB = telefono ;
GlobalData . emailContraenteB = email . toLowerCase ( ) ;
GlobalData . marcaETipoB = _marca . text . trim ( ) . toUpperCase ( ) ;
GlobalData . targaB = _targa . text . trim ( ) . toUpperCase ( ) ;
GlobalData . statoImmatricolazioneB = _statoImm . text . trim ( ) . toUpperCase ( ) ;
GlobalData . rimorchioB = rimorchioFinale ;
GlobalData . statoImmatricolazione2B = statoImm2Finale ;
2026-02-27 23:26:13 +01:00
} else {
2026-05-02 12:00:02 +02:00
GlobalData . cognomeContraenteA = _cognome . text . trim ( ) . toUpperCase ( ) ;
GlobalData . nomeContraenteA = _nome . text . trim ( ) . toUpperCase ( ) ;
GlobalData . codiceFiscaleContraenteA = cfInserito ;
GlobalData . indirizzoContraenteA = _indirizzo . text . trim ( ) . toUpperCase ( ) ;
GlobalData . capContraenteA = _cap . text . trim ( ) ;
GlobalData . statoContraenteA = _stato . text . trim ( ) . toUpperCase ( ) ;
GlobalData . nTelefonoMailContraenteA = telefono ;
GlobalData . emailContraenteA = email . toLowerCase ( ) ;
GlobalData . marcaETipoA = _marca . text . trim ( ) . toUpperCase ( ) ;
GlobalData . targaA = _targa . text . trim ( ) . toUpperCase ( ) ;
GlobalData . statoImmatricolazioneA = _statoImm . text . trim ( ) . toUpperCase ( ) ;
GlobalData . rimorchioA = rimorchioFinale ;
GlobalData . statoImmatricolazione2A = statoImm2Finale ;
2026-02-27 23:26:13 +01:00
}
// <-- SALVATAGGIO DEI DATI NELLA TASTIERA DI SISTEMA (Per l'Autofill) -->
TextInput . finishAutofillContext ( ) ;
if ( ! mounted ) return ;
Navigator . push ( context , MaterialPageRoute ( builder: ( c ) = > const Comp8Screen ( ) ) ) ;
}
@ override
Widget build ( BuildContext context ) {
Color mainCol = isB ? Colors . amber . shade700 : Colors . blue . shade900 ;
Color bgCol = isB ? const Color ( 0xFFFFF9C4 ) : const Color ( 0xFFE3F2FD ) ;
if ( ! _isReady ) {
return Scaffold ( backgroundColor: bgCol , body: Container ( ) ) ;
}
return PopScope (
canPop: true ,
child: GestureDetector (
onTap: ( ) = > FocusScope . of ( context ) . unfocus ( ) ,
child: Scaffold (
backgroundColor: bgCol ,
resizeToAvoidBottomInset: true ,
appBar: AppBar (
title: Text ( " Sez. 6-7: Veicolo ${ GlobalData . latoCorrente } " ) ,
backgroundColor: mainCol ,
foregroundColor: isB ? Colors . black : Colors . white ,
) ,
body: SafeArea (
child: Form ( // <-- FORM PER L'AUTOFILL
child: AutofillGroup ( // <-- GRUPPO AUTOFILL
child: CustomScrollView (
physics: const BouncingScrollPhysics ( ) ,
slivers: [
SliverPadding (
padding: const EdgeInsets . all ( 16 ) ,
sliver: SliverList (
delegate: SliverChildListDelegate ( [
_buildSectionCard (
titolo: " 6. CONTRAENTE / ASSICURATO " ,
accentColor: mainCol ,
children: [
_buildField ( _cognome , " Cognome * " , Icons . person ,
autofillHints: const [ AutofillHints . familyName ] ,
keyboardType: TextInputType . name ) ,
_buildField ( _nome , " Nome * " , Icons . person_outline ,
autofillHints: const [ AutofillHints . givenName ] ,
keyboardType: TextInputType . name ) ,
// Sostituito "Codice Fiscale *" con "Cod. Fiscale / P. IVA"
_buildField (
_cf ,
" Cod. Fiscale / P. IVA " ,
Icons . badge ,
isCF: true ,
focusNode: _cfFocusNode ,
onChanged: _controllaCfInTempoReale ,
) ,
_buildField ( _indirizzo , " Indirizzo * " , Icons . home ,
autofillHints: const [ AutofillHints . streetAddressLine1 , AutofillHints . fullStreetAddress ] ,
keyboardType: TextInputType . streetAddress ) ,
Row (
children: [
Expanded ( child: _buildField ( _cap , " C.A.P. * " , Icons . location_on ,
isNumeric: true ,
autofillHints: const [ AutofillHints . postalCode ] ,
keyboardType: const TextInputType . numberWithOptions ( signed: true ) ) ) ,
const SizedBox ( width: 10 ) ,
Expanded ( child: _buildField ( _stato , " Stato * " , Icons . flag ,
autofillHints: const [ AutofillHints . countryName ] ,
keyboardType: TextInputType . text ) ) ,
]
) ,
2026-04-28 20:20:45 +02:00
_buildField ( _tel , " Telefono * " , Icons . phone ,
autofillHints: const [ AutofillHints . telephoneNumber ] ,
keyboardType: TextInputType . phone ) ,
_buildField ( _email , " Email (Opzionale) " , Icons . email ,
autofillHints: const [ AutofillHints . email ] ,
2026-02-27 23:26:13 +01:00
keyboardType: TextInputType . emailAddress ) ,
] ,
) ,
_buildSectionCard (
titolo: " 7. VEICOLO A MOTORE " ,
accentColor: mainCol ,
children: [
_buildField ( _marca , " Marca e Tipo * " , Icons . directions_car ) ,
const SizedBox ( height: 10 ) ,
Row (
children: [
Expanded ( child: _buildField ( _targa , " Targa * " , Icons . numbers , isUpper: true ) ) ,
const SizedBox ( width: 10 ) ,
Expanded ( child: _buildField ( _rimorchio , " Rimorchio (Opz) " , Icons . rv_hookup ) ) ,
]
) ,
const SizedBox ( height: 10 ) ,
Row (
children: [
Expanded ( child: _buildField ( _statoImm , " Stato Imm. * " , Icons . public ) ) ,
const SizedBox ( width: 10 ) ,
Expanded ( child: _buildField ( _statoImm2 , " Stato Rimorchio " , Icons . public_off ) ) ,
]
) ,
] ,
) ,
] ) ,
) ,
) ,
SliverFillRemaining (
hasScrollBody: false ,
child: Align (
alignment: Alignment . bottomCenter ,
child: Padding (
padding: const EdgeInsets . all ( 16 ) ,
child: _navButtons ( mainCol ) ,
) ,
) ,
) ,
] ,
) ,
) ,
) ,
) ,
) ,
) ,
) ;
}
// WIDGET FIELD CON HINTS E KEYBOARD TYPES
Widget _buildField (
TextEditingController controller ,
String label ,
IconData icon ,
{
bool isUpper = false ,
bool isNumeric = false ,
bool isCF = false ,
FocusNode ? focusNode ,
Function ( String ) ? onChanged ,
Iterable < String > ? autofillHints ,
TextInputType ? keyboardType ,
}
) {
List < TextInputFormatter > formatters = [ ] ;
if ( isNumeric ) {
formatters . add ( FilteringTextInputFormatter . digitsOnly ) ;
formatters . add ( LengthLimitingTextInputFormatter ( 5 ) ) ;
} else if ( isCF ) {
formatters . add ( FilteringTextInputFormatter . allow ( RegExp ( " [a-zA-Z0-9] " ) ) ) ;
formatters . add ( UpperCaseTextFormatter ( ) ) ;
formatters . add ( LengthLimitingTextInputFormatter ( 16 ) ) ;
} else if ( isUpper ) {
formatters . add ( UpperCaseTextFormatter ( ) ) ;
formatters . add ( FilteringTextInputFormatter . deny ( RegExp ( r'\s' ) ) ) ;
}
TextInputType finalKeyboardType = keyboardType ? ? ( isNumeric ? TextInputType . number : TextInputType . text ) ;
return Padding (
padding: const EdgeInsets . only ( bottom: 12 ) ,
child: TextField (
controller: controller ,
focusNode: focusNode ,
onChanged: onChanged ,
keyboardType: finalKeyboardType ,
inputFormatters: formatters ,
autofillHints: autofillHints , // <- Suggerimenti OS nativi
textCapitalization: ( isUpper | | isCF ) ? TextCapitalization . characters : TextCapitalization . sentences ,
decoration: InputDecoration (
labelText: label ,
prefixIcon: Icon ( icon , size: 20 , color: isB ? Colors . orange . shade800 : Colors . blue . shade700 ) ,
border: OutlineInputBorder ( borderRadius: BorderRadius . circular ( 10 ) ) ,
filled: true ,
fillColor: Colors . white ,
) ,
) ,
) ;
}
Widget _buildSectionCard ( { required String titolo , required List < Widget > children , required Color accentColor } ) {
return Card (
elevation: 2 ,
margin: const EdgeInsets . only ( bottom: 16 ) ,
shape: RoundedRectangleBorder ( borderRadius: BorderRadius . circular ( 15 ) ) ,
child: Padding (
padding: const EdgeInsets . all ( 16 ) ,
child: Column (
crossAxisAlignment: CrossAxisAlignment . start ,
children: [
Text (
titolo ,
style: TextStyle ( fontWeight: FontWeight . bold , color: accentColor , fontSize: 16 )
) ,
const Divider ( height: 25 ) ,
. . . children ,
]
) ,
) ,
) ;
}
Widget _navButtons ( Color col ) {
return Row (
children: [
Expanded (
child: OutlinedButton (
onPressed: ( ) = > Navigator . pop ( context ) ,
style: OutlinedButton . styleFrom ( minimumSize: const Size ( 0 , 55 ) ) ,
child: const Text ( " INDIETRO " )
)
) ,
const SizedBox ( width: 15 ) ,
Expanded (
flex: 2 ,
child: ElevatedButton (
style: ElevatedButton . styleFrom (
backgroundColor: col ,
foregroundColor: isB ? Colors . black : Colors . white ,
minimumSize: const Size ( 0 , 55 )
) ,
onPressed: _salvaTutto ,
child: const Text ( " SALVA E PROSEGUI " , style: TextStyle ( fontWeight: FontWeight . bold ) )
)
) ,
]
) ;
}
@ override
void dispose ( ) {
_cfFocusNode . removeListener ( _onCfFocusChange ) ;
_cfFocusNode . dispose ( ) ;
_cognome . dispose ( ) ; _nome . dispose ( ) ; _cf . dispose ( ) ; _indirizzo . dispose ( ) ;
2026-04-28 20:20:45 +02:00
_cap . dispose ( ) ; _stato . dispose ( ) ; _tel . dispose ( ) ; _email . dispose ( ) ;
2026-02-27 23:26:13 +01:00
_marca . dispose ( ) ; _targa . dispose ( ) ; _rimorchio . dispose ( ) ;
_statoImm . dispose ( ) ; _statoImm2 . dispose ( ) ;
super . dispose ( ) ;
}
}