cid_app/lib/comp_6-7.dart

720 lines
No EOL
30 KiB
Dart

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';
// 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> {
late TextEditingController _cognome, _nome, _cf, _indirizzo, _cap, _stato, _tel;
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 {
final prefs = await SharedPreferences.getInstance();
String? sCognome = prefs.getString('user_cognome');
debugPrint("🤖 [AUTOFILL] Dati trovati in memoria: Cognome = $sCognome");
debugPrint("🤖 [AUTOFILL] Testo attuale nel controller: '${_cognome.text}'");
// 1. POPUP AUTOCOMPLETAMENTO LOCALE (se ci sono dati e i campi sono vuoti)
if (sCognome != null && sCognome.trim().isNotEmpty && _cognome.text.trim().isEmpty) {
debugPrint("🤖 [AUTOFILL] Mostro il popup dei dati salvati!");
await _mostraDialogoAutocompletamento(prefs);
} else {
debugPrint("🤖 [AUTOFILL] Condizioni non soddisfatte, salto il popup dati.");
}
// 2. POPUP INFORMATIVO STANDARD (Appare sempre DOPO)
if (mounted) {
debugPrint("🤖 [AUTOFILL] Mostro il popup informativo standard.");
_mostraInfoPopup(context);
}
}
void _initControllers() {
String pulisciBarre(String valore, String patternBarre) {
if (valore.contains("/")) return "";
return valore;
}
if (isB) {
_cognome = TextEditingController(text: GlobalData.Cognome_contraente_B);
_nome = TextEditingController(text: GlobalData.Nome_contraente_B);
_cf = TextEditingController(text: GlobalData.Codice_Fiscale_contraente_B);
_indirizzo = TextEditingController(text: GlobalData.Indirizzo_contraente_B);
_cap = TextEditingController(text: GlobalData.CAP_contraente_B);
_stato = TextEditingController(text: GlobalData.Stato_contraente_B.isEmpty ? "ITALIA" : GlobalData.Stato_contraente_B);
_tel = TextEditingController(text: GlobalData.N_telefono_mail_contraente_B);
_marca = TextEditingController(text: GlobalData.Marca_e_Tipo_B);
_targa = TextEditingController(text: GlobalData.Targa_B);
_statoImm = TextEditingController(text: GlobalData.Stato_immatricolazione_B.isEmpty ? "ITALIA" : GlobalData.Stato_immatricolazione_B);
_rimorchio = TextEditingController(text: pulisciBarre(GlobalData.Rimorchio_B, "/"));
_statoImm2 = TextEditingController(text: pulisciBarre(GlobalData.Stato_immatricolazione2_B, "/"));
} else {
_cognome = TextEditingController(text: GlobalData.Cognome_contraente_A);
_nome = TextEditingController(text: GlobalData.Nome_contraente_A);
_cf = TextEditingController(text: GlobalData.Codice_Fiscale_contraente_A);
_indirizzo = TextEditingController(text: GlobalData.Indirizzo_contraente_A);
_cap = TextEditingController(text: GlobalData.CAP_contraente_A);
_stato = TextEditingController(text: GlobalData.Stato_contraente_A.isEmpty ? "ITALIA" : GlobalData.Stato_contraente_A);
_tel = TextEditingController(text: GlobalData.N_telefono_mail_contraente_A);
_marca = TextEditingController(text: GlobalData.Marca_e_Tipo_A);
_targa = TextEditingController(text: GlobalData.Targa_A);
_statoImm = TextEditingController(text: GlobalData.Stato_immatricolazione_A.isEmpty ? "ITALIA" : GlobalData.Stato_immatricolazione_A);
_rimorchio = TextEditingController(text: pulisciBarre(GlobalData.Rimorchio_A, "/"));
_statoImm2 = TextEditingController(text: pulisciBarre(GlobalData.Stato_immatricolazione2_A, "/"));
}
}
// --- POPUP: CHIEDE SE USARE I DATI SALVATI IN PRECEDENZA ---
Future<void> _mostraDialogoAutocompletamento(SharedPreferences prefs) async {
String sCognome = prefs.getString('user_cognome') ?? '';
String sNome = prefs.getString('user_nome') ?? '';
String sCF = prefs.getString('user_cf') ?? '';
String sIndirizzo = prefs.getString('user_indirizzo') ?? '';
String sCap = prefs.getString('user_cap') ?? '';
String sTel = prefs.getString('user_tel') ?? '';
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))),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text("Abbiamo trovato i tuoi dati salvati in una compilazione precedente:", style: TextStyle(fontSize: 15)),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.grey.shade100, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.grey.shade300)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("$sNome $sCognome", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
if (sCF.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text("• CF/PIVA: $sCF")),
if (sIndirizzo.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text("$sIndirizzo, $sCap")),
if (sTel.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text("• Tel: $sTel")),
],
),
),
const SizedBox(height: 16),
const Text("Vuoi usarli per compilare automaticamente questa sezione?", style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold)),
],
),
),
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) {
setState(() {
_cognome.text = sCognome;
_nome.text = sNome;
_cf.text = sCF;
_indirizzo.text = sIndirizzo;
_cap.text = sCap;
_tel.text = sTel;
if (_cf.text.length == 16) {
_controllaCfInTempoReale(_cf.text);
}
});
}
}
void _mostraInfoPopup(BuildContext context) {
Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900;
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierLabel: "Popup",
barrierColor: Colors.black.withOpacity(0.5),
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];
}
Future<void> _salvaDatiSulDispositivo() async {
final prefs = await SharedPreferences.getInstance();
debugPrint("🤖 [AUTOFILL] Salvataggio dati sul dispositivo...");
if (_cognome.text.isNotEmpty) await prefs.setString('user_cognome', _cognome.text.trim());
if (_nome.text.isNotEmpty) await prefs.setString('user_nome', _nome.text.trim());
if (_cf.text.isNotEmpty) await prefs.setString('user_cf', _cf.text.trim());
if (_indirizzo.text.isNotEmpty) await prefs.setString('user_indirizzo', _indirizzo.text.trim());
if (_cap.text.isNotEmpty) await prefs.setString('user_cap', _cap.text.trim());
if (_tel.text.isNotEmpty) await prefs.setString('user_tel', _tel.text.trim());
}
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;
}
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.\nAtteso: $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.\nAtteso: $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) {
GlobalData.Cognome_contraente_B = _cognome.text.trim().toUpperCase();
GlobalData.Nome_contraente_B = _nome.text.trim().toUpperCase();
GlobalData.Codice_Fiscale_contraente_B = cfInserito;
GlobalData.Indirizzo_contraente_B = _indirizzo.text.trim().toUpperCase();
GlobalData.CAP_contraente_B = _cap.text.trim();
GlobalData.Stato_contraente_B = _stato.text.trim().toUpperCase();
GlobalData.N_telefono_mail_contraente_B = _tel.text.trim().toUpperCase();
GlobalData.Marca_e_Tipo_B = _marca.text.trim().toUpperCase();
GlobalData.Targa_B = _targa.text.trim().toUpperCase();
GlobalData.Stato_immatricolazione_B = _statoImm.text.trim().toUpperCase();
GlobalData.Rimorchio_B = rimorchioFinale;
GlobalData.Stato_immatricolazione2_B = statoImm2Finale;
} else {
GlobalData.Cognome_contraente_A = _cognome.text.trim().toUpperCase();
GlobalData.Nome_contraente_A = _nome.text.trim().toUpperCase();
GlobalData.Codice_Fiscale_contraente_A = cfInserito;
GlobalData.Indirizzo_contraente_A = _indirizzo.text.trim().toUpperCase();
GlobalData.CAP_contraente_A = _cap.text.trim();
GlobalData.Stato_contraente_A = _stato.text.trim().toUpperCase();
GlobalData.N_telefono_mail_contraente_A = _tel.text.trim().toUpperCase();
GlobalData.Marca_e_Tipo_A = _marca.text.trim().toUpperCase();
GlobalData.Targa_A = _targa.text.trim().toUpperCase();
GlobalData.Stato_immatricolazione_A = _statoImm.text.trim().toUpperCase();
GlobalData.Rimorchio_A = rimorchioFinale;
GlobalData.Stato_immatricolazione2_A = statoImm2Finale;
}
// <-- SALVATAGGIO IN MEMORIA DEI DATI (Per il Popup) -->
await _salvaDatiSulDispositivo();
// <-- 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)),
]
),
_buildField(_tel, "Tel / Email *", Icons.email,
autofillHints: const [AutofillHints.telephoneNumber, AutofillHints.email],
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();
_cap.dispose(); _stato.dispose(); _tel.dispose();
_marca.dispose(); _targa.dispose(); _rimorchio.dispose();
_statoImm.dispose(); _statoImm2.dispose();
super.dispose();
}
}