=== CAI FACILE PROJECT BACKUP === === pubspec.yaml === name: cid_app description: "Applicazione per la compilazione assistita del modulo CAI/CID digitale." publish_to: 'none' version: 1.0.5+3 environment: sdk: '>=3.10.0 <4.0.0' dependencies: flutter: sdk: flutter # --- MOTORE PDF E STAMPA --- syncfusion_flutter_core: ^32.2.3 syncfusion_flutter_pdf: ^32.2.3 pdf: ^3.10.7 printing: ^5.14.2 path_provider: ^2.1.5 share_plus: ^7.2.1 # Per aprire il PDF appena generato open_file: ^3.3.2 # --- FIREBASE E SICUREZZA --- firebase_core: ^3.10.1 cloud_firestore: ^5.6.2 firebase_database: ^11.3.0 firebase_auth: ^5.7.0 encrypt: ^5.0.3 # --- RETE E SERVIZI --- http: ^1.1.0 webview_flutter: ^4.4.0 flutter_inappwebview: ^6.0.0 intl: ^0.20.2 url_launcher: ^6.3.2 flutter_email_sender: ^6.0.3 # --- GEOLOCALIZZAZIONE --- geolocator: ^14.0.2 geocoding: ^4.0.0 # --- QR CODE E SCANSIONE --- qr_flutter: ^4.1.0 mobile_scanner: ^6.0.0 # --- CAMERA E OCR --- camera: ^0.11.0 google_mlkit_text_recognition: ^0.14.0 google_mlkit_barcode_scanning: ^0.13.0 google_mlkit_commons: ^0.9.0 permission_handler: ^11.3.1 # --- GRAFICA E INPUT --- image: ^4.2.0 signature: ^6.3.0 html: ^0.15.4 # LIBRERIE SYNCFUSION syncfusion_flutter_pdfviewer: ^32.2.3 syncfusion_flutter_signaturepad: ^32.2.3 device_info_plus: ^12.3.0 flutter_localizations: sdk: flutter dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^6.0.0 flutter_launcher_icons: ^0.13.1 flutter: uses-material-design: true assets: - assets/CAI_p1.pdf - assets/punti_danni_A.png - assets/punti_danni_B.png - assets/ospedali_completo.json - assets/sfondo_mappa.jpg - assets/fonts/Roboto-Bold.ttf - assets/sfondo_cid.jpg - assets/icona.png fonts: - family: Roboto fonts: - asset: assets/fonts/Roboto-Bold.ttf weight: 700 flutter_launcher_icons: android: "launcher_icon" ios: true image_path: "assets/icona.png" remove_alpha_ios: true === MAC OS CONFIG: Info.plist === CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu NSPrincipalClass NSApplication === MAC OS CONFIG: Entitlements === com.apple.security.app-sandbox com.apple.security.cs.allow-jit com.apple.security.network.server com.apple.security.app-sandbox === MAC OS CONFIG: Podfile === platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_macos_podfile_setup target 'Runner' do use_frameworks! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) end end === IOS CONFIG: Info.plist === CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName CAI App CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName cid_app CFBundlePackageType APPL CFBundleShortVersionString $(FLUTTER_BUILD_NAME) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes mailto LSRequiresIPhoneOS LSSupportsOpeningDocumentsInPlace NSCameraUsageDescription La fotocamera è necessaria per scansionare il QR Code e importare i dati del sinistro. NSLocationWhenInUseUsageDescription La tua posizione serve per compilare automaticamente il luogo dell'incidente nel modulo. NSLocationAlwaysAndWhenInUseUsageDescription La tua posizione serve per compilare il luogo dell'incidente anche se l'app è in background. NSPhotoLibraryAddUsageDescription Serve per salvare il modulo PDF o i disegni dei danni nel rullino. NSPhotoLibraryUsageDescription Serve per allegare le foto dei danni al veicolo. UIApplicationSupportsIndirectInputEvents UIFileSharingEnabled UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight === IOS CONFIG: Podfile === # Uncomment this line to define a global platform for your project platform :ios, '15.5' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths end end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) # --- AGGIUNGI QUESTE 3 RIGHE QUI SOTTO --- target.build_configurations.each do |config| config.build_settings['DEBUG_INFORMATION_FORMAT'] = 'dwarf-with-dsym' end # ---------------------------------------- end end === ANDROID CONFIG: AndroidManifest.xml === === ANDROID CONFIG: build.gradle === plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" } // 1. CARICAMENTO PASSWORD KEYSTORE (NUOVO) def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') if (keystorePropertiesFile.exists()) { keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) } def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } // --- FIX PER LE DIPENDENZE TROPPO RECENTI --- configurations.all { resolutionStrategy { force 'androidx.browser:browser:1.8.0' force 'androidx.core:core:1.13.1' force 'androidx.core:core-ktx:1.13.1' } } android { namespace "com.amastra.cid_app" compileSdk 36 ndkVersion "27.0.12077973" compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { jvmTarget = '17' } sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { applicationId "com.amastra.cid_app" minSdkVersion flutter.minSdkVersion targetSdkVersion 35 versionCode flutterVersionCode.toInteger() versionName flutterVersionName multiDexEnabled true } // 2. CONFIGURAZIONE FIRMA (NUOVO) signingConfigs { release { keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null storePassword keystoreProperties['storePassword'] } } buildTypes { release { // 3. USA LA FIRMA DI RILASCIO (CORRETTO) signingConfig signingConfigs.release minifyEnabled false shrinkResources false } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.0" implementation 'androidx.multidex:multidex:2.0.1' } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { jvmTarget = "17" } } === ANDROID CONFIG: build.gradle.kts === === SOURCE CODE === === FILE: lib/comp_1-5.dart === import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; import 'package:geolocator/geolocator.dart'; import 'package:geocoding/geocoding.dart'; import 'comp_6-7.dart'; import 'global_data.dart'; // Formattatore per inserire automaticamente gli slash nella data class DateInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { var text = newValue.text.replaceAll('/', ''); if (text.length > 8) text = text.substring(0, 8); var result = ""; for (int i = 0; i < text.length; i++) { if (i == 2 || i == 4) result += "/"; result += text[i]; } return newValue.copyWith(text: result, selection: TextSelection.collapsed(offset: result.length)); } } class Comp1_5Screen extends StatefulWidget { const Comp1_5Screen({super.key}); @override _Comp1_5ScreenState createState() => _Comp1_5ScreenState(); } class _Comp1_5ScreenState extends State { late TextEditingController _data, _ora, _luogo, _testimoni; late bool _feriti, _danniVeicoli, _danniCose; final bool isB = GlobalData.latoCorrente == 'B'; bool _isReady = false; // Per l'orientamento bool _gpsLoading = false; // Per mostrare lo spinner @override void initState() { super.initState(); _forzaVerticaleEInizializza(); } Future _forzaVerticaleEInizializza() async { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await Future.delayed(const Duration(milliseconds: 150)); _initControllers(); if (mounted) { setState(() => _isReady = true); // MOSTRA IL POPUP INFORMATIVO APPENA LA SCHERMATA È CARICATA WidgetsBinding.instance.addPostFrameCallback((_) { _mostraInfoPopup(context); }); // Se il luogo è vuoto, provo il GPS automatico in background if (_luogo.text.isEmpty) { _trovaPosizione(silenzioso: true); } } } void _initControllers() { _data = TextEditingController(text: GlobalData.data_incidente); if (_data.text.isEmpty) { _data.text = DateFormat('dd/MM/yyyy').format(DateTime.now()); } _ora = TextEditingController(text: GlobalData.ora); if (_ora.text.isEmpty) { _ora.text = DateFormat('HH:mm').format(DateTime.now()); } _luogo = TextEditingController(text: GlobalData.luogo); _testimoni = TextEditingController(text: GlobalData.testimoni); _feriti = GlobalData.feriti; _danniVeicoli = GlobalData.Veicoli_danni_materiali_oltre; _danniCose = GlobalData.Oggetti_diversi_danni_materiali; } // --- POPUP INFORMATIVO --- void _mostraInfoPopup(BuildContext context) { Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900; showGeneralDialog( context: context, barrierDismissible: false, // Obbliga l'utente a premere "Ho capito" barrierLabel: "Popup", barrierColor: Colors.black.withOpacity(0.5), // Sfondo scuro transitionDuration: const Duration(milliseconds: 400), // Durata della dissolvenza (300 millisecondi) pageBuilder: (context, animation, secondaryAnimation) { return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Row( children: [ Icon(Icons.info_outline, color: activeColor, size: 28), const SizedBox(width: 10), const Expanded(child: Text("Dati Generali", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20))), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text("In questa prima pagina dovrai inserire i dati condivisi dell'incidente (Sezioni 1-5 del modulo).", style: TextStyle(fontSize: 15)), const SizedBox(height: 16), _buildPopupRow(Icons.place, "Luogo", "Usa il mirino GPS per trovare automaticamente l'indirizzo esatto."), const SizedBox(height: 12), _buildPopupRow(Icons.local_hospital, "Feriti", "Indica se ci sono persone che hanno subito lesioni."), const SizedBox(height: 12), _buildPopupRow(Icons.people, "Testimoni", "Se qualcuno ha visto l'incidente, segna i suoi contatti (nome, cognome, telefono)."), ], ), ), 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)), ), ), ], ); }, // QUI GESTIAMO L'ANIMAZIONE DI DISSOLVENZA // NUOVA ANIMAZIONE COMBINATA (FADE + ZOOM) transitionBuilder: (context, animation, secondaryAnimation, child) { var curvePosizione = CurvedAnimation( parent: animation, curve: Curves.easeOutBack, // Quando entra, fa un piccolo "rimbalzo" frenato reverseCurve: Curves.easeInBack, // Quando esce, "prende la rincorsa" e cade giù ); var curveOpacita = CurvedAnimation( parent: animation, curve: Curves.easeOut, reverseCurve: Curves.easeIn, ); return SlideTransition( // Muove il popup sull'asse Y (verticale) position: Tween( begin: const Offset(0.0, 0.4), // Parte dal basso (o cade verso il basso) end: Offset.zero, // Centro esatto ).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), ], ), ), ), ], ); } // --- GEOLOCALIZZAZIONE --- Future _trovaPosizione({bool silenzioso = false}) async { if (!silenzioso) setState(() => _gpsLoading = true); try { LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { if (!silenzioso) throw Exception("Permesso negato"); return; } } if (permission == LocationPermission.deniedForever) { if (!silenzioso) throw Exception("Permessi GPS bloccati."); return; } Position position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high ).timeout(const Duration(seconds: 5)); List placemarks = await placemarkFromCoordinates(position.latitude, position.longitude); if (placemarks.isNotEmpty) { Placemark place = placemarks[0]; String via = place.thoroughfare ?? place.street ?? ""; String numero = place.subThoroughfare ?? ""; String citta = place.locality ?? place.subLocality ?? ""; String prov = place.administrativeArea ?? ""; String indirizzoCompleto = "$via $numero, $citta ($prov)".trim(); if (indirizzoCompleto.startsWith(",")) indirizzoCompleto = indirizzoCompleto.substring(1).trim(); if (indirizzoCompleto.isEmpty) { indirizzoCompleto = "${position.latitude}, ${position.longitude}"; } if (mounted) { setState(() { _luogo.text = indirizzoCompleto.toUpperCase(); }); } } } catch (e) { if (!silenzioso && mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Errore GPS: ${e.toString().replaceAll('Exception:', '')}"), backgroundColor: Colors.orange, )); } } finally { if (mounted) setState(() => _gpsLoading = false); } } Future _selezionaData(BuildContext context) async { DateTime initialDate = DateTime.now(); if (_data.text.length == 10) { try { List parti = _data.text.split('/'); initialDate = DateTime(int.parse(parti[2]), int.parse(parti[1]), int.parse(parti[0])); } catch (_) {} } final DateTime? picked = await showDatePicker( context: context, initialDate: initialDate, firstDate: DateTime(1990), lastDate: DateTime.now(), // Non si può fare un incidente nel futuro! locale: const Locale('it', 'IT'), ); if (picked != null) { String dataFormattata = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}"; setState(() { _data.text = dataFormattata; }); } } void _salvaEProsegui() { if (_data.text.isEmpty || _ora.text.isEmpty || _luogo.text.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Compila data, ora e luogo!"), backgroundColor: Colors.red)); return; } GlobalData.data_incidente = _data.text; GlobalData.ora = _ora.text; GlobalData.luogo = _luogo.text.toUpperCase(); GlobalData.feriti = _feriti; GlobalData.Veicoli_danni_materiali_oltre = _danniVeicoli; GlobalData.Oggetti_diversi_danni_materiali = _danniCose; GlobalData.testimoni = _testimoni.text.toUpperCase(); Navigator.push(context, MaterialPageRoute(builder: (context) => const Comp6_7Screen())); } @override Widget build(BuildContext context) { Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900; Color bgColor = isB ? const Color(0xFFFFF9C4) : const Color(0xFFE3F2FD); if (!_isReady) { return Scaffold(backgroundColor: bgColor, body: Container()); } return Scaffold( backgroundColor: bgColor, appBar: AppBar( title: const Text("Sez. 1-5: Dati Generali"), backgroundColor: activeColor, foregroundColor: Colors.white, ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // CARD 1: DATA E ORA _buildCard( titolo: "1. DATA E ORA", accentColor: activeColor, child: Row(children: [ Expanded( child: _buildField(_data, "Data (GG/MM/AAAA)", Icons.calendar_today, activeColor, isDate: true, customPrefix: InkWell( onTap: () => _selezionaData(context), borderRadius: BorderRadius.circular(20), child: Container( width: 38, alignment: Alignment.center, child: Icon(Icons.calendar_today, size: 20, color: activeColor), ), ), ), ), const SizedBox(width: 15), Expanded(child: _buildField(_ora, "Ora (HH:MM)", Icons.access_time, activeColor)), ]), ), // CARD 2: LUOGO _buildCard( titolo: "2. LUOGO", accentColor: activeColor, child: Row(children: [ Expanded(child: _buildField(_luogo, "Indirizzo / Luogo", Icons.map, activeColor, maxLines: 2)), _gpsLoading ? const Padding(padding: EdgeInsets.all(12), child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))) : IconButton( icon: const Icon(Icons.my_location, color: Colors.red), onPressed: () => _trovaPosizione(silenzioso: false), tooltip: "Usa GPS", ) ]), ), // CARD 3: FERITI _buildCard( titolo: "3. FERITI", accentColor: activeColor, child: _buildSwitch("Ci sono feriti?", _feriti, (v) => setState(() => _feriti = v), activeColor), ), // CARD 4: DANNI MATERIALI _buildCard( titolo: "4. ALTRI DANNI", accentColor: activeColor, child: Column(children: [ _buildSwitch("A veicoli oltre A e B?", _danniVeicoli, (v) => setState(() => _danniVeicoli = v), activeColor), const Divider(), _buildSwitch("A oggetti diversi dai veicoli?", _danniCose, (v) => setState(() => _danniCose = v), activeColor), ]), ), // CARD 5: TESTIMONI _buildCard( titolo: "5. TESTIMONI", accentColor: activeColor, child: _buildField(_testimoni, "Nomi, Indirizzi, Telefoni...", Icons.people, activeColor, maxLines: 3), ), const SizedBox(height: 20), ElevatedButton( onPressed: _salvaEProsegui, style: ElevatedButton.styleFrom( backgroundColor: activeColor, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 18), textStyle: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: const Text("SALVA E PROSEGUI"), ), const SizedBox(height: 30), ], ), ), ); } // --- WIDGET PERSONALIZZATO NO / SÌ --- Widget _buildSwitch(String label, bool value, Function(bool) onChanged, Color activeColor) { return InkWell( onTap: () => onChanged(!value), // Permette di cliccare su tutta la riga child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( children: [ // Etichetta Domanda Expanded( child: Text( label, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500), ), ), const SizedBox(width: 8), // Testo NO Text( "NO", style: TextStyle( fontWeight: !value ? FontWeight.bold : FontWeight.normal, color: !value ? Colors.red.shade700 : Colors.grey.shade400, fontSize: 14, ), ), // Switch Fisico Switch( value: value, onChanged: onChanged, activeColor: activeColor, activeTrackColor: activeColor.withOpacity(0.4), inactiveThumbColor: Colors.grey, inactiveTrackColor: Colors.grey.shade300, ), // Testo SÌ Text( "SÌ", style: TextStyle( fontWeight: value ? FontWeight.bold : FontWeight.normal, color: value ? activeColor : Colors.grey.shade400, fontSize: 14, ), ), ], ), ), ); } Widget _buildField(TextEditingController c, String label, IconData i, Color iconColor, {String? hint, int maxLines = 1, bool isDate = false, Widget? customPrefix}) { return TextField( controller: c, maxLines: maxLines, textCapitalization: TextCapitalization.sentences, keyboardType: isDate ? TextInputType.number : TextInputType.text, inputFormatters: isDate ? [DateInputFormatter(), LengthLimitingTextInputFormatter(10)] : [], style: TextStyle(fontSize: isDate ? 14 : 16), // Rimpicciolisce un po' il font se è una data decoration: InputDecoration( labelText: label, hintText: hint, prefixIcon: customPrefix ?? Icon(i, size: 20, color: iconColor), prefixIconConstraints: isDate ? const BoxConstraints(minWidth: 38, minHeight: 38) : null, // Stringe l'icona border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15), ), ); } Widget _buildCard({required String titolo, required Widget child, required Color accentColor}) { return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 10, offset: const Offset(0, 4))]), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(titolo, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: accentColor)), const Divider(height: 25, thickness: 1.2), child ]), ); } @override void dispose() { _data.dispose(); _ora.dispose(); _luogo.dispose(); _testimoni.dispose(); super.dispose(); } } === FILE: lib/comp_15.dart === import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'global_data.dart'; import 'comp_16.dart'; class Comp15Screen extends StatefulWidget { const Comp15Screen({super.key}); @override State createState() => _Comp15ScreenState(); } class _Comp15ScreenState extends State { late List _puntiFirma; late bool isB; bool _isNavigating = false; @override void initState() { super.initState(); isB = GlobalData.latoCorrente == 'B'; _puntiFirma = isB ? List.from(GlobalData.puntiFirmaB) : List.from(GlobalData.puntiFirmaA); // Appena entro, ruoto in orizzontale SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); } // --- PUNTO CHIAVE: LA PULIZIA AUTOMATICA --- @override void dispose() { // Quando questa pagina viene distrutta (in qualsiasi modo), // FORZO immediatamente il ritorno al verticale. SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); super.dispose(); } Future _tornaIndietro() async { _salvaInMemoria(); if (mounted) Navigator.pop(context); } void _salvaInMemoria() { if (isB) { GlobalData.puntiFirmaB = List.from(_puntiFirma); } else { GlobalData.puntiFirmaA = List.from(_puntiFirma); } } Future _confermaEProsegui() async { _salvaInMemoria(); if (_puntiFirma.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("La firma è obbligatoria!"), backgroundColor: Colors.red), ); return; } setState(() => _isNavigating = true); if (mounted) { // Prima di andare alla 16, forzo GIA' il verticale qui. await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); // Piccola pausa per dare tempo all'animazione di rotazione await Future.delayed(const Duration(milliseconds: 100)); if (!mounted) return; // Navigo verso la 16. Uso pushReplacement per distruggere la 15 (e chiamare dispose) // oppure push normale, ma avendo già forzato il portrait sopra siamo sicuri. await Navigator.push( context, MaterialPageRoute(builder: (c) => const Comp16Screen()) ); // --- AGGIUNTA FONDAMENTALE --- // Aspettiamo un attimo. Se stiamo facendo "Cancella tutto", // in questo lasso di tempo la pagina verrà smontata (mounted diventerà false) // e il codice sotto NON verrà eseguito, evitando la trottola. await Future.delayed(const Duration(milliseconds: 300)); // ----------------------------- if (mounted && ModalRoute.of(context)?.isCurrent == true) { await SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); setState(() => _isNavigating = false); } // --------------------------- } } @override Widget build(BuildContext context) { // Ribadisco orizzontale nel build per sicurezza // SystemChrome.setPreferredOrientations([ // DeviceOrientation.landscapeLeft, // DeviceOrientation.landscapeRight, // ]); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (!didPop) await _tornaIndietro(); }, child: Scaffold( backgroundColor: Colors.white, resizeToAvoidBottomInset: false, body: OrientationBuilder( builder: (context, orientation) { double shortestSide = MediaQuery.of(context).size.shortestSide; bool isTablet = shortestSide > 600; // Fix per iPad che potrebbe rimanere verticale if (orientation == Orientation.portrait) { return Center( child: RotatedBox( quarterTurns: 1, child: SizedBox( width: MediaQuery.of(context).size.height, height: MediaQuery.of(context).size.width, child: SafeArea(child: _buildBody(context, isTablet)), ), ), ); } return SafeArea(child: _buildBody(context, isTablet)); }, ), ), ); } Widget _buildBody(BuildContext context, bool isTablet) { Color mainCol = isB ? Colors.amber.shade700 : Colors.blue.shade900; double shortestSide = MediaQuery.of(context).size.shortestSide; double spessoreFirma = (shortestSide * 0.01).clamp(3.0, 8.0); double verticalPadding = isTablet ? MediaQuery.of(context).size.height * 0.12 : 2.0; double horizontalPadding = isTablet ? 80.0 : 5.0; return _isNavigating ? const Center(child: CircularProgressIndicator()) : Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( height: 50, color: mainCol, padding: const EdgeInsets.symmetric(horizontal: 10), child: Row(children: [ IconButton(icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: _tornaIndietro), Expanded(child: Text("15. Firma (${GlobalData.latoCorrente})", textAlign: TextAlign.center, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18))), const SizedBox(width: 48), ]), ), Expanded( child: Padding( padding: EdgeInsets.symmetric(vertical: verticalPadding, horizontal: horizontalPadding), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (isTablet) ...[const Text("Firma nello spazio sottostante:", style: TextStyle(fontSize: 16)), const SizedBox(height: 8)], Expanded( child: Container( width: double.infinity, decoration: BoxDecoration(border: Border.all(color: Colors.grey, width: 2), borderRadius: BorderRadius.circular(10), color: Colors.grey.shade50), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: GestureDetector( onPanUpdate: (d) => setState(() => _puntiFirma.add(d.localPosition)), onPanEnd: (d) => setState(() => _puntiFirma.add(null)), child: RepaintBoundary(child: CustomPaint(painter: FirmaPainter(_puntiFirma, spessoreFirma), size: Size.infinite)), ), ), ), ), ], ), ), ), Padding( padding: EdgeInsets.fromLTRB(20, 5, 20, isTablet ? 15 : 5), child: Row(children: [ Expanded(flex: 1, child: SizedBox(height: 45, child: OutlinedButton.icon(onPressed: () => setState(() => _puntiFirma.clear()), icon: const Icon(Icons.delete, color: Colors.red), label: const Text("CANCELLA", style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold)), style: OutlinedButton.styleFrom(side: const BorderSide(color: Colors.red))))), const SizedBox(width: 20), Expanded(flex: 2, child: SizedBox(height: 45, child: ElevatedButton.icon(onPressed: _confermaEProsegui, style: ElevatedButton.styleFrom(backgroundColor: Colors.green[700], foregroundColor: Colors.white, elevation: 5), icon: const Icon(Icons.check_circle_outline), label: const Text("CONFERMA FIRMA", style: TextStyle(fontWeight: FontWeight.bold))))), ]), ), ], ); } } class FirmaPainter extends CustomPainter { final List punti; final double spessore; FirmaPainter(this.punti, this.spessore); @override void paint(Canvas canvas, Size size) { Paint p = Paint()..color = Colors.black..strokeWidth = spessore..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round..style = PaintingStyle.stroke..isAntiAlias = true; for (int i = 0; i < punti.length - 1; i++) { if (punti[i] != null && punti[i + 1] != null) canvas.drawLine(punti[i]!, punti[i + 1]!, p); } } @override bool shouldRepaint(FirmaPainter old) => true; } === FILE: lib/comp_6-7.dart === import 'package:flutter/material.dart'; import 'package:flutter/services.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 { 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 void _onCfFocusChange() { if (!_cfFocusNode.hasFocus) { String cfInserito = _cf.text.trim().toUpperCase(); if (cfInserito.isNotEmpty) { if (!_isCodiceFiscaleValido(cfInserito)) { 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), ) ); } } } } // 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 _forzaVerticaleEInizializza() async { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await Future.delayed(const Duration(milliseconds: 200)); _initControllers(); if (mounted) { setState(() => _isReady = true); WidgetsBinding.instance.addPostFrameCallback((_) => _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, "/")); } } 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", "Attenzione al Codice Fiscale: il sistema verificherà in automatico se è scritto correttamente!"), 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(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 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 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() { FocusScope.of(context).unfocus(); if (_cognome.text.trim().isEmpty || _nome.text.trim().isEmpty || _cf.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(); if (!_isCodiceFiscaleValido(cfInserito)) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Codice Fiscale NON VALIDO (Errato calcolo finale)."), backgroundColor: Colors.orange, duration: Duration(seconds: 3), ) ); return; } 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; } 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: 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), _buildField(_nome, "Nome *", Icons.person_outline), // Il tuo campo originale, col tuo onChanged e il tuo FocusNode! _buildField( _cf, "Codice Fiscale *", Icons.badge, isCF: true, focusNode: _cfFocusNode, onChanged: _controllaCfInTempoReale ), _buildField(_indirizzo, "Indirizzo *", Icons.home), Row( children: [ Expanded(child: _buildField(_cap, "C.A.P. *", Icons.location_on, isNumeric: true)), const SizedBox(width: 10), Expanded(child: _buildField(_stato, "Stato *", Icons.flag)), ] ), _buildField(_tel, "Tel / Email *", Icons.email), ], ), _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), ), ), ), ], ), ), ), ), ); } // Il tuo widget originale intatto Widget _buildField( TextEditingController controller, String label, IconData icon, { bool isUpper = false, bool isNumeric = false, bool isCF = false, FocusNode? focusNode, Function(String)? onChanged, } ) { List 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'))); } return Padding( padding: const EdgeInsets.only(bottom: 12), child: TextField( controller: controller, focusNode: focusNode, onChanged: onChanged, keyboardType: isNumeric ? TextInputType.number : TextInputType.text, inputFormatters: formatters, 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 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(); } } === FILE: lib/cai_mapping.dart === class CaiMapping { static const Map testi = { // HEADER 'data_incidente': 'data_sinistro', 'ora': 'ora', 'luogo': 'luogo_sinistro', 'testimoni': 'testimoni1', // VEICOLO A 'Cognome_contraente_A': 'cognome_contraenteA', 'Nome_contraente_A': 'nome_contraenteA', 'Codice_Fiscale_contraente_A': 'codice_fiscale_contrA', 'Indirizzo_contraente_A': 'indirizzo_contrA', 'CAP_contraente_A': 'cap_contrA', 'Stato_contraente_A': 'stato_contrA', 'N_telefono_mail_contraente_A': 'tel_contrA', 'Marca_e_Tipo_A': 'marcaA', 'Targa_A': 'targa_veicoloA', 'Stato_immatricolazione_A': 'stato_im_veicoloA', 'Rimorchio_A': 'targa_rimorchioA', 'Stato_immatricolazione2_A': 'stato_im_rimorchioA', 'Denominazione_A': 'COMPAGNIA', 'Numero_Polizza_A': 'numero_polizzaA', 'N_carta_verde_A': 'CVA', 'Data_Inizio_Dal_A': 'ass_dalA', 'Data_Scadenza_Al_A': 'ass_alA', 'Agenzia_A': 'AGENZIA_A', 'Denominazione_agenzia_A': 'denom_A', 'Indirizzo_agenzia_A': 'ind_ag_A', 'Stato_agenzia_A': 'ag_stat_A', 'N_tel_mail_agenzia_A': 'tel_ag_A', 'Cognome_cond_A': 'cogn_cond_A', 'Nome_cond_A': 'nome_cond_A', 'Data_nascita_cond_A': 'dnascita_condA', 'Cod_fiscale_cond_A': 'codice_fiscale_conduA', 'Indirizzo_cond_A': 'indir_conduA', 'Stato_cond_A': 'stato_conduA', 'N_tel_mail_cond_A': 'tel_conduA', 'N_Patente_cond_A': 'n_p_conduA', 'Scadenza_cond_A': 'condu_scad_A', // --- AGGIUNTO CATEGORIA A --- 'Categoria_cond_A': 'cat_A', // <--- VERIFICA QUESTO NOME NEL PDF (es. cat_A, catA, CategA...) 'danni_visibili_A': 'danni_vis_A1', 'osservazioni_A': 'osservazioniA', // VEICOLO B 'Cognome_contraente_B': 'cognome_contraenteB', 'Nome_contraente_B': 'nome_contraenteB', 'Codice_Fiscale_contraente_B': 'codice_fiscale_contrB', 'Indirizzo_contraente_B': 'indirizzo_contrB', 'CAP_contraente_B': 'cap_contrB', 'Stato_contraente_B': 'stato_contrB', 'N_telefono_mail_contraente_B': 'tel_contrB', 'Marca_e_Tipo_B': 'marcaB', 'Targa_B': 'targa_veicoloB', 'Stato_immatricolazione_B': 'stato_im_veicoloB', 'Rimorchio_B': 'targa_rimorchioB', 'Stato_immatricolazione2_B': 'stato_im_rimorchioB', 'Denominazione_B': 'compagnia1', 'Numero_Polizza_B': 'numero_polizzaB', 'N_carta_verde_B': 'CVB', 'Data_Inizio_Dal_B': 'ass_dalB', 'Data_Scadenza_Al_B': 'ass_alB', 'Agenzia_B': 'AGENZIA_B', 'Denominazione_agenzia_B': 'denom_B', 'Indirizzo_agenzia_B': 'ind_ag_B', 'Stato_agenzia_B': 'ag_stat_B', 'N_tel_mail_agenzia_B': 'tel_ag_B', 'Cognome_cond_B': 'cogn_cond_B', 'Nome_cond_B': 'nome_cond_B', 'Data_nascita_cond_B': 'dnascita_condB', 'Cod_fiscale_cond_B': 'codice_fiscale_conduB', 'Indirizzo_cond_B': 'indir_conduB', 'Stato_cond_B': 'stato_conduB', 'N_tel_mail_cond_B': 'tel_conduB', 'N_Patente_cond_B': 'n_p_conduB', 'Scadenza_cond_B': 'condu_scad_B', // --- AGGIUNTO CATEGORIA B --- 'Categoria_cond_B': 'cat_B', // <--- VERIFICA QUESTO NOME NEL PDF 'danni_visibili_B': 'danni_vis_B1', 'osservazioni_B': 'osservazioniB', }; // CHECKBOX static const String feriti_NO = 'x'; static const String feriti_SI = 'y'; static const String danni_veicoli_NO = 'C'; static const String danni_veicoli_SI = 'D'; static const String danni_oggetti_NO = 'A'; static const String danni_oggetti_SI = 'B'; static const String danni_mat_A_NO = 'danni_noA'; static const String danni_mat_A_SI = 'danni_siA'; static const String danni_mat_B_NO = 'danni_noB'; static const String danni_mat_B_SI = 'danni_siB'; // CIRCOSTANZE (Indice 0 vuoto) static const List circostanzeA = [ '', 'A01', 'A02', 'A03', 'A04', 'A05', 'A06', 'A07', 'A08', 'A09', 'A10', 'A11', 'A12', 'A13', 'A14', 'A15', 'A16', 'A17' ]; static const List circostanzeB = [ '', 'B01', 'B02', 'B03', 'B04', 'B05', 'B06', 'B07', 'B08', 'B09', 'B10', 'B11', 'B12', 'B13', 'B14', 'B15', 'B16', 'B17' ]; // TOTALI static const String tot_crocette_A = 'A_tot'; static const String tot_crocette_B = 'B_tot'; // BOX IMMAGINI static const String box_grafico = 'disegno13'; static const String box_firma_A = 'firmaA'; static const String box_firma_B = 'firmaB'; // Opzionali se servono static const String box_urto_A = 'danni_vis_A1'; static const String box_urto_B = 'danni_vis_B1'; } === FILE: lib/temp/pdf_engine.dart === import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/services.dart' show rootBundle; import 'package:syncfusion_flutter_pdf/pdf.dart'; import 'package:flutter/material.dart'; import 'global_data.dart'; import 'models.dart'; import 'cai_mapping.dart'; class PdfEngine { static Future> generaDocumentoCai() async { PdfDocument? document; try { final ByteData data = await rootBundle.load('assets/CAI_p1.pdf'); // 1. Caricamento e Copia final List bytesCopia = data.buffer.asUint8List().toList(); document = PdfDocument(inputBytes: bytesCopia); final PdfForm form = document.form; final PdfPage page = document.pages[0]; form.setDefaultAppearance(false); // Mappatura Map mappaCampi = {}; for (int i = 0; i < form.fields.count; i++) { if (form.fields[i].name != null) { mappaCampi[form.fields[i].name!.trim().toUpperCase()] = form.fields[i]; } } // --- COMPILAZIONE STANDARD (La tua versione preferita) --- // 1. TESTI CaiMapping.testi.forEach((keyGlobal, keyPdf) { String valore = _valoreDaGlobal(keyGlobal); String keyPdfNorm = keyPdf.trim().toUpperCase(); if (mappaCampi.containsKey(keyPdfNorm) && valore.isNotEmpty) { final field = mappaCampi[keyPdfNorm]; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); field.text = valore.toUpperCase(); } } }); // 2. CHECKBOX _scriviX(mappaCampi, [GlobalData.feriti ? CaiMapping.feriti_SI : CaiMapping.feriti_NO]); _scriviX(mappaCampi, [GlobalData.Veicoli_danni_materiali_oltre ? CaiMapping.danni_veicoli_SI : CaiMapping.danni_veicoli_NO]); _scriviX(mappaCampi, [GlobalData.Oggetti_diversi_danni_materiali ? CaiMapping.danni_oggetti_SI : CaiMapping.danni_oggetti_NO]); _scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_A ? CaiMapping.danni_mat_A_SI : CaiMapping.danni_mat_A_NO]); _scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_B ? CaiMapping.danni_mat_B_SI : CaiMapping.danni_mat_B_NO]); String catA = GlobalData.Categoria_cond_A.toUpperCase().trim(); if (catA == 'A') _scriviX(mappaCampi, ['cat_a_A']); else if (catA == 'B') _scriviX(mappaCampi, ['cat_b_A']); else if (catA.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_A'], catA); String catB = GlobalData.Categoria_cond_B.toUpperCase().trim(); if (catB == 'A') _scriviX(mappaCampi, ['cat_a_B']); else if (catB == 'B') _scriviX(mappaCampi, ['cat_b_B']); else if (catB.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_B'], catB); // 3. CIRCOSTANZE int countA = 0; int countB = 0; for (int i = 1; i <= 17; i++) { if (GlobalData.circostanzeA[i] == true) { if (_scriviX(mappaCampi, [i < 10 ? "A0$i" : "A$i"])) countA++; } if (GlobalData.circostanzeB[i] == true) { List nomiTarget = []; if (i == 9) nomiTarget = ["Check Box 26", "CheckBox26", "26"]; else if (i == 10) nomiTarget = ["Check Box 27", "CheckBox27", "27"]; else if (i == 11) nomiTarget = ["Check Box 28", "CheckBox28", "28"]; else if (i == 12) nomiTarget = ["Check Box 29", "CheckBox29", "29"]; else nomiTarget = [i < 10 ? "B0$i" : "B$i"]; if (_scriviX(mappaCampi, nomiTarget)) countB++; } } _scriviTestoTotale(mappaCampi, ['A_TOT', 'A_tot'], countA.toString()); _scriviTestoTotale(mappaCampi, ['B_TOT', 'B_tot'], countB.toString()); // 4. PUNTI URTO for (String punto in GlobalData.puntiUrtoA_List) _scriviXRossa(mappaCampi, [punto]); for (String punto in GlobalData.puntiUrtoB_List) _scriviXRossa(mappaCampi, [punto]); // 5. IMMAGINI await _disegnaInBox(page, mappaCampi, CaiMapping.box_grafico, await _renderGraficoV40(GlobalData.tratti.cast().toList(), GlobalData.elementi.cast().toList())); await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_A, await _renderFirmaTight(GlobalData.puntiFirmaA, Colors.black)); await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_B, await _renderFirmaTight(GlobalData.puntiFirmaB, Colors.black)); // ================================================================= // FASE CRITICA: SALVATAGGIO -> RICARICA -> FLATTEN (Anti-Crash) // ================================================================= // 1. Salviamo il file compilato in memoria. Questo corregge gli errori interni del PDF. List bytesTemporanei = await document.save(); document.dispose(); // Chiudiamo il vecchio // 2. Riapriamo il file "pulito" PdfDocument docFinale = PdfDocument(inputBytes: bytesTemporanei); // 3. Ora eseguiamo il FLATTEN. // È INDISPENSABILE per vedere le X nell'immagine di anteprima. // Poiché il file è stato appena rigenerato, NON DOVREBBE CRASHARE. try { docFinale.form.flattenAllFields(); } catch (e) { debugPrint("⚠️ Errore Flattening anche dopo pulizia: $e"); // Se fallisce ancora, usiamo il fallback ReadOnly, ma l'immagine potrebbe essere incompleta. docFinale.form.readOnly = true; } // 4. Salvataggio finale List bytesFinali = await docFinale.save(); docFinale.dispose(); return bytesFinali; } catch (e) { debugPrint("ERRORE GENERAZIONE PDF: $e"); return []; } } // --- HELPERS (Standard) --- static bool _scriviX(Map mappa, List nomiPossibili) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 14); field.foreColor = PdfColor(0, 0, 0); field.text = "X"; } else if (field is PdfCheckBoxField) { field.isChecked = true; } return true; } } return false; } static void _scriviTesto(Map mappa, List nomiPossibili, String testo) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 10); field.foreColor = PdfColor(0, 0, 0); field.text = testo; } return; } } } static bool _scriviXRossa(Map mappa, List nomiPossibili) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 16, style: PdfFontStyle.bold); field.foreColor = PdfColor(255, 0, 0); field.text = "X"; return true; } } } return false; } static void _scriviTestoTotale(Map mappa, List nomi, String testo) { for (String nome in nomi) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); field.textAlignment = PdfTextAlignment.center; field.text = testo; } return; } } } static Future _disegnaInBox(PdfPage page, Map mappa, String nomeCampo, Uint8List? imgBytes) async { String key = nomeCampo.trim().toUpperCase(); if (imgBytes == null || !mappa.containsKey(key)) return; Rect boxRect = mappa[key]!.bounds; PdfBitmap bitmap = PdfBitmap(imgBytes); double imageW = bitmap.width.toDouble(); double imageH = bitmap.height.toDouble(); if (imageW <= 0 || imageH <= 0) return; double ratioX = boxRect.width / imageW; double ratioY = boxRect.height / imageH; double scale = (ratioX < ratioY) ? ratioX : ratioY; double drawW = imageW * scale; double drawH = imageH * scale; double offsetX = boxRect.left + (boxRect.width - drawW) / 2; double offsetY = boxRect.top + (boxRect.height - drawH) / 2; page.graphics.drawImage(bitmap, Rect.fromLTWH(offsetX, offsetY, drawW, drawH)); } static Future _renderFirmaTight(List punti, Color colore) async { if (punti.isEmpty) return null; double minX = double.infinity, minY = double.infinity, maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (var p in punti) { if (p != null) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } } double padding = 20.0; double firmaW = maxX - minX; double firmaH = maxY - minY; if (firmaW <= 0) firmaW = 1; if (firmaH <= 0) firmaH = 1; double resolutionScale = 3.0; double canvasW = (firmaW + padding * 2) * resolutionScale; double canvasH = (firmaH + padding * 2) * resolutionScale; final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.scale(resolutionScale); canvas.translate(-minX + padding, -minY + padding); final paint = Paint()..color = colore..strokeWidth = 5.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round; for (int i = 0; i < punti.length - 1; i++) { if (punti[i] != null && punti[i+1] != null) { canvas.drawLine(punti[i]!, punti[i+1]!, paint); } } final img = await recorder.endRecording().toImage(canvasW.toInt(), canvasH.toInt()); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } static Future _renderGraficoV40(List tratti, List elementi) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); final size = const Size(2000, 800); final painter = PainterV40(tratti, elementi); painter.paint(canvas, size); final img = await recorder.endRecording().toImage(size.width.toInt(), size.height.toInt()); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } static String _valoreDaGlobal(String key) { switch (key) { case 'data_incidente': return GlobalData.data_incidente; case 'ora': return GlobalData.ora; case 'luogo': return GlobalData.luogo; case 'testimoni': return GlobalData.testimoni; case 'danni_visibili_A': return GlobalData.danni_visibili_A; case 'osservazioni_A': return GlobalData.osservazioni_A; case 'danni_visibili_B': return GlobalData.danni_visibili_B; case 'osservazioni_B': return GlobalData.osservazioni_B; case 'Cognome_contraente_A': return GlobalData.Cognome_contraente_A; case 'Nome_contraente_A': return GlobalData.Nome_contraente_A; case 'Codice_Fiscale_contraente_A': return GlobalData.Codice_Fiscale_contraente_A; case 'Indirizzo_contraente_A': return GlobalData.Indirizzo_contraente_A; case 'CAP_contraente_A': return GlobalData.CAP_contraente_A; case 'Stato_contraente_A': return GlobalData.Stato_contraente_A; case 'N_telefono_mail_contraente_A': return GlobalData.N_telefono_mail_contraente_A; case 'Marca_e_Tipo_A': return GlobalData.Marca_e_Tipo_A; case 'Targa_A': return GlobalData.Targa_A; case 'Stato_immatricolazione_A': return GlobalData.Stato_immatricolazione_A; case 'Rimorchio_A': return GlobalData.Rimorchio_A; case 'Stato_immatricolazione2_A': return GlobalData.Stato_immatricolazione2_A; case 'Denominazione_A': return GlobalData.Denominazione_A; case 'Numero_Polizza_A': return GlobalData.Numero_Polizza_A; case 'N_carta_verde_A': return GlobalData.N_carta_verde_A; case 'Data_Inizio_Dal_A': return GlobalData.Data_Inizio_Dal_A; case 'Data_Scadenza_Al_A': return GlobalData.Data_Scadenza_Al_A; case 'Agenzia_A': return GlobalData.Agenzia_A; case 'Indirizzo_agenzia_A': return GlobalData.Indirizzo_agenzia_A; case 'Stato_agenzia_A': return GlobalData.Stato_agenzia_A; case 'Denominazione_agenzia_A': return GlobalData.Denominazione_agenzia_A; case 'N_tel_mail_agenzia_A': return GlobalData.N_tel_mail_agenzia_A; case 'Cognome_cond_A': return GlobalData.Cognome_cond_A; case 'Nome_cond_A': return GlobalData.Nome_cond_A; case 'Data_nascita_cond_A': return GlobalData.Data_nascita_cond_A; case 'Cod_fiscale_cond_A': return GlobalData.Cod_fiscale_cond_A; case 'Indirizzo_cond_A': return GlobalData.Indirizzo_cond_A; case 'Stato_cond_A': return GlobalData.Stato_cond_A; case 'N_tel_mail_cond_A': return GlobalData.N_tel_mail_cond_A; case 'N_Patente_cond_A': return GlobalData.N_Patente_cond_A; case 'Scadenza_cond_A': return GlobalData.Scadenza_cond_A; case 'Cognome_contraente_B': return GlobalData.Cognome_contraente_B; case 'Nome_contraente_B': return GlobalData.Nome_contraente_B; case 'Codice_Fiscale_contraente_B': return GlobalData.Codice_Fiscale_contraente_B; case 'Indirizzo_contraente_B': return GlobalData.Indirizzo_contraente_B; case 'CAP_contraente_B': return GlobalData.CAP_contraente_B; case 'Stato_contraente_B': return GlobalData.Stato_contraente_B; case 'N_telefono_mail_contraente_B': return GlobalData.N_telefono_mail_contraente_B; case 'Marca_e_Tipo_B': return GlobalData.Marca_e_Tipo_B; case 'Targa_B': return GlobalData.Targa_B; case 'Stato_immatricolazione_B': return GlobalData.Stato_immatricolazione_B; case 'Rimorchio_B': return GlobalData.Rimorchio_B; case 'Stato_immatricolazione2_B': return GlobalData.Stato_immatricolazione2_B; case 'Denominazione_B': return GlobalData.Denominazione_B; case 'Numero_Polizza_B': return GlobalData.Numero_Polizza_B; case 'N_carta_verde_B': return GlobalData.N_carta_verde_B; case 'Data_Inizio_Dal_B': return GlobalData.Data_Inizio_Dal_B; case 'Data_Scadenza_Al_B': return GlobalData.Data_Scadenza_Al_B; case 'Agenzia_B': return GlobalData.Agenzia_B; case 'Indirizzo_agenzia_B': return GlobalData.Indirizzo_agenzia_B; case 'Stato_agenzia_B': return GlobalData.Stato_agenzia_B; case 'Denominazione_agenzia_B': return GlobalData.Denominazione_agenzia_B; case 'N_tel_mail_agenzia_B': return GlobalData.N_tel_mail_agenzia_B; case 'Cognome_cond_B': return GlobalData.Cognome_cond_B; case 'Nome_cond_B': return GlobalData.Nome_cond_B; case 'Data_nascita_cond_B': return GlobalData.Data_nascita_cond_B; case 'Cod_fiscale_cond_B': return GlobalData.Cod_fiscale_cond_B; case 'Indirizzo_cond_B': return GlobalData.Indirizzo_cond_B; case 'Stato_cond_B': return GlobalData.Stato_cond_B; case 'N_tel_mail_cond_B': return GlobalData.N_tel_mail_cond_B; case 'N_Patente_cond_B': return GlobalData.N_Patente_cond_B; case 'Scadenza_cond_B': return GlobalData.Scadenza_cond_B; default: return ""; } } } class PainterV40 extends CustomPainter { final List tr; final List el; PainterV40(this.tr, this.el); final List palette = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red]; @override void paint(Canvas canvas, Size size) { // 1. SFONDO BIANCO (Risolve il problema del nero) final Paint backgroundPaint = Paint()..color = Colors.white; canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint); // 2. DISEGNO GRIGLIA (Opzionale, ma rende il disegno professionale come l'originale) final Paint gridPaint = Paint() ..color = Colors.grey.shade300 ..strokeWidth = 2.0; double step = 40.0; // Dimensione quadretti // Linee verticali for (double x = 0; x <= size.width; x += step) { canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint); } // Linee orizzontali for (double y = 0; y <= size.height; y += step) { canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint); } // Se non ci sono tratti o elementi, ci fermiamo qui (abbiamo disegnato solo sfondo e griglia pulita) if (tr.isEmpty && el.isEmpty) return; // --- CALCOLO BOUNDING BOX PER IL CONTENUTO --- double minX = double.infinity, minY = double.infinity; double maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (var t in tr) { for (var p in t.punti) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } } for (var e in el) { if (e.posizione.dx - 30 < minX) minX = e.posizione.dx - 30; if (e.posizione.dx + 30 > maxX) maxX = e.posizione.dx + 30; if (e.posizione.dy - 30 < minY) minY = e.posizione.dy - 30; if (e.posizione.dy + 30 > maxY) maxY = e.posizione.dy + 30; } // Se non abbiamo trovato nulla (caso raro), usiamo valori di default if (minX == double.infinity) { minX = 0; maxX = 100; minY = 0; maxY = 100; } double padding = 40.0; double drawingW = maxX - minX + (padding * 2); double drawingH = maxY - minY + (padding * 2); if (drawingW <= 0) drawingW = 100; if (drawingH <= 0) drawingH = 100; // Scala per adattare il disegno al box (Contain) double scaleX = size.width / drawingW; double scaleY = size.height / drawingH; double scale = (scaleX < scaleY) ? scaleX : scaleY; // Centratura double offsetX = (size.width - (drawingW * scale)) / 2; double offsetY = (size.height - (drawingH * scale)) / 2; canvas.save(); canvas.translate(offsetX, offsetY); canvas.scale(scale); canvas.translate(-minX + padding, -minY + padding); // --- DISEGNO STRADE E FRECCE --- Paint pStrada = Paint() ..color = Colors.black ..strokeWidth = 4.0 / scale ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; for (var t in tr) { if (t.punti.length > 1) { Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy); for (var pt in t.punti) path.lineTo(pt.dx, pt.dy); canvas.drawPath(path, pStrada); if (t.tipo == 'freccia') { double a = (t.punti.last - t.punti[t.punti.length - 2]).direction; canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a - 0.5, 15), pStrada); canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a + 0.5, 15), pStrada); } } } // --- DISEGNO AUTO E TESTI --- for (var e in el) { canvas.save(); canvas.translate(e.posizione.dx, e.posizione.dy); canvas.rotate(e.rotazione); if (e.tipo == 'testo') { final tp = TextPainter( text: TextSpan(text: e.label ?? "", style: const TextStyle(color: Colors.black, fontSize: 24, fontWeight: FontWeight.bold)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } else if (e.tipo.startsWith('auto')) { int idx = ((e.label ?? "A").codeUnitAt(0) - 65) % palette.length; Paint p = Paint()..color = palette[idx]; // Corpo auto colorato canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: 60, height: 30), p); // Bordo auto nero canvas.drawRect(Rect.fromCenter(center: Offset.zero, width: 60, height: 30), Paint()..style=PaintingStyle.stroke..color=Colors.black..strokeWidth=2); // Lettera A/B Bianca final tp = TextPainter( text: TextSpan(text: e.label ?? "A", style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 20)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } canvas.restore(); } canvas.restore(); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } === FILE: lib/temp/comp_16.dart === import 'dart:io'; import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:path_provider/path_provider.dart'; import 'package:printing/printing.dart'; import 'package:share_plus/share_plus.dart'; import 'scambio_dati_screen.dart'; import 'pdf_engine.dart'; import 'global_data.dart'; import 'main.dart'; import 'comp_6-7.dart'; import 'comp_1-5.dart'; class Comp16Screen extends StatefulWidget { const Comp16Screen({super.key}); @override State createState() => _Comp16ScreenState(); } class _Comp16ScreenState extends State with WidgetsBindingObserver { bool _scambioEffettuato = false; bool _datiPresenti = false; bool _ioHoApprovato = false; bool _tuttiHannoApprovato = false; bool _staCancellando = false; bool _cancellazioneAvviataDaMe = false; String _statusText = "Esegui lo Scambio Dati per iniziare."; Color _statusColor = Colors.orange.shade800; IconData _statusIcon = Icons.warning_amber_rounded; File? _filePdfReale; Uint8List? _immagineAnteprima; bool _isLoading = false; StreamSubscription? _roomSubscription; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); _puliziaIngresso(); } Future _puliziaIngresso() async { if (GlobalData.idScambioTemporaneo == null && GlobalData.idSessione != null) { GlobalData.idScambioTemporaneo = GlobalData.idSessione; } if (GlobalData.idScambioTemporaneo == null) { if (GlobalData.latoCorrente == 'A') GlobalData.resetB(); else GlobalData.resetA(); } if (mounted) _verificaStatoPostScambio(); } void _verificaStatoPostScambio() { bool datiOk = false; if (GlobalData.latoCorrente == 'A') { datiOk = GlobalData.Cognome_contraente_B.trim().isNotEmpty && GlobalData.Targa_B.trim().isNotEmpty; } else { datiOk = GlobalData.Cognome_contraente_A.trim().isNotEmpty && GlobalData.Targa_A.trim().isNotEmpty; } if (mounted) { setState(() { if (datiOk) { _scambioEffettuato = true; _datiPresenti = true; _statusText = "Dati ricevuti. Generazione anteprima..."; _statusColor = Colors.blue.shade800; _statusIcon = Icons.pending_actions; _generaDocumenti(); _attivaAscoltoStanza(); } else { _resetStatiUI(); } }); } } void _resetStatiUI() { _scambioEffettuato = false; _datiPresenti = false; _ioHoApprovato = false; _tuttiHannoApprovato = false; _statusText = "Esegui lo Scambio Dati per iniziare."; _statusColor = Colors.orange.shade800; _statusIcon = Icons.warning_amber_rounded; _filePdfReale = null; _immagineAnteprima = null; } void _attivaAscoltoStanza() { String? idDaAscoltare = GlobalData.idScambioTemporaneo ?? GlobalData.idSessione; if (idDaAscoltare == null || _roomSubscription != null) return; _roomSubscription = FirebaseFirestore.instance .collection('scambi_cid') .doc(idDaAscoltare) .snapshots() .listen((snapshot) async { if (!snapshot.exists) { if (_ioHoApprovato) { _roomSubscription?.cancel(); _roomSubscription = null; return; } if (!_staCancellando && !_cancellazioneAvviataDaMe && mounted) { _gestisciCancellazioneAltrui(); } return; } final data = snapshot.data(); if (data == null) return; if (data['status'] == 'retry') { if (!_cancellazioneAvviataDaMe) _gestisciCancellazioneAltrui(); return; } bool appA = data['approved_A'] == true; bool appB = data['approved_B'] == true; if (appA && appB) { if (mounted) { setState(() { _tuttiHannoApprovato = true; _ioHoApprovato = true; _statusText = "DATI APPROVATI!\nPDF creato procedi con il salvataggio sul dispositivo o invialo"; _statusColor = Colors.green.shade800; _statusIcon = Icons.check_circle; }); String? id = GlobalData.idSessione ?? GlobalData.idScambioTemporaneo; if (id != null) FirebaseFirestore.instance.collection('scambi_cid').doc(id).delete().catchError((_){}); } } else if (_ioHoApprovato) { if (mounted) { setState(() { _statusText = "Hai approvato. In attesa dell'altro utente..."; _statusColor = Colors.amber.shade800; _statusIcon = Icons.hourglass_top; }); } } }); } // =========================================================================== // GESTIONE CANCELLAZIONE // =========================================================================== Future _eseguiPuliziaFirebase({required bool notificaAltri}) async { setState(() { _isLoading = true; _cancellazioneAvviataDaMe = true; }); await _roomSubscription?.cancel(); _roomSubscription = null; Set idsDaCancellare = {}; if (GlobalData.idScambioTemporaneo != null) idsDaCancellare.add(GlobalData.idScambioTemporaneo!); if (GlobalData.idSessione != null) idsDaCancellare.add(GlobalData.idSessione!); for (String id in idsDaCancellare) { if (notificaAltri) { try { await FirebaseFirestore.instance.collection('scambi_cid').doc(id).update({'status': 'retry'}) .timeout(const Duration(seconds: 2)); await Future.delayed(const Duration(milliseconds: 300)); } catch (_) {} } try { await FirebaseFirestore.instance.collection('scambi_cid').doc(id).delete(); } catch (_) {} } } Future _tornaIndietroConPulizia() async { await _eseguiPuliziaFirebase(notificaAltri: true); _resetDatiLocali(); if (mounted) { setState(() => _isLoading = false); if (GlobalData.latoCorrente == 'A') { Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp1_5Screen())); } else { Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp6_7Screen())); } } } Future _ioApprovo() async { String? id = GlobalData.idSessione ?? GlobalData.idScambioTemporaneo; if (id != null) { try { String field = (GlobalData.latoCorrente == 'A') ? 'approved_A' : 'approved_B'; await FirebaseFirestore.instance.collection('scambi_cid').doc(id).update({field: true}); } catch (_) {} } if (mounted) { setState(() { _ioHoApprovato = true; }); } } Future _concludiEHome() async { await _eseguiPuliziaFirebase(notificaAltri: false); GlobalData.reset(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); if (mounted) { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (c) => const HomeScreen()), (r) => false); } } void _resetDatiLocali() { if (GlobalData.latoCorrente == 'A') GlobalData.resetB(); else GlobalData.resetA(); GlobalData.idScambioTemporaneo = null; GlobalData.idSessione = null; } void _gestisciCancellazioneAltrui() { _roomSubscription?.cancel(); _roomSubscription = null; if (mounted) { // Chiude eventuali altri dialoghi aperti (es. quello dell'anteprima) Navigator.of(context).popUntil((route) => route.isFirst || route.settings.name == null); showDialog( context: context, barrierDismissible: false, // L'utente DEVE premere il tasto builder: (ctx) => AlertDialog( // 1. Forma moderna con angoli molto arrotondati shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.0)), backgroundColor: Colors.white, surfaceTintColor: Colors.transparent, // Evita tinte strane su Material 3 // 2. Icona grande in cima al titolo icon: Icon( Icons.warning_amber_rounded, size: 60, color: Colors.amber.shade800 ), iconPadding: const EdgeInsets.only(top: 24, bottom: 16), // 3. Titolo in grassetto title: Text( "Attenzione", textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 22, color: Colors.amber.shade900 ) ), // 4. Contenuto con il tuo testo, centrato e leggibile content: const Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Text( "L'altro utente ha deciso di modificare i propri dati o non ha accettato i tuoi.\n\nSarai riportato alla schermata iniziale dove potrai eventualmente apporre modifiche.", textAlign: TextAlign.center, style: TextStyle(fontSize: 16, height: 1.4, color: Colors.black87), ), ), // 5. Spaziatura azioni actionsPadding: const EdgeInsets.fromLTRB(24, 0, 24, 24), actions: [ // 6. Pulsante moderno full-width SizedBox( width: double.infinity, // Occupa tutta la larghezza child: ElevatedButton( onPressed: () { Navigator.pop(ctx); // Chiude il dialog _resetDatiLocali(); // Pulisce la RAM // Torna alla schermata di input corretta if (GlobalData.latoCorrente == 'A') { Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp1_5Screen())); } else { Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp6_7Screen())); } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.amber.shade800, // Colore coerente con l'icona foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0, // Look più piatto e moderno ), child: const Text( "HO CAPITO", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16) ) ), ) ], ), ); } } Future _generaDocumenti() async { if (!mounted) return; setState(() => _isLoading = true); try { final List pdfBytes = await PdfEngine.generaDocumentoCai(); if (pdfBytes.isEmpty) throw Exception("PDF vuoto"); final tempDir = await getTemporaryDirectory(); final file = File('${tempDir.path}/CID_${DateTime.now().millisecondsSinceEpoch}.pdf'); await file.writeAsBytes(pdfBytes, flush: true); Uint8List? anteprima; await for (final page in Printing.raster(Uint8List.fromList(pdfBytes), pages: [0], dpi: 150)) { anteprima = await page.toPng(); break; } if (mounted) { setState(() { _filePdfReale = file; _immagineAnteprima = anteprima; _isLoading = false; }); } } catch (e) { if (mounted) setState(() => _isLoading = false); } } Future _vaiAScambioDati() async { await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScambioDatiScreen())); _verificaStatoPostScambio(); } void _apriAnteprimaSchermoIntero() { if (!_scambioEffettuato || !_datiPresenti || _immagineAnteprima == null || _filePdfReale == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Dati non pronti!"))); return; } Navigator.push(context, MaterialPageRoute(builder: (context) => ImageViewerScreen( imageBytes: _immagineAnteprima!, pdfFile: _filePdfReale!, isAlreadyApproved: _ioHoApprovato, onConfirmCorrection: _tornaIndietroConPulizia, onConfirmApproval: _ioApprovo ))); } Future _inviaMailConAllegato(BuildContext context) async { if (_filePdfReale == null) return; try { bool isA = GlobalData.latoCorrente == 'A'; String polizzaChiScrive = (isA ? GlobalData.Numero_Polizza_A : GlobalData.Numero_Polizza_B).trim(); String targaChiScrive = (isA ? GlobalData.Targa_A : GlobalData.Targa_B).trim(); String firmaChiScrive = "${isA ? GlobalData.Nome_contraente_A : GlobalData.Nome_contraente_B} ${isA ? GlobalData.Cognome_contraente_A : GlobalData.Cognome_contraente_B}"; String contattoChiScrive = (isA ? GlobalData.N_telefono_mail_contraente_A : GlobalData.N_telefono_mail_contraente_B).trim(); String compagniaUtente = (isA ? GlobalData.Denominazione_A : GlobalData.Denominazione_B).trim().toUpperCase(); String emailDestinatario = ""; if (GlobalData.assicurazioni.containsKey(compagniaUtente)) { emailDestinatario = GlobalData.assicurazioni[compagniaUtente]!; } else { for (var key in GlobalData.assicurazioni.keys) { if (key.isNotEmpty && (compagniaUtente.contains(key) || key.contains(compagniaUtente))) { emailDestinatario = GlobalData.assicurazioni[key]!; break; } } } List listaCC = []; if (contattoChiScrive.contains("@")) listaCC.add(contattoChiScrive); String oggetto = "DENUNCIA SINISTRO - Polizza n. $polizzaChiScrive - Targa $targaChiScrive"; String corpo = "Spett.le Compagnia,\n\n" "Con la presente inoltro in allegato il modulo CAI relativo al sinistro avvenuto in data ${GlobalData.data_incidente} alle ore ${GlobalData.ora} nel comune di ${GlobalData.luogo}.\n\n" "Rimaniamo in attesa dell'apertura del fascicolo.\n\n" "Cordiali saluti,\n$firmaChiScrive\nContatto: $contattoChiScrive"; final Email email = Email( subject: oggetto, body: corpo, recipients: emailDestinatario.isNotEmpty ? [emailDestinatario] : [], cc: listaCC, attachmentPaths: [_filePdfReale!.path], isHTML: false, ); await FlutterEmailSender.send(email); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore mail: $e"))); } } Future _salvaPdfLocale(BuildContext context) async { if (_filePdfReale == null) return; await Share.shareXFiles([XFile(_filePdfReale!.path, mimeType: 'application/pdf')], subject: 'Modulo CAI', text: 'Ecco il modulo CAI compilato.'); } @override void dispose() { _roomSubscription?.cancel(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override Widget build(BuildContext context) { bool pdfPronto = !_isLoading && _filePdfReale != null && _immagineAnteprima != null; bool abilitaAnteprima = _scambioEffettuato && _datiPresenti && pdfPronto; bool abilitaFinali = _tuttiHannoApprovato && pdfPronto; String testoAnteprima = !_scambioEffettuato ? "2. ANTEPRIMA (Prima fai Scambio)" : (_ioHoApprovato ? "ANTEPRIMA (IN ATTESA...)" : "2. APRI ANTEPRIMA E APPROVA"); if (_tuttiHannoApprovato) testoAnteprima = "ANTEPRIMA (COMPLETATA)"; return PopScope( canPop: false, onPopInvoked: (didPop) async { if (didPop) return; if (_ioHoApprovato) _concludiEHome(); else _tornaIndietroConPulizia(); }, child: Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( title: const Text("Invio e Salvataggio", style: TextStyle(fontWeight: FontWeight.w800, fontSize: 20)), centerTitle: true, backgroundColor: Colors.blue.shade900.withOpacity(0.95), foregroundColor: Colors.white, elevation: 10, leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: _ioHoApprovato ? _concludiEHome : _tornaIndietroConPulizia ), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(20))), ), body: Stack(children: [ Positioned.fill(child: Comp16BackgroundImage()), SafeArea(child: SingleChildScrollView(padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildStatusCard(), const SizedBox(height: 20), _btn("1. SCAMBIO DATI (QR CODE)", Icons.qr_code_scanner, Colors.orange.shade800, onTap: _vaiAScambioDati, disabled: _ioHoApprovato), const SizedBox(height: 20), _btn(testoAnteprima, Icons.visibility, _statusColor, onTap: abilitaAnteprima ? _apriAnteprimaSchermoIntero : null, disabled: !abilitaAnteprima), const SizedBox(height: 8), Divider(color: Colors.white.withOpacity(0.5), thickness: 1), const SizedBox(height: 8), Builder(builder: (ctx) => _btn("SALVA SUL DISPOSITIVO", Icons.save_alt, Colors.green.shade700, onTap: abilitaFinali ? () => _salvaPdfLocale(ctx) : null, disabled: !abilitaFinali)), const SizedBox(height: 20), Builder(builder: (ctx) => _btn("INVIA ALL'ASSICURAZIONE", Icons.send_rounded, Colors.green.shade700, onTap: abilitaFinali ? () => _inviaMailConAllegato(ctx) : null, disabled: !abilitaFinali)), const SizedBox(height: 40), _btn( _tuttiHannoApprovato ? "TORNA ALLA HOME" : "CANCELLA TUTTO E ESCI", _tuttiHannoApprovato ? Icons.home : Icons.delete_sweep, _tuttiHannoApprovato ? Colors.green.shade800 : Colors.red.shade900, onTap: _tuttiHannoApprovato ? _concludiEHome : _tornaIndietroConPulizia, disabled: false ), const SizedBox(height: 30) ]))), if (_isLoading) Container(color: Colors.black54, child: const Center(child: Column(mainAxisSize: MainAxisSize.min, children: [CircularProgressIndicator(color: Colors.white), SizedBox(height: 20), Text("Elaborazione in corso...", style: TextStyle(color: Colors.white))]))), ]), ), ); } Widget _btn(String label, IconData icon, Color color, {VoidCallback? onTap, bool disabled = false}) { bool on = onTap != null && !disabled; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: on ? [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(0, 4), blurRadius: 5)] : [], ), child: ElevatedButton( onPressed: on ? onTap : null, style: ElevatedButton.styleFrom( backgroundColor: on ? color : Colors.grey, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), child: Row(children: [ Icon(icon, size: 28), const SizedBox(width: 20), Expanded(child: Text(label, textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), Icon(Icons.lock, size: 20, color: Colors.transparent) ]) ), ); } Widget _buildStatusCard() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(16), border: Border.all(color: _statusColor, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 6, offset: const Offset(0, 3))]), child: Row(children: [ Icon(_statusIcon, color: _statusColor, size: 36), const SizedBox(width: 15), Expanded(child: Text(_statusText, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: _statusColor))) ]) ); } } // Widget Sfondo Modificato: Ingrandito (1.3x) e Spostato in basso (+100px) class Comp16BackgroundImage extends StatelessWidget { const Comp16BackgroundImage({super.key}); @override Widget build(BuildContext context) { return Container( height: double.infinity, width: double.infinity, color: const Color(0xFFF0F4F8), child: Transform.translate( offset: const Offset(0, 100), // Sposta in basso di 100px child: Transform.scale( scale: 1.3, // Ingrandisce del 30% child: Image.asset( 'assets/sfondo_mappa.jpg', fit: BoxFit.cover, alignment: Alignment.center, // Centrale per zoomare uniforme color: const Color(0xFFF0F4F8).withOpacity(0.6), colorBlendMode: BlendMode.lighten, errorBuilder: (c, e, s) => Container(color: Colors.grey.shade200), ), ), ), ); } } class ImageViewerScreen extends StatelessWidget { final Uint8List imageBytes; final File pdfFile; final bool isAlreadyApproved; final Function onConfirmCorrection; final Function onConfirmApproval; const ImageViewerScreen({super.key, required this.imageBytes, required this.pdfFile, required this.isAlreadyApproved, required this.onConfirmCorrection, required this.onConfirmApproval}); Future _askCorrection(BuildContext context) async { String titolo = isAlreadyApproved ? "Chiudere?" : "Richiedere correzione?"; String testo = isAlreadyApproved ? "Hai già approvato. Uscendo tornerai alla schermata precedente in attesa dell'altro utente." : "Questo annullerà lo scambio per entrambi e vi riporterà alla modifica."; String tasto = isAlreadyApproved ? "CHIUDI" : "CORREGGI"; bool? conf = await showDialog(context: context, builder: (c) => AlertDialog( title: Text(titolo), content: Text(testo), actions: [ TextButton(onPressed: () => Navigator.pop(c, false), child: const Text("ANNULLA")), ElevatedButton(onPressed: () => Navigator.pop(c, true), child: Text(tasto)) ] )); if (conf == true) { Navigator.pop(context); if (!isAlreadyApproved) onConfirmCorrection(); } } Future _askApproval(BuildContext context) async { Navigator.pop(context); onConfirmApproval(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar(title: const Text("Verifica Dati"), backgroundColor: Colors.black, foregroundColor: Colors.white), body: Column(children: [ Expanded(child: InteractiveViewer(minScale: 0.5, maxScale: 4.0, child: Center(child: Container(color: Colors.white, child: Image.memory(imageBytes, fit: BoxFit.contain))))), Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration(color: Colors.white, boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: const Offset(0, -2))]), child: SafeArea( child: Row(children: [ Expanded( child: ElevatedButton.icon( onPressed: () => _askCorrection(context), icon: Icon(isAlreadyApproved ? Icons.arrow_back : Icons.edit), label: Text(isAlreadyApproved ? "INDIETRO" : "CORREGGI"), style: ElevatedButton.styleFrom(backgroundColor: Colors.orange.shade800, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton.icon( onPressed: isAlreadyApproved ? null : () => _askApproval(context), icon: isAlreadyApproved ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.check_circle), label: Text(isAlreadyApproved ? "IN ATTESA..." : "APPROVA"), style: ElevatedButton.styleFrom(backgroundColor: isAlreadyApproved ? Colors.grey : Colors.green.shade700, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), ), ), ]), ), ) ]), ); } } === FILE: lib/global_data.dart === import 'dart:ui'; import 'package:cid_app/models.dart'; class GlobalData { // --- VARIABILI DI SESSIONE --- static String latoCorrente = 'A'; static String? idScambioTemporaneo; static String? chiaveSegretaCorrente; static String? idSessione; // --- DATI GENERALI (NON CANCELLATI DAI RESET PARZIALI) --- static String data_incidente = ""; static String ora = ""; static String luogo = ""; static String testimoni = ""; static bool feriti = false; static bool Veicoli_danni_materiali_oltre = false; static bool Oggetti_diversi_danni_materiali = false; // --- DATI LATO A (BLU) --- static String Cognome_contraente_A = ""; static String Nome_contraente_A = ""; static String Codice_Fiscale_contraente_A = ""; static String Indirizzo_contraente_A = ""; static String CAP_contraente_A = ""; static String Stato_contraente_A = ""; static String N_telefono_mail_contraente_A = ""; static String Marca_e_Tipo_A = ""; static String Targa_A = ""; static String Stato_immatricolazione_A = ""; static String Rimorchio_A = ""; static String Stato_immatricolazione2_A = ""; static String Denominazione_A = ""; static String Numero_Polizza_A = ""; static String N_carta_verde_A = ""; static String Data_Inizio_Dal_A = ""; static String Data_Scadenza_Al_A = ""; static String Agenzia_A = ""; static String Denominazione_agenzia_A = ""; static String Indirizzo_agenzia_A = ""; static String Stato_agenzia_A = ""; static String N_tel_mail_agenzia_A = ""; static bool FLAG_danni_mat_assicurati_A = false; static String Cognome_cond_A = ""; static String Nome_cond_A = ""; static String Data_nascita_cond_A = ""; static String Cod_fiscale_cond_A = ""; static String Indirizzo_cond_A = ""; static String Stato_cond_A = ""; static String N_tel_mail_cond_A = ""; static String N_Patente_cond_A = ""; static String Scadenza_cond_A = ""; static String Categoria_cond_A = ""; static List puntiUrtoA_List = []; static String danni_visibili_A = ""; static String osservazioni_A = ""; static Map circostanzeA = {}; static int totaleCrocetteA = 0; static List puntiFirmaA = []; // --- DATI LATO B (GIALLO) --- static String Cognome_contraente_B = ""; static String Nome_contraente_B = ""; static String Codice_Fiscale_contraente_B = ""; static String Indirizzo_contraente_B = ""; static String CAP_contraente_B = ""; static String Stato_contraente_B = ""; static String N_telefono_mail_contraente_B = ""; static String Marca_e_Tipo_B = ""; static String Targa_B = ""; static String Stato_immatricolazione_B = ""; static String Rimorchio_B = ""; static String Stato_immatricolazione2_B = ""; static String Denominazione_B = ""; static String Numero_Polizza_B = ""; static String N_carta_verde_B = ""; static String Data_Inizio_Dal_B = ""; static String Data_Scadenza_Al_B = ""; static String Agenzia_B = ""; static String Denominazione_agenzia_B = ""; static String Indirizzo_agenzia_B = ""; static String Stato_agenzia_B = ""; static String N_tel_mail_agenzia_B = ""; static bool FLAG_danni_mat_assicurati_B = false; static String Cognome_cond_B = ""; static String Nome_cond_B = ""; static String Data_nascita_cond_B = ""; static String Cod_fiscale_cond_B = ""; static String Indirizzo_cond_B = ""; static String Stato_cond_B = ""; static String N_tel_mail_cond_B = ""; static String N_Patente_cond_B = ""; static String Scadenza_cond_B = ""; static String Categoria_cond_B = ""; static List puntiUrtoB_List = []; static String danni_visibili_B = ""; static String osservazioni_B = ""; static Map circostanzeB = {}; static int totaleCrocetteB = 0; static List puntiFirmaB = []; // --- DATI GRAFICI --- static List tratti = []; static List elementi = []; static Map assicurazioni = { "AEGON": "aegon@pec.aegon.it", "AIG EUROPE": "insurance@aigeurope.postecert.it", "AIG LIFE": "aiglifestabile@pec.it", "ALLIANZ": "allianz.spa@pec.allianz.it", "ALLIANZ DIRECT": "allianzdirect@pec.allianzdirect.it", "ALLEANZA": "alleanza@pec.alleanza.it", "AMISSIMA": "amissima@pec.it", "AMTRUST ASSICURAZIONI": "amtrust.assicurazioni@pec.it", "ARAG": "arag@legalmail.it", "ARCA": "arcaassicurazioni@pec.unipol.it", "ASSICURATRICE MILANESE": "assicuratricemilanese@legalmail.it", "ASSIMOCO": "assimoco@legalmail.it", "ATHORA ITALIA": "athoraitalia@legalmail.it", "AVIVA": "aviva@pec.aviva.it", "AXA": "axaassicurazioni@axa.legalmail.it", "BCC ASSICURAZIONI": "bccassicurazioni@pec.it", "BCC VITA": "bccvita@legalmail.it", "BENE ASSICURAZIONI": "beneassicurazioni@legalmail.it", "BEREBEL": "berebel@pec.unipol.it", "BNP PARIBAS CARDIF": "cardif.assicurazioni@pec.bnpparibas.com", "CARIGE ASSICURAZIONI": "carigeassicurazioni@pec.it", "CATTOLICA": "cattolica.assicurazioni@pec.gruppocattolica.it", "CF ASSICURAZIONI": "cfassicurazioni@pec.it", "CHUBB": "chubb.italy@pec.chubb.com", "CNP VITA ASSICURA": "cnpvitaassicura@pec.it", "CONTE.IT": "admiralinteractive@legalmail.it", "CREDIT AGRICOLE": "creditagricoleassicurazioni@pec.ca-assurances.it", "CRONOS VITA": "cronosvita@legalmail.it", "DARAG ITALIA": "darag.italia@legalmail.it", "DAS": "das@legalmail.it", "DONAU": "donau@pec.it", "ERGO ASSICURAZIONI": "ergoassicurazioni@legalmail.it", "EUROHERC": "euroherc@legalmail.it", "EUROP ASSISTANCE": "europassistance@pec.europassistance.it", "FIDEURAM VITA": "fideuramvita@pec.fideuram.it", "GENERALI ITALIA": "generaliitalia@pec.generaligroup.com", "GENERTEL": "genertel@pec.genertel.it", "GIOTTO ASSICURAZIONI": "giottoassicurazioni@pec.it", "GLOBAL ASSISTANCE": "globalassistancespa@legalmail.it", "GREAT LAKES": "greatlakes@legalmail.it", "GROUPAMA": "groupama@legalmail.it", "HDI": "hdi.assicurazioni@pec.hdia.it", "HELVETIA": "helvetia@actaliscertymail.it", "INCONTRA ASSICURAZIONI": "incontraassicurazioni@pec.it", "INTESA SANPAOLO": "intesasanpaoloassicura@pec.intesasanpaolo.com", "INTESA SANPAOLO RBM SALUTE": "rbmsalute@pec.rbmsalute.it", "ITALIANA ASSICURAZIONI": "italiana@pec.italiana.it", "ITAS": "itas.mutua@pec-gruppoitas.it", "LINEAR": "linear@pec.unipol.it", "MAPFRE": "mapfreassicurazioni@pec.it", "MEDIOLANUM ASSICURAZIONI": "mediolanumassicurazioni@pec.mediolanum.it", "METLIFE": "metlife@pec.metlife.it", "NET INSURANCE": "netinsurance@legalmail.it", "NOBIS ASSICURAZIONI": "nobisassicurazioni@pec.it", "POSTE ASSICURA": "posteassicura@pec.posteassicura.it", "POSTE VITA": "postevita@pec.postevita.it", "PRIMA.IT": "prima@pec.prima.it", "QBE INSURANCE": "qbeitaly@pec.qbe.com", "QUIXA": "quixa.assicurazioni@legalmail.it", "REALE MUTUA": "realemutua@pec.realemutua.it", "SARA": "saraassicurazioni@sara.telecompost.it", "SOGESSUR": "sogessur@pec.it", "SWISS RE": "swissre@pec.swissre.com", "TELEPASS ASSICURA": "telepassassicura@pec.telepass.com", "TOKIO MARINE EUROPE": "tokiomareineeurope@legalmail.it", "TUA": "tuaassicurazioni@pec.it", "UNIQA": "uniqa@pec.uniqa.it", "UNIPOLSAI": "unipolsaiassicurazioni@pec.unipol.it", "VERTI": "verti@pec.verti.it", "VIENNA INSURANCE (WIENER)": "wieneritalia@legalmail.it", "VITTORIA": "vittoriaassicurazioni@pec.vittoriaassicurazioni.it", "WAKAM": "wakam@pec.it", "XL INSURANCE": "xlinsurance@legalmail.it", "ZURICH": "zurich.insurance.company@pec.zurich.it", "ALTRO (Inserimento manuale)": "" }; // --- RESET TOTALE --- static void reset() { latoCorrente = 'A'; data_incidente = ""; ora = ""; luogo = ""; testimoni = ""; feriti = false; Veicoli_danni_materiali_oltre = false; Oggetti_diversi_danni_materiali = false; resetA(); resetB(); elementi = []; tratti = []; idSessione = null; chiaveSegretaCorrente = null; idScambioTemporaneo = null; } // --- RESET PARZIALE --- static void resetSoloLatoOpposto() { if (latoCorrente == 'A') { resetB(); } else { resetA(); } idScambioTemporaneo = null; chiaveSegretaCorrente = null; } static void resetA() { Cognome_contraente_A = ""; Nome_contraente_A = ""; Codice_Fiscale_contraente_A = ""; Indirizzo_contraente_A = ""; CAP_contraente_A = ""; Stato_contraente_A = ""; N_telefono_mail_contraente_A = ""; Marca_e_Tipo_A = ""; Targa_A = ""; Stato_immatricolazione_A = ""; Rimorchio_A = ""; Stato_immatricolazione2_A = ""; Denominazione_A = ""; Numero_Polizza_A = ""; N_carta_verde_A = ""; Data_Inizio_Dal_A = ""; Data_Scadenza_Al_A = ""; Agenzia_A = ""; Denominazione_agenzia_A = ""; Indirizzo_agenzia_A = ""; Stato_agenzia_A = ""; N_tel_mail_agenzia_A = ""; FLAG_danni_mat_assicurati_A = false; Cognome_cond_A = ""; Nome_cond_A = ""; Data_nascita_cond_A = ""; Cod_fiscale_cond_A = ""; Indirizzo_cond_A = ""; Stato_cond_A = ""; N_tel_mail_cond_A = ""; N_Patente_cond_A = ""; Scadenza_cond_A = ""; Categoria_cond_A = ""; puntiUrtoA_List = []; danni_visibili_A = ""; osservazioni_A = ""; circostanzeA = {}; totaleCrocetteA = 0; puntiFirmaA = []; } static void resetB() { Cognome_contraente_B = ""; Nome_contraente_B = ""; Codice_Fiscale_contraente_B = ""; Indirizzo_contraente_B = ""; CAP_contraente_B = ""; Stato_contraente_B = ""; N_telefono_mail_contraente_B = ""; Marca_e_Tipo_B = ""; Targa_B = ""; Stato_immatricolazione_B = ""; Rimorchio_B = ""; Stato_immatricolazione2_B = ""; Denominazione_B = ""; Numero_Polizza_B = ""; N_carta_verde_B = ""; Data_Inizio_Dal_B = ""; Data_Scadenza_Al_B = ""; Agenzia_B = ""; Denominazione_agenzia_B = ""; Indirizzo_agenzia_B = ""; Stato_agenzia_B = ""; N_tel_mail_agenzia_B = ""; FLAG_danni_mat_assicurati_B = false; Cognome_cond_B = ""; Nome_cond_B = ""; Data_nascita_cond_B = ""; Cod_fiscale_cond_B = ""; Indirizzo_cond_B = ""; Stato_cond_B = ""; N_tel_mail_cond_B = ""; N_Patente_cond_B = ""; Scadenza_cond_B = ""; Categoria_cond_B = ""; puntiUrtoB_List = []; danni_visibili_B = ""; osservazioni_B = ""; circostanzeB = {}; totaleCrocetteB = 0; puntiFirmaB = []; } // --- DEBUG COMPLETO (Tutti i campi popolati) --- static void popolaDatiDiTest() { idScambioTemporaneo = null; // Header data_incidente = "01/01/2024"; ora = "12:30"; luogo = "ROMA, VIA DEL CORSO 10"; testimoni = "SIG. BIANCHI GIOVANNI, VIA VERDI 5, MILANO"; feriti = false; Veicoli_danni_materiali_oltre = false; Oggetti_diversi_danni_materiali = true; if (latoCorrente == 'A') { // Dati A Cognome_contraente_A = "ROSSI"; Nome_contraente_A = "MARIO"; Codice_Fiscale_contraente_A = "RSSMRA80A01H501U"; Indirizzo_contraente_A = "VIA ROMA 1"; CAP_contraente_A = "00100"; Stato_contraente_A = "ITALIA"; N_telefono_mail_contraente_A = "333.1234567"; Marca_e_Tipo_A = "FIAT PANDA"; Targa_A = "AA123AA"; Stato_immatricolazione_A = "IT"; Rimorchio_A = ""; Stato_immatricolazione2_A = ""; Denominazione_A = "GENERALI"; Numero_Polizza_A = "123456"; N_carta_verde_A = "CV-001"; Data_Inizio_Dal_A = "01/01/2023"; Data_Scadenza_Al_A = "01/01/2024"; Agenzia_A = "ROMA"; Denominazione_agenzia_A = "AG. CENTRALE"; Indirizzo_agenzia_A = "VIA PO 20"; Stato_agenzia_A = "IT"; N_tel_mail_agenzia_A = "ag@mail.it"; FLAG_danni_mat_assicurati_A = true; Cognome_cond_A = "ROSSI"; Nome_cond_A = "MARIO"; Data_nascita_cond_A = "01/01/1980"; Cod_fiscale_cond_A = "RSSMRA80"; Indirizzo_cond_A = "VIA ROMA 1"; Stato_cond_A = "IT"; N_tel_mail_cond_A = "333.1234567"; N_Patente_cond_A = "PAT-001"; Scadenza_cond_A = "01/01/2030"; Categoria_cond_A = "B"; puntiUrtoA_List = ["Anteriore"]; danni_visibili_A = "PARAURTI ROTTO"; osservazioni_A = "RAGIONE PIENA"; circostanzeA = {1:true}; puntiFirmaA = [const Offset(0,0), const Offset(10,10)]; tratti = [TrattoPenna([const Offset(10,10), const Offset(100,100)], tipo: 'penna')]; resetB(); } else { // Dati B Cognome_contraente_B = "VERDI"; Nome_contraente_B = "LUIGI"; Codice_Fiscale_contraente_B = "VRDLGU90B02F205Z"; Indirizzo_contraente_B = "MILANO"; CAP_contraente_B = "20100"; Stato_contraente_B = "ITALIA"; N_telefono_mail_contraente_B = "340.9876543"; Marca_e_Tipo_B = "FORD FIESTA"; Targa_B = "BB987BB"; Stato_immatricolazione_B = "IT"; Rimorchio_B = ""; Stato_immatricolazione2_B = ""; Denominazione_B = "ALLIANZ"; Numero_Polizza_B = "987654"; N_carta_verde_B = "CV-002"; Data_Inizio_Dal_B = "01/01/2023"; Data_Scadenza_Al_B = "01/01/2024"; Agenzia_B = "MILANO"; Denominazione_agenzia_B = "AG. NORD"; Indirizzo_agenzia_B = "VIA DANTE 1"; Stato_agenzia_B = "IT"; N_tel_mail_agenzia_B = "mi@mail.it"; FLAG_danni_mat_assicurati_B = false; Cognome_cond_B = "VERDI"; Nome_cond_B = "LUIGI"; Data_nascita_cond_B = "02/02/1990"; Cod_fiscale_cond_B = "VRDLGU90"; Indirizzo_cond_B = "MILANO"; Stato_cond_B = "IT"; N_tel_mail_cond_B = "340.9876543"; N_Patente_cond_B = "PAT-002"; Scadenza_cond_B = "02/02/2030"; Categoria_cond_B = "B"; puntiUrtoB_List = ["Posteriore"]; danni_visibili_B = "PARAURTI POST ROTTO"; osservazioni_B = "NON HO VISTO"; circostanzeB = {12:true}; puntiFirmaB = [const Offset(0,0), const Offset(10,10)]; resetA(); } } } === FILE: lib/firebase_exchange.dart === import 'package:cloud_firestore/cloud_firestore.dart'; import 'dart:math'; import 'dart:async'; import 'cid_data_manager.dart'; import 'global_data.dart'; class FirebaseExchange { static final FirebaseFirestore _db = FirebaseFirestore.instance; static const String collectionName = 'scambi_cid'; // --- LATO CHE GENERA IL QR (Host) --- static Future> avviaSessioneRealTime() async { String sessionId = _generaCodiceUnivoco(); String mioLato = GlobalData.latoCorrente; // Estrazione dati Map mieiDati = CidDataManager.estraiDatiPerExport(); Map payload = { 'created_at': FieldValue.serverTimestamp(), 'expires_at': DateTime.now().add(const Duration(minutes: 10)).millisecondsSinceEpoch, 'dati_$mioLato': mieiDati, }; // Scrittura su Firebase await _db.collection(collectionName).doc(sessionId).set(payload); return { 'sessionId': sessionId, 'stream': _db.collection(collectionName).doc(sessionId).snapshots() }; } // --- LATO CHE SCANSIONA (Guest) --- static Future completaScambio(String sessionId) async { try { sessionId = sessionId.trim().toUpperCase(); DocumentReference docRef = _db.collection(collectionName).doc(sessionId); DocumentSnapshot doc = await docRef.get(); if (!doc.exists) return false; Map data = doc.data() as Map; String mioLato = GlobalData.latoCorrente; String latoAltro = mioLato == 'A' ? 'B' : 'A'; // 1. Importo i dati dell'altro if (data['dati_$latoAltro'] != null) { CidDataManager.importaDati(data['dati_$latoAltro']); } else { return false; } // 2. Invio i miei dati per completare lo scambio Map mieiDati = CidDataManager.estraiDatiPerExport(); await docRef.update({ 'dati_$mioLato': mieiDati }); return true; } catch (e) { print("Errore durante lo scambio: $e"); return false; } } static String _generaCodiceUnivoco() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; Random rnd = Random(); return String.fromCharCodes(Iterable.generate( 6, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); } } === FILE: lib/security_service.dart === // FILE: lib/security_service.dart import 'package:encrypt/encrypt.dart' as encrypt; import 'dart:convert'; import 'dart:math'; class SecurityService { // 1. GENERATORE DI CHIAVI static String generaChiaveSessione() { const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890'; Random rnd = Random.secure(); return String.fromCharCodes(Iterable.generate(32, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); } // 2. CRIPTAZIONE static String criptaDati(Map dati, String chiaveSegreta) { try { final key = encrypt.Key.fromUtf8(chiaveSegreta); final iv = encrypt.IV.fromLength(16); final encrypter = encrypt.Encrypter(encrypt.AES(key)); String jsonString = jsonEncode(dati); final encrypted = encrypter.encrypt(jsonString, iv: iv); return "${iv.base64}:${encrypted.base64}"; } catch (e) { return "Errore Criptazione: $e"; } } // 3. DECRIPTAZIONE static Map decriptaDati(String pacchettoCriptato, String chiaveSegreta) { try { final parts = pacchettoCriptato.split(':'); if (parts.length != 2) throw Exception("Formato dati non valido"); final iv = encrypt.IV.fromBase64(parts[0]); final encryptedData = encrypt.Encrypted.fromBase64(parts[1]); final key = encrypt.Key.fromUtf8(chiaveSegreta); final encrypter = encrypt.Encrypter(encrypt.AES(key)); final decrypted = encrypter.decrypt(encryptedData, iv: iv); return jsonDecode(decrypted); } catch (e) { return {"errore": "Impossibile decriptare: $e"}; } } } === FILE: lib/pdf_engine.dart === import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/services.dart' show rootBundle; import 'package:syncfusion_flutter_pdf/pdf.dart'; import 'package:flutter/material.dart'; import 'global_data.dart'; import 'models.dart'; import 'cai_mapping.dart'; class PdfEngine { static Future> generaDocumentoCai() async { PdfDocument? document; try { final ByteData data = await rootBundle.load('assets/CAI_p1.pdf'); // 1. Caricamento e Copia final List bytesCopia = data.buffer.asUint8List().toList(); document = PdfDocument(inputBytes: bytesCopia); final PdfForm form = document.form; final PdfPage page = document.pages[0]; // form.setDefaultAppearance(false); // REMOVED to fix empty fields issue // Mappatura Campi Map mappaCampi = {}; for (int i = 0; i < form.fields.count; i++) { if (form.fields[i].name != null) { mappaCampi[form.fields[i].name!.trim().toUpperCase()] = form.fields[i]; } } // --- COMPILAZIONE --- // 1. TESTI STANDARD (Escludiamo i Danni per gestirli dopo con split a 20 char) CaiMapping.testi.forEach((keyGlobal, keyPdf) { if (keyGlobal.contains("danni_visibili")) return; String valore = _valoreDaGlobal(keyGlobal); String keyPdfNorm = keyPdf.trim().toUpperCase(); if (mappaCampi.containsKey(keyPdfNorm) && valore.isNotEmpty) { final field = mappaCampi[keyPdfNorm]; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); // Font 8 field.foreColor = PdfColor(0, 0, 0); field.textAlignment = PdfTextAlignment.left; field.text = valore.toUpperCase(); } } }); // 1.1 GESTIONE SPECIALE DANNI (Split forzato a 20 caratteri) _riempiCampoSplit(mappaCampi, GlobalData.danni_visibili_A, "DANNI_VIS_A1", "DANNI_VIS_A2"); _riempiCampoSplit(mappaCampi, GlobalData.danni_visibili_B, "DANNI_VIS_B1", "DANNI_VIS_B2"); // 2. CHECKBOX _scriviX(mappaCampi, [GlobalData.feriti ? CaiMapping.feriti_SI : CaiMapping.feriti_NO]); _scriviX(mappaCampi, [GlobalData.Veicoli_danni_materiali_oltre ? CaiMapping.danni_veicoli_SI : CaiMapping.danni_veicoli_NO]); _scriviX(mappaCampi, [GlobalData.Oggetti_diversi_danni_materiali ? CaiMapping.danni_oggetti_SI : CaiMapping.danni_oggetti_NO]); _scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_A ? CaiMapping.danni_mat_A_SI : CaiMapping.danni_mat_A_NO]); _scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_B ? CaiMapping.danni_mat_B_SI : CaiMapping.danni_mat_B_NO]); String catA = GlobalData.Categoria_cond_A.toUpperCase().trim(); if (catA == 'A') _scriviX(mappaCampi, ['cat_a_A']); else if (catA == 'B') _scriviX(mappaCampi, ['cat_b_A']); else if (catA.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_A'], catA); String catB = GlobalData.Categoria_cond_B.toUpperCase().trim(); if (catB == 'A') _scriviX(mappaCampi, ['cat_a_B']); else if (catB == 'B') _scriviX(mappaCampi, ['cat_b_B']); else if (catB.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_B'], catB); // 3. CIRCOSTANZE int countA = 0; int countB = 0; for (int i = 1; i <= 17; i++) { if (GlobalData.circostanzeA[i] == true) { if (_scriviX(mappaCampi, [i < 10 ? "A0$i" : "A$i"])) countA++; } if (GlobalData.circostanzeB[i] == true) { List nomiTarget = []; if (i == 9) nomiTarget = ["Check Box 26", "CheckBox26", "26"]; else if (i == 10) nomiTarget = ["Check Box 27", "CheckBox27", "27"]; else if (i == 11) nomiTarget = ["Check Box 28", "CheckBox28", "28"]; else if (i == 12) nomiTarget = ["Check Box 29", "CheckBox29", "29"]; else nomiTarget = [i < 10 ? "B0$i" : "B$i"]; if (_scriviX(mappaCampi, nomiTarget)) countB++; } } _scriviTestoTotale(mappaCampi, ['A_TOT', 'A_tot'], countA.toString()); _scriviTestoTotale(mappaCampi, ['B_TOT', 'B_tot'], countB.toString()); // 4. PUNTI URTO for (String punto in GlobalData.puntiUrtoA_List) _scriviXRossa(mappaCampi, [punto]); for (String punto in GlobalData.puntiUrtoB_List) _scriviXRossa(mappaCampi, [punto]); // 5. IMMAGINI E GRAFICO // Render the updated graph (with new vehicle types) to an image Uint8List? graficoBytes = await _renderGraficoV40( GlobalData.tratti.cast().toList(), GlobalData.elementi.cast().toList() ); // Draw the graph image into the specific PDF box await _disegnaInBox(page, mappaCampi, CaiMapping.box_grafico, graficoBytes); await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_A, await _renderFirmaTight(GlobalData.puntiFirmaA, Colors.black)); await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_B, await _renderFirmaTight(GlobalData.puntiFirmaB, Colors.black)); // ================================================================= // SALVATAGGIO SICURO // ================================================================= List bytesTemporanei = await document.save(); document.dispose(); PdfDocument docFinale = PdfDocument(inputBytes: bytesTemporanei); try { docFinale.form.flattenAllFields(); } catch (e) { debugPrint("⚠️ Errore Flattening: $e"); docFinale.form.readOnly = true; } List bytesFinali = await docFinale.save(); docFinale.dispose(); return bytesFinali; } catch (e) { debugPrint("ERRORE GENERAZIONE PDF: $e"); return []; } } // ... (Keep existing helper methods: _riempiCampoSplit, _scriviX, _scriviTesto, _scriviXRossa, _scriviTestoTotale, _disegnaInBox, _renderFirmaTight, _valoreDaGlobal) // Re-pasting them here for completeness to ensure no missing dependencies static void _riempiCampoSplit(Map mappa, String testoCompleto, String key1, String key2) { if (testoCompleto.isEmpty) return; String riga1 = ""; String riga2 = ""; int limite = 20; if (testoCompleto.length <= limite) { riga1 = testoCompleto; } else { int splitIndex = testoCompleto.lastIndexOf(" ", limite); if (splitIndex == -1) splitIndex = limite; riga1 = testoCompleto.substring(0, splitIndex).trim(); riga2 = testoCompleto.substring(splitIndex).trim(); } if (mappa.containsKey(key1)) { final f1 = mappa[key1] as PdfTextBoxField; f1.font = PdfStandardFont(PdfFontFamily.helvetica, 8); f1.textAlignment = PdfTextAlignment.left; f1.text = riga1.toUpperCase(); } if (mappa.containsKey(key2)) { final f2 = mappa[key2] as PdfTextBoxField; f2.font = PdfStandardFont(PdfFontFamily.helvetica, 8); f2.textAlignment = PdfTextAlignment.left; f2.text = riga2.toUpperCase(); } } static bool _scriviX(Map mappa, List nomiPossibili) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 14); field.foreColor = PdfColor(0, 0, 0); field.textAlignment = PdfTextAlignment.center; field.text = "X"; } else if (field is PdfCheckBoxField) { field.isChecked = true; } return true; } } return false; } static void _scriviTesto(Map mappa, List nomiPossibili, String testo) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); field.foreColor = PdfColor(0, 0, 0); field.text = testo; } return; } } } static bool _scriviXRossa(Map mappa, List nomiPossibili) { for (String nome in nomiPossibili) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 16, style: PdfFontStyle.bold); field.foreColor = PdfColor(255, 0, 0); field.textAlignment = PdfTextAlignment.center; field.text = "X"; return true; } } } return false; } static void _scriviTestoTotale(Map mappa, List nomi, String testo) { for (String nome in nomi) { String key = nome.trim().toUpperCase(); if (mappa.containsKey(key)) { final field = mappa[key]!; if (field is PdfTextBoxField) { field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); field.textAlignment = PdfTextAlignment.center; field.text = testo; } return; } } } static Future _disegnaInBox(PdfPage page, Map mappa, String nomeCampo, Uint8List? imgBytes) async { String key = nomeCampo.trim().toUpperCase(); if (imgBytes == null || !mappa.containsKey(key)) return; Rect boxRect = mappa[key]!.bounds; PdfBitmap bitmap = PdfBitmap(imgBytes); double imageW = bitmap.width.toDouble(); double imageH = bitmap.height.toDouble(); if (imageW <= 0 || imageH <= 0) return; double ratioX = boxRect.width / imageW; double ratioY = boxRect.height / imageH; double scale = (ratioX < ratioY) ? ratioX : ratioY; double drawW = imageW * scale; double drawH = imageH * scale; double offsetX = boxRect.left + (boxRect.width - drawW) / 2; double offsetY = boxRect.top + (boxRect.height - drawH) / 2; page.graphics.drawImage(bitmap, Rect.fromLTWH(offsetX, offsetY, drawW, drawH)); } static Future _renderFirmaTight(List punti, Color colore) async { if (punti.isEmpty) return null; double minX = double.infinity, minY = double.infinity, maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (var p in punti) { if (p != null) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } } double padding = 20.0; double firmaW = maxX - minX; double firmaH = maxY - minY; if (firmaW <= 0) firmaW = 1; if (firmaH <= 0) firmaH = 1; double resolutionScale = 3.0; double canvasW = (firmaW + padding * 2) * resolutionScale; double canvasH = (firmaH + padding * 2) * resolutionScale; final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); canvas.scale(resolutionScale); canvas.translate(-minX + padding, -minY + padding); final paint = Paint()..color = colore..strokeWidth = 5.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round; for (int i = 0; i < punti.length - 1; i++) { if (punti[i] != null && punti[i+1] != null) { canvas.drawLine(punti[i]!, punti[i+1]!, paint); } } final img = await recorder.endRecording().toImage(canvasW.toInt(), canvasH.toInt()); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } static Future _renderGraficoV40(List tratti, List elementi) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); final size = const Size(2000, 800); final painter = PainterV40(tratti, elementi); painter.paint(canvas, size); final img = await recorder.endRecording().toImage(size.width.toInt(), size.height.toInt()); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); return byteData?.buffer.asUint8List(); } static String _valoreDaGlobal(String key) { switch (key) { case 'data_incidente': return GlobalData.data_incidente; case 'ora': return GlobalData.ora; case 'luogo': return GlobalData.luogo; case 'testimoni': return GlobalData.testimoni; case 'danni_visibili_A': return GlobalData.danni_visibili_A; case 'osservazioni_A': return GlobalData.osservazioni_A; case 'danni_visibili_B': return GlobalData.danni_visibili_B; case 'osservazioni_B': return GlobalData.osservazioni_B; case 'Cognome_contraente_A': return GlobalData.Cognome_contraente_A; case 'Nome_contraente_A': return GlobalData.Nome_contraente_A; case 'Codice_Fiscale_contraente_A': return GlobalData.Codice_Fiscale_contraente_A; case 'Indirizzo_contraente_A': return GlobalData.Indirizzo_contraente_A; case 'CAP_contraente_A': return GlobalData.CAP_contraente_A; case 'Stato_contraente_A': return GlobalData.Stato_contraente_A; case 'N_telefono_mail_contraente_A': return GlobalData.N_telefono_mail_contraente_A; case 'Marca_e_Tipo_A': return GlobalData.Marca_e_Tipo_A; case 'Targa_A': return GlobalData.Targa_A; case 'Stato_immatricolazione_A': return GlobalData.Stato_immatricolazione_A; case 'Rimorchio_A': return GlobalData.Rimorchio_A; case 'Stato_immatricolazione2_A': return GlobalData.Stato_immatricolazione2_A; case 'Denominazione_A': return GlobalData.Denominazione_A; case 'Numero_Polizza_A': return GlobalData.Numero_Polizza_A; case 'N_carta_verde_A': return GlobalData.N_carta_verde_A; case 'Data_Inizio_Dal_A': return GlobalData.Data_Inizio_Dal_A; case 'Data_Scadenza_Al_A': return GlobalData.Data_Scadenza_Al_A; case 'Agenzia_A': return GlobalData.Agenzia_A; case 'Indirizzo_agenzia_A': return GlobalData.Indirizzo_agenzia_A; case 'Stato_agenzia_A': return GlobalData.Stato_agenzia_A; case 'Denominazione_agenzia_A': return GlobalData.Denominazione_agenzia_A; case 'N_tel_mail_agenzia_A': return GlobalData.N_tel_mail_agenzia_A; case 'Cognome_cond_A': return GlobalData.Cognome_cond_A; case 'Nome_cond_A': return GlobalData.Nome_cond_A; case 'Data_nascita_cond_A': return GlobalData.Data_nascita_cond_A; case 'Cod_fiscale_cond_A': return GlobalData.Cod_fiscale_cond_A; case 'Indirizzo_cond_A': return GlobalData.Indirizzo_cond_A; case 'Stato_cond_A': return GlobalData.Stato_cond_A; case 'N_tel_mail_cond_A': return GlobalData.N_tel_mail_cond_A; case 'N_Patente_cond_A': return GlobalData.N_Patente_cond_A; case 'Scadenza_cond_A': return GlobalData.Scadenza_cond_A; case 'Cognome_contraente_B': return GlobalData.Cognome_contraente_B; case 'Nome_contraente_B': return GlobalData.Nome_contraente_B; case 'Codice_Fiscale_contraente_B': return GlobalData.Codice_Fiscale_contraente_B; case 'Indirizzo_contraente_B': return GlobalData.Indirizzo_contraente_B; case 'CAP_contraente_B': return GlobalData.CAP_contraente_B; case 'Stato_contraente_B': return GlobalData.Stato_contraente_B; case 'N_telefono_mail_contraente_B': return GlobalData.N_telefono_mail_contraente_B; case 'Marca_e_Tipo_B': return GlobalData.Marca_e_Tipo_B; case 'Targa_B': return GlobalData.Targa_B; case 'Stato_immatricolazione_B': return GlobalData.Stato_immatricolazione_B; case 'Rimorchio_B': return GlobalData.Rimorchio_B; case 'Stato_immatricolazione2_B': return GlobalData.Stato_immatricolazione2_B; case 'Denominazione_B': return GlobalData.Denominazione_B; case 'Numero_Polizza_B': return GlobalData.Numero_Polizza_B; case 'N_carta_verde_B': return GlobalData.N_carta_verde_B; case 'Data_Inizio_Dal_B': return GlobalData.Data_Inizio_Dal_B; case 'Data_Scadenza_Al_B': return GlobalData.Data_Scadenza_Al_B; case 'Agenzia_B': return GlobalData.Agenzia_B; case 'Indirizzo_agenzia_B': return GlobalData.Indirizzo_agenzia_B; case 'Stato_agenzia_B': return GlobalData.Stato_agenzia_B; case 'Denominazione_agenzia_B': return GlobalData.Denominazione_agenzia_B; case 'N_tel_mail_agenzia_B': return GlobalData.N_tel_mail_agenzia_B; case 'Cognome_cond_B': return GlobalData.Cognome_cond_B; case 'Nome_cond_B': return GlobalData.Nome_cond_B; case 'Data_nascita_cond_B': return GlobalData.Data_nascita_cond_B; case 'Cod_fiscale_cond_B': return GlobalData.Cod_fiscale_cond_B; case 'Indirizzo_cond_B': return GlobalData.Indirizzo_cond_B; case 'Stato_cond_B': return GlobalData.Stato_cond_B; case 'N_tel_mail_cond_B': return GlobalData.N_tel_mail_cond_B; case 'N_Patente_cond_B': return GlobalData.N_Patente_cond_B; case 'Scadenza_cond_B': return GlobalData.Scadenza_cond_B; default: return ""; } } } class PainterV40 extends CustomPainter { final List tr; final List el; PainterV40(this.tr, this.el); final List palette = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red]; @override void paint(Canvas canvas, Size size) { // 1. SFONDO BIANCO final Paint backgroundPaint = Paint()..color = Colors.white; canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint); // 2. GRIGLIA final Paint gridPaint = Paint()..color = Colors.grey.shade300..strokeWidth = 2.0; double step = 40.0; for (double x = 0; x <= size.width; x += step) { canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint); } for (double y = 0; y <= size.height; y += step) { canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint); } if (tr.isEmpty && el.isEmpty) return; // --- BOUNDING BOX --- double minX = double.infinity, minY = double.infinity; double maxX = double.negativeInfinity, maxY = double.negativeInfinity; for (var t in tr) { for (var p in t.punti) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } } for (var e in el) { if (e.posizione.dx - 30 < minX) minX = e.posizione.dx - 30; if (e.posizione.dx + 30 > maxX) maxX = e.posizione.dx + 30; if (e.posizione.dy - 30 < minY) minY = e.posizione.dy - 30; if (e.posizione.dy + 30 > maxY) maxY = e.posizione.dy + 30; } if (minX == double.infinity) { minX = 0; maxX = 100; minY = 0; maxY = 100; } double padding = 40.0; double drawingW = maxX - minX + (padding * 2); double drawingH = maxY - minY + (padding * 2); if (drawingW <= 0) drawingW = 100; if (drawingH <= 0) drawingH = 100; double scaleX = size.width / drawingW; double scaleY = size.height / drawingH; double scale = (scaleX < scaleY) ? scaleX : scaleY; double offsetX = (size.width - (drawingW * scale)) / 2; double offsetY = (size.height - (drawingH * scale)) / 2; canvas.save(); canvas.translate(offsetX, offsetY); canvas.scale(scale); canvas.translate(-minX + padding, -minY + padding); // --- DISEGNO STRADE E FRECCE --- Paint pStrada = Paint() ..color = Colors.black ..strokeWidth = 4.0 / scale ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; for (var t in tr) { if (t.punti.length > 1) { Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy); for (var pt in t.punti) path.lineTo(pt.dx, pt.dy); canvas.drawPath(path, pStrada); if (t.tipo == 'freccia') { double a = (t.punti.last - t.punti[t.punti.length - 2]).direction; canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a - 0.5, 15), pStrada); canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a + 0.5, 15), pStrada); } } } // --- DISEGNO ELEMENTI --- for (var e in el) { canvas.save(); canvas.translate(e.posizione.dx, e.posizione.dy); canvas.rotate(e.rotazione); if (e.tipo == 'testo') { final tp = TextPainter( text: TextSpan(text: e.label ?? "", style: const TextStyle(color: Colors.black, fontSize: 24, fontWeight: FontWeight.bold)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } else { String lettera = e.label ?? "A"; int idx = (lettera.isNotEmpty) ? (lettera.codeUnitAt(0) - 65) % palette.length : 0; Color colore = palette[idx]; if (e.tipo.startsWith('auto')) { _disegnaAuto(canvas, colore, lettera); } else if (e.tipo.startsWith('moto')) { _disegnaMoto(canvas, colore, lettera); } else if (e.tipo.startsWith('furgone')) { _disegnaFurgone(canvas, colore, lettera); } } canvas.restore(); } canvas.restore(); } // --- METODI DI DISEGNO SPECIFICI --- void _disegnaAuto(Canvas canvas, Color colore, String lettera) { double w = 48.0; double h = 24.0; Paint pBody = Paint()..color = colore; Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5; // Corpo RRect bodyRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset.zero, width: w, height: h), const Radius.circular(5)); canvas.drawRRect(bodyRect, pBody); // Cabina Paint pCabin = Paint()..color = Colors.black.withOpacity(0.2); canvas.drawRect(Rect.fromLTRB(-w/2 + 6, -h/2 + 3, w/4, h/2 - 3), pCabin); // Fari Paint pLights = Paint()..color = Colors.yellow; canvas.drawCircle(Offset(w/2 - 2, h/2 - 4), 2.5, pLights); canvas.drawCircle(Offset(w/2 - 2, -h/2 + 4), 2.5, pLights); // Bordo canvas.drawRRect(bodyRect, pBorder); // Lettera _disegnaLettera(canvas, lettera); } void _disegnaMoto(Canvas canvas, Color colore, String lettera) { double w = 34.0; double h = 12.0; Paint pBody = Paint()..color = colore; Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.0; Paint pBlack = Paint()..color = Colors.black; // Ruote (piene) canvas.drawCircle(Offset(-w/2 + 4, 0), 5.0, pBlack); canvas.drawCircle(Offset(w/2 - 4, 0), 5.0, pBlack); // Corpo Rect bodyRect = Rect.fromCenter(center: Offset.zero, width: w - 10, height: h - 4); canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBody); canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBorder); // Sella canvas.drawRect(Rect.fromCenter(center: Offset(-5, 0), width: 12, height: 8), pBlack); // Manubrio Paint pManubrio = Paint()..color = Colors.black..strokeWidth = 3.0..strokeCap = StrokeCap.round; canvas.drawLine(Offset(w/2 - 12, -10), Offset(w/2 - 12, 10), pManubrio); // Faro canvas.drawCircle(Offset(w/2, 0), 3.0, Paint()..color = Colors.yellow); _disegnaLettera(canvas, lettera, fontSize: 10); } void _disegnaFurgone(Canvas canvas, Color colore, String lettera) { double w = 60.0; double h = 26.0; Paint pBody = Paint()..color = colore; Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5; // Vano Carico (Box posteriore) Rect caricoRect = Rect.fromLTRB(-w/2, -h/2, w/4, h/2); canvas.drawRect(caricoRect, pBody); canvas.drawRect(caricoRect, pBorder); // Cabina (Box anteriore) Rect cabinaRect = Rect.fromLTRB(w/4, -h/2 + 1, w/2, h/2 - 1); canvas.drawRect(cabinaRect, pBody); canvas.drawRect(cabinaRect, pBorder); // Parabrezza Paint pVetro = Paint()..color = Colors.black.withOpacity(0.3); canvas.drawRect(Rect.fromLTRB(w/4 + 2, -h/2 + 3, w/2 - 2, h/2 - 3), pVetro); // Fari Paint pLights = Paint()..color = Colors.yellow; canvas.drawRect(Rect.fromLTWH(w/2 - 2, -h/2 + 2, 2, 4), pLights); canvas.drawRect(Rect.fromLTWH(w/2 - 2, h/2 - 6, 2, 4), pLights); _disegnaLettera(canvas, lettera); } void _disegnaLettera(Canvas canvas, String txt, {double fontSize = 16}) { final tp = TextPainter( text: TextSpan(text: txt, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } === FILE: lib/comp_13.dart === import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'global_data.dart'; import 'models.dart'; import 'comp_15.dart'; class Comp13Screen extends StatefulWidget { const Comp13Screen({super.key}); @override State createState() => _Comp13ScreenState(); } class _Comp13ScreenState extends State { List _elementi = []; List _tratti = []; String modo = 'penna'; @override void initState() { super.initState(); _elementi = List.from(GlobalData.elementi); _tratti = List.from(GlobalData.tratti); // Proviamo a chiedere il landscape al sistema (non sempre funziona su iPad) _setLandscape(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) _mostraIstruzioni(); }); } Future _setLandscape() async { await SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); } Future _setPortrait() async { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); } Future _esci() async { GlobalData.tratti = List.from(_tratti); GlobalData.elementi = List.from(_elementi); await _setPortrait(); if (mounted) Navigator.pop(context); } Future _vaiAvanti() async { GlobalData.tratti = List.from(_tratti); GlobalData.elementi = List.from(_elementi); // Prima di cambiare pagina, rimettiamo in verticale // await _setPortrait(); if (mounted) { await Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp15Screen())); // Al ritorno, forziamo di nuovo orizzontale await _setLandscape(); } } // --- FUNZIONE HELPER PER RUOTARE I DIALOGHI --- // Se l'iPad è verticale, ruota il contenuto del dialogo di 90 gradi Widget _ruotaSeNecessario(BuildContext context, Widget child) { return OrientationBuilder( builder: (context, orientation) { return orientation == Orientation.portrait ? RotatedBox(quarterTurns: 1, child: child) : child; }, ); } void _mostraIstruzioni() { showDialog( context: context, builder: (ctx) => _ruotaSeNecessario(ctx, AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), title: Row( children: [ Icon(Icons.help_outline, color: Colors.blue.shade900), const SizedBox(width: 10), const Text("Guida al Disegno", style: TextStyle(fontWeight: FontWeight.bold)), ], ), content: SizedBox( width: 500, // Larghezza fissa per evitare problemi di layout ruotato height: 300, // Altezza fissa per scrollare comodamente child: Scrollbar( thumbVisibility: true, child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: const Text( "• 🖊 Usa la penna per disegnare le strade\n" "• 🚗 🏍️ 🚛 Seleziona e tocca per aggiungere veicoli\n" "• ↗️ Inserisci frecce di direzione\n" "• 📝 Aggiungi testo (es. nomi vie)\n" "• 🔄 Tocca un elemento per ruotarlo\n" "• ❌ Premi a lungo un oggetto per eliminarlo\n" "• 🗑️ Usa il tasto Cestino per resettare tutto", style: TextStyle(fontSize: 16, height: 1.6, color: Colors.black87), ), ), ), ), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade900, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), onPressed: () => Navigator.pop(ctx), child: const Text("HO CAPITO", style: TextStyle(fontWeight: FontWeight.bold)), ), ) ], )), ); } void _gestisciInserimento(Offset pos) async { if (modo == 'testo') { TextEditingController tc = TextEditingController(); await showDialog( context: context, builder: (c) => _ruotaSeNecessario(c, Dialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Container( width: 300, padding: const EdgeInsets.all(20), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text("Inserisci Testo", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 10), TextField( controller: tc, autofocus: true, decoration: const InputDecoration(hintText: "Nome via...", border: OutlineInputBorder()) ), const SizedBox(height: 20), ElevatedButton( onPressed: () { if (tc.text.isNotEmpty) { setState(() => _elementi.add(ElementoGrafico(pos, 'testo', label: tc.text))); } Navigator.pop(c); }, child: const Text("INSERISCI") ) ], ), ), )), ); } else if (['auto', 'moto', 'furgone'].contains(modo)) { int numeroVeicoli = _elementi.where((e) => e.tipo.startsWith('auto') || e.tipo.startsWith('moto') || e.tipo.startsWith('furgone')).length; String prossimaLettera = String.fromCharCode(65 + numeroVeicoli); setState(() { _elementi.add(ElementoGrafico(pos, '$modo$prossimaLettera', label: prossimaLettera)); }); } } @override Widget build(BuildContext context) { // 1. Logica di orientamento principale (Body) // Se siamo verticali, ruotiamo tutto di 90 gradi. return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (!didPop) await _esci(); }, child: OrientationBuilder( builder: (context, orientation) { final bool isPortrait = orientation == Orientation.portrait; return Scaffold( backgroundColor: Colors.white, resizeToAvoidBottomInset: false, // Se Portrait -> Ruota Body. Se Landscape -> Body normale. body: isPortrait ? RotatedBox(quarterTurns: 1, child: _buildBodyContent(context)) : _buildBodyContent(context), ); }, ), ); } // Contenuto principale estratto per facilitare la rotazione Widget _buildBodyContent(BuildContext context) { // Calcoliamo dimensioni sicure final size = MediaQuery.of(context).size; final double maxToolbarHeight = size.shortestSide * 0.8; return SafeArea( child: Stack( children: [ Column( children: [ Container( height: 50, color: Colors.blueGrey[50], padding: const EdgeInsets.symmetric(horizontal: 10), child: Row( children: [ IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: _esci, ), const Expanded( child: Text( "13. Grafico", textAlign: TextAlign.center, style: TextStyle(color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold), ), ), IconButton( icon: const Icon(Icons.help_outline, color: Colors.black), onPressed: _mostraIstruzioni ), const SizedBox(width: 15), ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8) ), icon: const Icon(Icons.check), label: const Text("SALVA"), onPressed: _vaiAvanti, ), ], ), ), Expanded( child: GestureDetector( onLongPressStart: (d) { setState(() { _elementi.removeWhere((e) => e.contiene(d.localPosition)); _tratti.removeWhere((t) => t.contiene(d.localPosition)); }); }, onTapDown: (d) { bool colpito = false; for (var e in _elementi) { if (e.contiene(d.localPosition)) { setState(() => e.rotazione += 0.785); colpito = true; break; } } if (!colpito && (['auto', 'moto', 'furgone', 'testo'].contains(modo))) { _gestisciInserimento(d.localPosition); } }, onPanStart: (d) { if (modo == 'penna' || modo == 'freccia') { setState(() => _tratti.add(TrattoPenna([d.localPosition], tipo: modo))); } }, onPanUpdate: (d) { if (modo == 'penna' || modo == 'freccia') { setState(() => _tratti.last.punti.add(d.localPosition)); } }, child: Container( color: Colors.white, width: double.infinity, height: double.infinity, child: CustomPaint( painter: PainterV40(_tratti, _elementi), size: Size.infinite, ), ), ), ), ], ), Positioned( left: 10, top: 60, child: Container( width: 55, constraints: BoxConstraints(maxHeight: maxToolbarHeight), decoration: BoxDecoration( color: Colors.white.withOpacity(0.95), borderRadius: BorderRadius.circular(25), boxShadow: [const BoxShadow(color: Colors.black26, blurRadius: 4)], border: Border.all(color: Colors.grey.shade300), ), child: ClipRRect( borderRadius: BorderRadius.circular(25), child: SingleChildScrollView( padding: const EdgeInsets.symmetric(vertical: 8), child: Column( mainAxisSize: MainAxisSize.min, children: [ _toolBtn(Icons.edit, 'penna', Colors.black), _toolBtn(Icons.trending_flat, 'freccia', Colors.black), _toolBtn(Icons.title, 'testo', Colors.black), const Divider(indent: 8, endIndent: 8, height: 15), _toolBtn(Icons.directions_car, 'auto', Colors.blue), _toolBtn(Icons.two_wheeler, 'moto', Colors.orange.shade800), _toolBtn(Icons.local_shipping, 'furgone', Colors.green), const Divider(indent: 8, endIndent: 8, height: 15), IconButton( icon: const Icon(Icons.delete_forever, color: Colors.red), tooltip: "Cancella tutto", onPressed: () => setState(() { _tratti.clear(); _elementi.clear(); }), ), ], ), ), ), ), ), ], ), ); } Widget _toolBtn(IconData icon, String tool, Color activeColor) { bool isSelected = modo == tool; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: () => setState(() => modo = tool), child: Container( width: 40, height: 40, decoration: BoxDecoration( color: isSelected ? activeColor.withOpacity(0.15) : Colors.transparent, border: isSelected ? Border.all(color: activeColor, width: 2) : null, shape: BoxShape.circle, ), child: Icon( icon, color: activeColor, size: 24, ), ), ), ), ); } } class PainterV40 extends CustomPainter { final List tr; final List el; PainterV40(this.tr, this.el); final List palette = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red, Colors.teal]; @override void paint(Canvas canvas, Size size) { Paint pStrada = Paint()..color = Colors.black..strokeWidth = 3.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round; for (var t in tr) { if (t.punti.length > 1) { // Disegno il tratto stradale Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy); for (var pt in t.punti) path.lineTo(pt.dx, pt.dy); canvas.drawPath(path, pStrada); // --- CORREZIONE FRECCIA --- if (t.tipo == 'freccia') { Offset pTip = t.punti.last; Offset pBack = t.punti[t.punti.length - 2]; // STABILIZZAZIONE: Cerco un punto precedente che sia distante almeno 10 pixel // dalla punta. Questo ignora i micro-movimenti finali del dito (jitter) // che causavano l'inversione della freccia. for (int i = t.punti.length - 2; i >= 0; i--) { if ((t.punti[i] - pTip).distance > 10.0) { pBack = t.punti[i]; break; } } _disegnaPunta(canvas, pBack, pTip, pStrada); } // -------------------------- } } for (var e in el) { canvas.save(); canvas.translate(e.posizione.dx, e.posizione.dy); canvas.rotate(e.rotazione); if (e.tipo == 'testo') { _disegnaTesto(canvas, e.label ?? ""); } else if (e.tipo.startsWith('auto')) { _disegnaAuto(canvas, e); } else if (e.tipo.startsWith('moto')) { _disegnaMoto(canvas, e); } else if (e.tipo.startsWith('furgone')) { _disegnaFurgone(canvas, e); } canvas.restore(); } } // ... (TUTTI GLI ALTRI METODI RESTANO IDENTICI) ... Color _getColoreDaLettera(String lettera) { if (lettera.isEmpty) return Colors.grey; int idx = (lettera.codeUnitAt(0) - 65) % palette.length; return palette[idx]; } void _disegnaLettera(Canvas canvas, String lettera, {double fontSize = 16}) { final tp = TextPainter( text: TextSpan(text: lettera, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2)); } void _disegnaAuto(Canvas canvas, ElementoGrafico e) { String lettera = e.label ?? "A"; Color colore = _getColoreDaLettera(lettera); double w = 48.0; double h = 24.0; Paint pBody = Paint()..color = colore; Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5; RRect bodyRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset.zero, width: w, height: h), const Radius.circular(5)); canvas.drawRRect(bodyRect, pBody); Paint pCabin = Paint()..color = Colors.black.withOpacity(0.2); canvas.drawRect(Rect.fromLTRB(-w/2 + 6, -h/2 + 3, w/4, h/2 - 3), pCabin); Paint pLights = Paint()..color = Colors.yellow; canvas.drawCircle(Offset(w/2 - 2, h/2 - 4), 2.5, pLights); canvas.drawCircle(Offset(w/2 - 2, -h/2 + 4), 2.5, pLights); canvas.drawRRect(bodyRect, pBorder); _disegnaLettera(canvas, lettera); } void _disegnaMoto(Canvas canvas, ElementoGrafico e) { String lettera = e.label ?? "A"; Color colore = _getColoreDaLettera(lettera); double len = 40.0; double wid = 14.0; Paint pBody = Paint()..color = colore; Paint pBlack = Paint()..color = Colors.black; Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.0; canvas.drawCircle(Offset(-len/2 + 4, 0), 5.0, pBlack); canvas.drawCircle(Offset(len/2 - 4, 0), 5.0, pBlack); Rect bodyRect = Rect.fromCenter(center: Offset.zero, width: len - 10, height: wid - 4); canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBody); canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBorder); Rect sellaRect = Rect.fromCenter(center: Offset(-5, 0), width: 12, height: 8); canvas.drawRect(sellaRect, pBlack); Paint pManubrio = Paint()..color = Colors.black..strokeWidth = 3.0..strokeCap = StrokeCap.round; double xHandle = len/2 - 12; canvas.drawLine(Offset(xHandle, -10), Offset(xHandle, 10), pManubrio); Paint pLights = Paint()..color = Colors.yellow; canvas.drawCircle(Offset(len/2, 0), 3.0, pLights); _disegnaLettera(canvas, lettera, fontSize: 10); } void _disegnaFurgone(Canvas canvas, ElementoGrafico e) { String lettera = e.label ?? "A"; Color colore = _getColoreDaLettera(lettera); double w = 60.0; double h = 26.0; Paint pBody = Paint()..color = colore; Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5; Rect caricoRect = Rect.fromLTRB(-w/2, -h/2, w/4, h/2); canvas.drawRect(caricoRect, pBody); canvas.drawRect(caricoRect, pBorder); Rect cabinaRect = Rect.fromLTRB(w/4, -h/2 + 1, w/2, h/2 - 1); canvas.drawRect(cabinaRect, pBody); canvas.drawRect(cabinaRect, pBorder); Paint pVetro = Paint()..color = Colors.black.withOpacity(0.3); canvas.drawRect(Rect.fromLTRB(w/4 + 2, -h/2 + 3, w/2 - 2, h/2 - 3), pVetro); Paint pLights = Paint()..color = Colors.yellow; canvas.drawRect(Rect.fromLTWH(w/2 - 2, -h/2 + 2, 2, 4), pLights); canvas.drawRect(Rect.fromLTWH(w/2 - 2, h/2 - 6, 2, 4), pLights); _disegnaLettera(canvas, lettera); } void _disegnaTesto(Canvas canvas, String txt) { final tp = TextPainter( text: TextSpan(text: txt, style: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width/2, -tp.height/2)); } void _disegnaPunta(Canvas canvas, Offset p1, Offset p2, Paint paint) { double angle = (p2 - p1).direction; canvas.drawLine(p2, p2 - Offset.fromDirection(angle - 0.5, 10), paint); canvas.drawLine(p2, p2 - Offset.fromDirection(angle + 0.5, 10), paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } === FILE: lib/cid_data_manager.dart === import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'global_data.dart'; import 'models.dart'; class CidDataManager { // =========================================================================== // METODI PER SCAMBIO DATI (QR/P2P) // =========================================================================== static Map estraiDatiPerExport() { String lato = GlobalData.latoCorrente; String val(String vA, String vB, String l) => l == 'A' ? vA : vB; bool valB(bool bA, bool bB, String l) => l == 'A' ? bA : bB; return { 'lato': lato, 'generali': { 'data': GlobalData.data_incidente, 'ora': GlobalData.ora, 'luogo': GlobalData.luogo, 'feriti': GlobalData.feriti, 'testimoni': GlobalData.testimoni, 'danni_materiali': GlobalData.Veicoli_danni_materiali_oltre, 'oggetti_diversi': GlobalData.Oggetti_diversi_danni_materiali, }, 'circostanze': _serializeCircostanze(lato == 'A' ? GlobalData.circostanzeA : GlobalData.circostanzeB), 'punti_urto': lato == 'A' ? GlobalData.puntiUrtoA_List : GlobalData.puntiUrtoB_List, // CASSETTO 1: GRAFICO INCIDENTE (Strade, Auto) 'grafico': { 'tratti_dinamica': GlobalData.tratti.map((t) => t.toMap()).toList(), 'elementi_dinamica': GlobalData.elementi.map((e) => e.toMap()).toList(), }, // CASSETTO 2: FIRMA (Dati grafici personali) 'firma': _serializePunti(lato == 'A' ? GlobalData.puntiFirmaA : GlobalData.puntiFirmaB), 'contraente': { 'cognome': val(GlobalData.Cognome_contraente_A, GlobalData.Cognome_contraente_B, lato), 'nome': val(GlobalData.Nome_contraente_A, GlobalData.Nome_contraente_B, lato), 'cf': val(GlobalData.Codice_Fiscale_contraente_A, GlobalData.Codice_Fiscale_contraente_B, lato), 'indirizzo': val(GlobalData.Indirizzo_contraente_A, GlobalData.Indirizzo_contraente_B, lato), 'cap': val(GlobalData.CAP_contraente_A, GlobalData.CAP_contraente_B, lato), 'stato': val(GlobalData.Stato_contraente_A, GlobalData.Stato_contraente_B, lato), 'tel': val(GlobalData.N_telefono_mail_contraente_A, GlobalData.N_telefono_mail_contraente_B, lato), }, 'veicolo': { 'marca': val(GlobalData.Marca_e_Tipo_A, GlobalData.Marca_e_Tipo_B, lato), 'targa': val(GlobalData.Targa_A, GlobalData.Targa_B, lato), 'stato_imm': val(GlobalData.Stato_immatricolazione_A, GlobalData.Stato_immatricolazione_B, lato), 'stato_imm2': val(GlobalData.Stato_immatricolazione2_A, GlobalData.Stato_immatricolazione2_B, lato), 'rimorchio': val(GlobalData.Rimorchio_A, GlobalData.Rimorchio_B, lato), }, 'assicurazione': { 'denominazione': val(GlobalData.Denominazione_A, GlobalData.Denominazione_B, lato), 'polizza': val(GlobalData.Numero_Polizza_A, GlobalData.Numero_Polizza_B, lato), 'agenzia': val(GlobalData.Agenzia_A, GlobalData.Agenzia_B, lato), 'denom_agenzia': val(GlobalData.Denominazione_agenzia_A, GlobalData.Denominazione_agenzia_B, lato), 'indirizzo_agenzia': val(GlobalData.Indirizzo_agenzia_A, GlobalData.Indirizzo_agenzia_B, lato), 'stato_agenzia': val(GlobalData.Stato_agenzia_A, GlobalData.Stato_agenzia_B, lato), 'tel_agenzia': val(GlobalData.N_tel_mail_agenzia_A, GlobalData.N_tel_mail_agenzia_B, lato), 'carta_verde': val(GlobalData.N_carta_verde_A, GlobalData.N_carta_verde_B, lato), 'validita_dal': val(GlobalData.Data_Inizio_Dal_A, GlobalData.Data_Inizio_Dal_B, lato), 'validita_al': val(GlobalData.Data_Scadenza_Al_A, GlobalData.Data_Scadenza_Al_B, lato), 'flag_danni': valB(GlobalData.FLAG_danni_mat_assicurati_A, GlobalData.FLAG_danni_mat_assicurati_B, lato), }, 'conducente': { 'cognome': val(GlobalData.Cognome_cond_A, GlobalData.Cognome_cond_B, lato), 'nome': val(GlobalData.Nome_cond_A, GlobalData.Nome_cond_B, lato), 'nascita': val(GlobalData.Data_nascita_cond_A, GlobalData.Data_nascita_cond_B, lato), 'cf': val(GlobalData.Cod_fiscale_cond_A, GlobalData.Cod_fiscale_cond_B, lato), 'indirizzo': val(GlobalData.Indirizzo_cond_A, GlobalData.Indirizzo_cond_B, lato), 'stato': val(GlobalData.Stato_cond_A, GlobalData.Stato_cond_B, lato), 'tel': val(GlobalData.N_tel_mail_cond_A, GlobalData.N_tel_mail_cond_B, lato), 'patente': val(GlobalData.N_Patente_cond_A, GlobalData.N_Patente_cond_B, lato), 'cat_patente': val(GlobalData.Categoria_cond_A, GlobalData.Categoria_cond_B, lato), 'scad_patente': val(GlobalData.Scadenza_cond_A, GlobalData.Scadenza_cond_B, lato), }, 'danni_osservazioni': { 'visibili': val(GlobalData.danni_visibili_A, GlobalData.danni_visibili_B, lato), 'osservazioni': val(GlobalData.osservazioni_A, GlobalData.osservazioni_B, lato), } }; } static void importaDati(Map data) { String latoRemoto = data['lato'] ?? 'B'; // 1. Dati Comuni if (data['generali'] != null) { var gen = data['generali']; if ((gen['data'] ?? "").isNotEmpty) GlobalData.data_incidente = gen['data']; if ((gen['ora'] ?? "").isNotEmpty) GlobalData.ora = gen['ora']; if ((gen['luogo'] ?? "").isNotEmpty) GlobalData.luogo = gen['luogo']; if (gen['feriti'] != null) GlobalData.feriti = gen['feriti']; if (gen['testimoni'] != null) GlobalData.testimoni = gen['testimoni']; if (gen['danni_materiali'] != null) GlobalData.Veicoli_danni_materiali_oltre = gen['danni_materiali']; if (gen['oggetti_diversi'] != null) GlobalData.Oggetti_diversi_danni_materiali = gen['oggetti_diversi']; } // --- PROTEZIONE GRAFICO (CASSETTO 1) --- // Solo se i dati arrivano da A, permettiamo di sovrascrivere il grafico comune. // Se arrivano da B, IGNORIAMO questa chiave (così il disegno di A resta intatto). if (latoRemoto == 'A' && data['grafico'] != null) { var graf = data['grafico']; if (graf['tratti_dinamica'] != null) { List listRaw = graf['tratti_dinamica']; GlobalData.tratti = listRaw.map((x) => TrattoPenna.fromMap(x)).toList(); } if (graf['elementi_dinamica'] != null) { List listRaw = graf['elementi_dinamica']; GlobalData.elementi = listRaw.map((x) => ElementoGrafico.fromMap(x)).toList(); } } // --- PROTEZIONE FIRMA (CASSETTO 2) --- // La firma NON viene bloccata. Ognuno ha la sua firma e deve poterla inviare. // Viene salvata in variabili distinte (puntiFirmaA e puntiFirmaB). if (data['firma'] != null) { List puntiFirma = _deserializePunti(data['firma']); if (latoRemoto == 'A') GlobalData.puntiFirmaA = puntiFirma; else GlobalData.puntiFirmaB = puntiFirma; // Qui B salva la sua firma } // 3. Circostanze if (data['circostanze'] != null) { Map rawCirc = data['circostanze']; Map mappaCirc = {}; rawCirc.forEach((k, v) => mappaCirc[int.tryParse(k) ?? 0] = v as bool); if (latoRemoto == 'A') GlobalData.circostanzeA = mappaCirc; else GlobalData.circostanzeB = mappaCirc; } // 4. Punti Urto if (data['punti_urto'] != null) { List puntiRecuperati = List.from(data['punti_urto']); if (latoRemoto == 'A') { GlobalData.puntiUrtoA_List = puntiRecuperati; } else { GlobalData.puntiUrtoB_List = puntiRecuperati; } } void setVal(Function(String) setA, Function(String) setB, dynamic val) { if (val == null) return; if (latoRemoto == 'A') setA(val.toString()); else setB(val.toString()); } void setBool(Function(bool) setA, Function(bool) setB, dynamic val) { if (val == null) return; if (latoRemoto == 'A') setA(val as bool); else setB(val as bool); } if (data['contraente'] != null) { var c = data['contraente']; setVal((v) => GlobalData.Cognome_contraente_A = v, (v) => GlobalData.Cognome_contraente_B = v, c['cognome']); setVal((v) => GlobalData.Nome_contraente_A = v, (v) => GlobalData.Nome_contraente_B = v, c['nome']); setVal((v) => GlobalData.Codice_Fiscale_contraente_A = v, (v) => GlobalData.Codice_Fiscale_contraente_B = v, c['cf']); setVal((v) => GlobalData.Indirizzo_contraente_A = v, (v) => GlobalData.Indirizzo_contraente_B = v, c['indirizzo']); setVal((v) => GlobalData.CAP_contraente_A = v, (v) => GlobalData.CAP_contraente_B = v, c['cap']); setVal((v) => GlobalData.Stato_contraente_A = v, (v) => GlobalData.Stato_contraente_B = v, c['stato']); setVal((v) => GlobalData.N_telefono_mail_contraente_A = v, (v) => GlobalData.N_telefono_mail_contraente_B = v, c['tel']); } if (data['veicolo'] != null) { var v = data['veicolo']; setVal((v) => GlobalData.Marca_e_Tipo_A = v, (v) => GlobalData.Marca_e_Tipo_B = v, v['marca']); setVal((v) => GlobalData.Targa_A = v, (v) => GlobalData.Targa_B = v, v['targa']); setVal((v) => GlobalData.Stato_immatricolazione_A = v, (v) => GlobalData.Stato_immatricolazione_B = v, v['stato_imm']); setVal((v) => GlobalData.Stato_immatricolazione2_A = v, (v) => GlobalData.Stato_immatricolazione2_B = v, v['stato_imm2']); setVal((v) => GlobalData.Rimorchio_A = v, (v) => GlobalData.Rimorchio_B = v, v['rimorchio']); } if (data['assicurazione'] != null) { var a = data['assicurazione']; setVal((v) => GlobalData.Denominazione_A = v, (v) => GlobalData.Denominazione_B = v, a['denominazione']); setVal((v) => GlobalData.Numero_Polizza_A = v, (v) => GlobalData.Numero_Polizza_B = v, a['polizza']); setVal((v) => GlobalData.Agenzia_A = v, (v) => GlobalData.Agenzia_B = v, a['agenzia']); setVal((v) => GlobalData.Denominazione_agenzia_A = v, (v) => GlobalData.Denominazione_agenzia_B = v, a['denom_agenzia']); setVal((v) => GlobalData.Indirizzo_agenzia_A = v, (v) => GlobalData.Indirizzo_agenzia_B = v, a['indirizzo_agenzia']); setVal((v) => GlobalData.Stato_agenzia_A = v, (v) => GlobalData.Stato_agenzia_B = v, a['stato_agenzia']); setVal((v) => GlobalData.N_tel_mail_agenzia_A = v, (v) => GlobalData.N_tel_mail_agenzia_B = v, a['tel_agenzia']); setVal((v) => GlobalData.N_carta_verde_A = v, (v) => GlobalData.N_carta_verde_B = v, a['carta_verde']); setVal((v) => GlobalData.Data_Inizio_Dal_A = v, (v) => GlobalData.Data_Inizio_Dal_B = v, a['validita_dal']); setVal((v) => GlobalData.Data_Scadenza_Al_A = v, (v) => GlobalData.Data_Scadenza_Al_B = v, a['validita_al']); setBool((v) => GlobalData.FLAG_danni_mat_assicurati_A = v, (v) => GlobalData.FLAG_danni_mat_assicurati_B = v, a['flag_danni']); } if (data['conducente'] != null) { var c = data['conducente']; setVal((v) => GlobalData.Cognome_cond_A = v, (v) => GlobalData.Cognome_cond_B = v, c['cognome']); setVal((v) => GlobalData.Nome_cond_A = v, (v) => GlobalData.Nome_cond_B = v, c['nome']); setVal((v) => GlobalData.Data_nascita_cond_A = v, (v) => GlobalData.Data_nascita_cond_B = v, c['nascita']); setVal((v) => GlobalData.Cod_fiscale_cond_A = v, (v) => GlobalData.Cod_fiscale_cond_B = v, c['cf']); setVal((v) => GlobalData.Indirizzo_cond_A = v, (v) => GlobalData.Indirizzo_cond_B = v, c['indirizzo']); setVal((v) => GlobalData.Stato_cond_A = v, (v) => GlobalData.Stato_cond_B = v, c['stato']); setVal((v) => GlobalData.N_tel_mail_cond_A = v, (v) => GlobalData.N_tel_mail_cond_B = v, c['tel']); setVal((v) => GlobalData.N_Patente_cond_A = v, (v) => GlobalData.N_Patente_cond_B = v, c['patente']); setVal((v) => GlobalData.Categoria_cond_A = v, (v) => GlobalData.Categoria_cond_B = v, c['cat_patente']); setVal((v) => GlobalData.Scadenza_cond_A = v, (v) => GlobalData.Scadenza_cond_B = v, c['scad_patente']); } if (data['danni_osservazioni'] != null) { var d = data['danni_osservazioni']; setVal((v) => GlobalData.danni_visibili_A = v, (v) => GlobalData.danni_visibili_B = v, d['visibili']); setVal((v) => GlobalData.osservazioni_A = v, (v) => GlobalData.osservazioni_B = v, d['osservazioni']); } } // =========================================================================== // METODI PER SALVATAGGIO CLOUD (FIREBASE) // =========================================================================== static Future salvaDati(String sessionId, String lato) async { final docRef = FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId); // Estrae tutti i dati dalla memoria locale Map datiExport = estraiDatiPerExport(); // Prepariamo i dati da inviare al server Map updateData = { 'generali': datiExport['generali'], 'lato_$lato': { ...datiExport['contraente'], ...datiExport['veicolo'], ...datiExport['assicurazione'], ...datiExport['conducente'], 'danni_visibili': datiExport['danni_osservazioni']['visibili'], 'osservazioni': datiExport['danni_osservazioni']['osservazioni'], 'circostanze': datiExport['circostanze'], 'punti_urto': datiExport['punti_urto'], 'firma': datiExport['firma'], // <-- La firma è dentro "lato_A" o "lato_B", quindi è al sicuro 'completo': true, }, 'timestamp': FieldValue.serverTimestamp(), }; // --- PROTEZIONE AGGIUNTIVA PER IL CLOUD --- // Il GRAFICO lo aggiorniamo nel DB solo se siamo il LATO A. // Il LATO B non ha diritto di toccare questa chiave. if (lato == 'A') { updateData['grafico'] = datiExport['grafico']; } await docRef.set(updateData, SetOptions(merge: true)); return sessionId; } // --- METODO RECUPERA DATI CLOUD --- static Future caricaDati(String sessionId, String latoDaCaricare) async { final doc = await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).get(); if (!doc.exists) return; final data = doc.data()!; Map datiDaImportare = {}; datiDaImportare['lato'] = latoDaCaricare; if (data['generali'] != null) datiDaImportare['generali'] = data['generali']; if (data['grafico'] != null) datiDaImportare['grafico'] = data['grafico']; if (data['lato_$latoDaCaricare'] != null) { Map latoData = data['lato_$latoDaCaricare']; datiDaImportare['contraente'] = latoData; datiDaImportare['veicolo'] = latoData; datiDaImportare['assicurazione'] = latoData; datiDaImportare['conducente'] = latoData; datiDaImportare['danni_osservazioni'] = { 'visibili': latoData['danni_visibili'], 'osservazioni': latoData['osservazioni'], }; datiDaImportare['circostanze'] = latoData['circostanze']; datiDaImportare['punti_urto'] = latoData['punti_urto']; datiDaImportare['firma'] = latoData['firma']; // Recupera la firma specifica dal lato } importaDati(datiDaImportare); } // =========================================================================== // HELPER SERIALIZZAZIONE // =========================================================================== static Map _serializeCircostanze(Map circ) { return circ.map((key, value) => MapEntry(key.toString(), value)); } static List> _serializePunti(List punti) { List> res = []; for (var p in punti) { if (p != null) res.add({'dx': p.dx, 'dy': p.dy}); } return res; } static List _deserializePunti(dynamic lista) { if (lista is! List) return []; return lista.map((item) { if (item == null) return null; return Offset((item['dx'] as num).toDouble(), (item['dy'] as num).toDouble()); }).toList(); } } === FILE: lib/firebase_options.dart === // File: lib/firebase_options.dart import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform, kIsWeb; class DefaultFirebaseOptions { static FirebaseOptions get currentPlatform { if (kIsWeb) { throw UnsupportedError( 'DefaultFirebaseOptions have not been configured for web', ); } switch (defaultTargetPlatform) { // ======================================================= // 🤖 CONFIGURAZIONE ANDROID (Compilata con i tuoi dati JSON) // ======================================================= case TargetPlatform.android: return const FirebaseOptions( apiKey: 'AIzaSyB9f420xu5fu_aaaFgvRYMQVS2L8Ddudbo', // Presa dal tuo JSON appId: '1:1060927868658:android:84884a85cd60c4e8084a6b', // Presa dal tuo JSON messagingSenderId: '1060927868658', projectId: 'cid-app-sincro', storageBucket: 'cid-app-sincro.firebasestorage.app', databaseURL: 'https://cid-app-sincro-default-rtdb.europe-west1.firebasedatabase.app', ); // ======================================================= // 🍎 CONFIGURAZIONE iOS (Compilata con i tuoi dati PLIST) // ======================================================= case TargetPlatform.iOS: return const FirebaseOptions( apiKey: 'AIzaSyBDEXTqdXbwNVeK4yy8m9uMwk1xzeSYWJ8', appId: '1:1060927868658:ios:be05f4b773c220e3084a6b', messagingSenderId: '1060927868658', projectId: 'cid-app-sincro', storageBucket: 'cid-app-sincro.firebasestorage.app', databaseURL: 'https://cid-app-sincro-default-rtdb.europe-west1.firebasedatabase.app', iosBundleId: 'com.example.cidApp', ); case TargetPlatform.macOS: throw UnsupportedError('MacOS not configured'); case TargetPlatform.windows: throw UnsupportedError('Windows not configured'); case TargetPlatform.linux: throw UnsupportedError('Linux not configured'); default: throw UnsupportedError( 'DefaultFirebaseOptions are not supported for this platform.', ); } } } === FILE: lib/build_cai_app.dart === import 'dart:io'; void main() async { print("🛠️ Avvio build..."); // 1. Esegui la build var process = await Process.start('flutter', ['build', 'appbundle']); // Mostra l'output in tempo reale stdout.addStream(process.stdout); stderr.addStream(process.stderr); var exitCode = await process.exitCode; if (exitCode != 0) { print("❌ Errore nella build."); return; } // 2. Leggi la versione dal pubspec.yaml var pubspec = await File('pubspec.yaml').readAsLines(); var versionLine = pubspec.firstWhere((line) => line.startsWith('version:')); var version = versionLine.split('version: ')[1].trim(); // 3. DEFINIZIONE PERCORSI // A. DOVE SI TROVA ORA (Sempre nella cartella del progetto) var sourceFile = File('build/app/outputs/bundle/release/app-release.aab'); // B. DOVE LO VUOI METTERE (Il tuo disco esterno) // Nota: Assicurati che la cartella "buid" esista o correggi in "build" se era un refuso var targetDirectory = '/Volumes/NVME-2TB/cai/buid'; if (await sourceFile.exists()) { // Crea la directory sul disco esterno se non esiste (per sicurezza) await Directory(targetDirectory).create(recursive: true); // Crea il percorso completo di destinazione var newPath = '$targetDirectory/CAI_App_v$version.aab'; // Esegue la copia await sourceFile.copy(newPath); print("✅ File creato con successo!"); print("📂 Destinazione: $newPath"); } else { print("❌ Impossibile trovare il file generato in build/app/outputs/..."); } } === FILE: lib/comp_12.dart === // Versione: FINAL - SLIVER LAYOUT (Footer Sicuro e Testi Corretti) import 'package:flutter/material.dart'; import 'global_data.dart'; import 'comp_13.dart'; // IMPORTA IL GRAFICO import 'comp_15.dart'; // IMPORTA LE FIRME class Comp12Screen extends StatefulWidget { const Comp12Screen({super.key}); @override _Comp12ScreenState createState() => _Comp12ScreenState(); } class _Comp12ScreenState extends State { final List _testiCircostanze = [ "1. In fermata / in sosta", "2. Ripartiva dopo una sosta / apriva una portiera", "3. Stava parcheggiando", "4. Usciva da un parcheggio / luogo privato", "5. Entrava in un parcheggio / luogo privato", "6. Si immetteva in una piazza a senso rotatorio", "7. Circolava su una piazza a senso rotatorio", "8. Tamponava procedendo nello stesso senso", "9. Procedeva nello stesso senso ma in fila diversa", "10. Cambiava fila", "11. Sorpassava", "12. Girava a destra", "13. Girava a sinistra", "14. Retrocedeva", "15. Invadeva la sede stradale riservata", "16. Proveniva da destra", "17. Non osservava il segnale di precedenza/semaforo" ]; bool _isReady = false; @override void initState() { super.initState(); _inizializzaPagina(); } Future _inizializzaPagina() async { await Future.delayed(const Duration(milliseconds: 200)); if (mounted) { setState(() => _isReady = true); // MOSTRA IL POPUP ANIMATO ALL'AVVIO WidgetsBinding.instance.addPostFrameCallback((_) => _mostraInfoPopup(context)); } } // --- POPUP INFORMATIVO ANIMATO --- void _mostraInfoPopup(BuildContext context) { bool isB = GlobalData.latoCorrente == 'B'; 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.fact_check, color: activeColor, size: 28), const SizedBox(width: 10), const Expanded(child: Text("Circostanze Incidente", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text("Indica l'esatta dinamica del Veicolo ${GlobalData.latoCorrente} al momento dell'urto.", style: const TextStyle(fontSize: 15)), const SizedBox(height: 16), _buildPopupRow(Icons.check_box_outlined, "Selezione Multipla", "Puoi spuntare anche più di una circostanza, ma assicurati che descrivano correttamente l'accaduto."), const SizedBox(height: 12), _buildPopupRow(Icons.warning_amber_rounded, "Attenzione", "Le circostanze selezionate in questa pagina sono fondamentali per stabilire la responsabilità del sinistro!"), ], ), ), 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( 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), ], ), ), ), ], ); } void _prosegui() { bool isB = GlobalData.latoCorrente == 'B'; if (isB) { // IL LATO B SALTA IL GRAFICO E VA DIRETTAMENTE ALLE FIRME (15) Navigator.push( context, MaterialPageRoute(builder: (context) => const Comp15Screen()), ); } else { // IL LATO A VA AL GRAFICO (13) Navigator.push( context, MaterialPageRoute(builder: (context) => const Comp13Screen()), ); } } @override Widget build(BuildContext context) { bool isB = GlobalData.latoCorrente == 'B'; Color mainCol = isB ? Colors.amber.shade700 : Colors.blue.shade900; Color bgCol = isB ? const Color(0xFFFFF9C4) : const Color(0xFFF5F7FA); if (!_isReady) { return Scaffold(backgroundColor: bgCol, body: Container()); } return Scaffold( backgroundColor: bgCol, appBar: AppBar( title: Text("12. Circostanze (${GlobalData.latoCorrente})"), backgroundColor: mainCol, foregroundColor: isB ? Colors.black : Colors.white, ), // --- SLIVER LAYOUT --- body: SafeArea( child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // 1. Header (Testo Istruzioni) SliverToBoxAdapter( child: Container( padding: const EdgeInsets.all(15), color: Colors.white, margin: const EdgeInsets.only(bottom: 1), child: Text( "Seleziona le circostanze per il veicolo ${GlobalData.latoCorrente}.", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: mainCol), ), ), ), // 2. Lista Scrollabile (Le 17 Checkbox) SliverList( delegate: SliverChildBuilderDelegate( (context, index) { int circIndex = index + 1; bool isChecked = isB ? (GlobalData.circostanzeB[circIndex] ?? false) : (GlobalData.circostanzeA[circIndex] ?? false); return Column( children: [ Container( color: Colors.white, child: CheckboxListTile( activeColor: mainCol, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), title: Text("${circIndex}. ${_testiCircostanze[index]}", style: const TextStyle(fontSize: 14)), value: isChecked, onChanged: (bool? val) { setState(() { if (isB) { GlobalData.circostanzeB[circIndex] = val ?? false; } else { GlobalData.circostanzeA[circIndex] = val ?? false; } }); }, ), ), const Divider(height: 1, thickness: 1, indent: 16, endIndent: 16), ], ); }, childCount: _testiCircostanze.length, ), ), // 3. Piè di pagina elastico (Bottoni) SliverFillRemaining( hasScrollBody: false, child: Align( alignment: Alignment.bottomCenter, child: Container( padding: const EdgeInsets.all(20), child: _navButtons(context, mainCol, isB), ), ), ), ], ), ), ); } Widget _navButtons(BuildContext context, Color color, bool isB) { return Row( children: [ // Tasto INDIETRO (Expanded flex 1) Expanded( flex: 4, // Proporzione 4/10 child: OutlinedButton( onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 55), padding: const EdgeInsets.symmetric(horizontal: 5), // Padding ridotto ), // FittedBox evita che il testo vada a capo se lo spazio è poco child: const FittedBox( child: Text("INDIETRO", style: TextStyle(fontWeight: FontWeight.bold)) ) ) ), const SizedBox(width: 15), // Tasto SALVA E PROCEDI (Expanded flex 2) Expanded( flex: 6, // Proporzione 6/10 (più largo) child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: isB ? Colors.black : Colors.white, minimumSize: const Size(0, 55) ), onPressed: _prosegui, child: const FittedBox( child: Text("SALVA E PROCEDI", style: TextStyle(fontWeight: FontWeight.bold)) ) ) ), ], ); } } === FILE: lib/utils/cai_constants.dart === class CaiMapping { static const Map fields = { // --- 1. INFORMAZIONI GENERALI --- 'dataIncidente': 'data sinistro', 'oraIncidente': 'ora', 'luogoIncidente': 'luogo sinistro', // --- 2. VEICOLO A (Colonna Sinistra) --- // Contraente/Assicurato A 'cognomeContraenteA': 'cognome', 'nomeContraenteA': 'nome', 'codiceFiscaleA': 'codice fiscale', 'comuneA': 'comune', 'capA': 'cap', 'statoA': 'stato', // Dati Veicolo A 'marcaVeicoloA': 'marca e tipo', 'targaA': 'targa', 'statoImmatricolazioneA': 'stato immatricolazione', // Compagnia Assicurativa A 'compagniaA': 'COMPAGNIA', 'numeroPolizzaA': 'numero polizza', 'agenziaA': 'AGENZIA', // Conducente A (ATTENZIONE: Refuso 'cogmome' nel PDF originale) 'cognomeConducenteA': 'cogmome', 'nomeConducenteA': 'Nome', 'codiceFiscaleConducenteA': 'Codice fiscale', 'patenteA': 'numero patente', // --- 3. VEICOLO B (Colonna Destra) --- // Contraente/Assicurato B 'cognomeContraenteB': 'Cognome assicurato', 'nomeContraenteB': 'Nome Assicurato', 'codiceFiscaleB': 'codice fiscale assicurato', 'comuneB': 'comune/prov/indirizzo', // Dati Veicolo B 'marcaVeicoloB': 'marca e modello', 'targaB': 'targa1', 'statoImmatricolazioneB': 'stato immatricolazione1', // Compagnia Assicurativa B 'compagniaB': 'compagnia1', 'numeroPolizzaB': 'numero polizza1', 'agenziaB': 'agenzia1', // Conducente B 'cognomeConducenteB': 'cognome1', 'nomeConducenteB': 'nome1', 'codiceFiscaleConducenteB': 'codice fiscale1', 'patenteB': 'num patente', // --- 4. TESTIMONI E OSSERVAZIONI --- 'testimone1': '1° teste', 'testimone2': '2° teste', 'osservazioniA': 'osservazioni', 'osservazioniB': 'osservazioni1', }; } === FILE: lib/test_scraping.dart === import 'package:flutter/material.dart'; import 'verifica_rca_screen.dart'; // Assicurati che l'import sia corretto void main() { runApp(const MaterialApp( home: TestScrapingPage(), )); } class TestScrapingPage extends StatefulWidget { const TestScrapingPage({super.key}); @override _TestScrapingPageState createState() => _TestScrapingPageState(); } class _TestScrapingPageState extends State { // Metti una targa vera qui per fare prima nei test final TextEditingController _targaController = TextEditingController(text: "AB123CD"); String _risultato = "Nessun dato ancora"; void _lanciaVerifica() async { // 1. Lancia la schermata di verifica final dataScadenza = await Navigator.push( context, MaterialPageRoute( builder: (context) => VerificaRcaScreen(targa: _targaController.text), ), ); // 2. Quando torni indietro, se c'è un risultato, mostralo if (dataScadenza != null) { setState(() { _risultato = "Scadenza trovata: $dataScadenza"; }); // Feedback visivo ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("Data importata: $dataScadenza"), backgroundColor: Colors.green), ); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("TEST SCRAPING RCA"), backgroundColor: Colors.orange), body: Padding( padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("Inserisci una targa reale per testare:", style: TextStyle(fontSize: 16)), const SizedBox(height: 10), TextField( controller: _targaController, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: "Targa", hintText: "Es. GA000GA", ), textCapitalization: TextCapitalization.characters, ), const SizedBox(height: 30), ElevatedButton.icon( onPressed: _lanciaVerifica, icon: const Icon(Icons.search), label: const Text("VERIFICA COPERTURA"), style: ElevatedButton.styleFrom( minimumSize: const Size(double.infinity, 50), textStyle: const TextStyle(fontSize: 18), ), ), const SizedBox(height: 40), const Divider(), const Text("RISULTATO:", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)), const SizedBox(height: 10), Text( _risultato, style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue), ), ], ), ), ); } } === FILE: lib/main.dart === import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; // ⚠️ IMPORTANTE: Assicurati che questo file esista (generato da flutterfire configure) import 'firebase_options.dart'; import 'global_data.dart'; import 'scelta_lato.dart'; import 'carro_attr.dart'; import 'ps.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // 1. BLOCCO INIZIALE VERTICALE await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); // 2. INIZIALIZZAZIONE FIREBASE CON BIVIO IOS/ANDROID // Il controllo .isEmpty evita il crash "Duplicate App" durante il debug if (Firebase.apps.isEmpty) { await Firebase.initializeApp( // Questa riga gestisce automaticamente il bivio tra le chiavi Android e iOS // leggendole dal file firebase_options.dart options: DefaultFirebaseOptions.currentPlatform, ); } _effettuaLoginAnonimo(); runApp(const MyApp()); } Future _effettuaLoginAnonimo() async { try { if (FirebaseAuth.instance.currentUser == null) { await FirebaseAuth.instance.signInAnonymously(); debugPrint("✅ Login anonimo effettuato."); } } catch (e) { debugPrint("⚠️ Login anonimo fallito (ritento tra 2s): $e"); Future.delayed(const Duration(seconds: 2), _effettuaLoginAnonimo); } } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'La tua App CAI', debugShowCheckedModeBanner: false, // --- IMPOSTIAMO I COLORI GLOBALI DEL CALENDARIO E DELL'APP --- theme: ThemeData.light().copyWith( colorScheme: const ColorScheme.light( primary: Color(0xFF1565C0), // Blu (usato per i calendari) onPrimary: Colors.white, onSurface: Colors.black, ), ), localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('it', 'IT'), ], home: const HomeScreen(), ); } } class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State with RouteAware { @override void initState() { super.initState(); // Al primo avvio puliamo tutto _resetCompleto(); } // Metodo centralizzato per reset e orientamento Future _resetCompleto() async { GlobalData.reset(); await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); debugPrint("🏠 Home: Dati resettati e Verticale forzato."); } @override Widget build(BuildContext context) { // Intercettiamo il "ritorno" alla home per forzare il verticale // (utile se si torna indietro con gesture o tasto back fisico) return PopScope( canPop: false, // La home è la root, non si esce child: Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( title: const Text( 'CAI Facile', style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5, fontSize: 24) ), centerTitle: true, backgroundColor: Colors.blue.shade900.withOpacity(0.95), foregroundColor: Colors.white, elevation: 10, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)) ), ), body: Stack( children: [ // 1. Sfondo Base (Mappa) const Positioned.fill(child: BackgroundImage()), // Contenuto SafeArea( child: Center( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // 1. Compila Sinistro _buildButton3D( context: context, label: "COMPILA SINISTRO", icon: Icons.assignment_outlined, baseColor: Colors.blue.shade800, onTap: () async { // Reset esplicito prima di iniziare await _resetCompleto(); if (context.mounted) { Navigator.push( context, MaterialPageRoute(builder: (c) => const SceltaLatoScreen()) ).then((_) { // Quando l'utente torna indietro dalla compilazione, // forziamo di nuovo il reset e il verticale _resetCompleto(); }); } }, ), const SizedBox(height: 25), // 2. Carro Attrezzi _buildButton3D( context: context, label: "SOS CARRO ATTREZZI", icon: Icons.support_agent, baseColor: Colors.red.shade800, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (c) => const CarroAttrezziScreen())); }, ), const SizedBox(height: 25), // 3. Pronto Soccorso _buildButton3D( context: context, label: "PRONTO SOCCORSO", icon: Icons.local_hospital_outlined, baseColor: Colors.green.shade800, onTap: () { Navigator.push(context, MaterialPageRoute(builder: (c) => const ProntoSoccorsoScreen())); }, ), ], ), ), ), ), ), ], ), ), ); } // Widget helper per i bottoni con effetto 3D Widget _buildButton3D({ required BuildContext context, required String label, required IconData icon, required Color baseColor, required VoidCallback onTap, }) { return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 6), blurRadius: 8, spreadRadius: 1) ], gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ baseColor.withOpacity(0.9), baseColor, baseColor.withRed((baseColor.red - 20).clamp(0, 255)) ], ), ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(16), onTap: onTap, splashColor: Colors.white.withOpacity(0.2), child: Padding( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20), child: Row( children: [ Icon(icon, size: 32, color: Colors.white), const SizedBox(width: 20), Expanded( child: Text( label, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.w800, fontSize: 18, letterSpacing: 1.0 ) ) ), Icon(Icons.arrow_forward_ios, size: 18, color: Colors.white.withOpacity(0.7)), ], ), ), ), ), ); } } // Widget Sfondo class BackgroundImage extends StatelessWidget { const BackgroundImage({super.key}); @override Widget build(BuildContext context) { return Container( color: const Color(0xFFF0F4F8), child: Image.asset( 'assets/sfondo_mappa.jpg', fit: BoxFit.cover, color: const Color(0xFFF0F4F8).withOpacity(0.6), colorBlendMode: BlendMode.lighten, errorBuilder: (c, e, s) => Container(color: Colors.grey.shade200), ), ); } } === FILE: lib/verifica_rca_screen.dart === import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; class VerificaRcaScreen extends StatefulWidget { final String targa; const VerificaRcaScreen({super.key, required this.targa}); @override _VerificaRcaScreenState createState() => _VerificaRcaScreenState(); } class _VerificaRcaScreenState extends State { InAppWebViewController? webViewController; bool isLoading = true; String? dataScadenzaTrovata; bool ricercaFallita = false; final String urlPortale = "https://www.ilportaledellautomobilista.it/web/portale-automobilista/verifica-copertura-rc"; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Verifica Copertura RCA"), backgroundColor: Colors.blue[900], foregroundColor: Colors.white, ), body: Stack( children: [ // 1. WEBVIEW (Il motore nascosto/mascherato) InAppWebView( initialUrlRequest: URLRequest(url: WebUri(urlPortale)), initialSettings: InAppWebViewSettings( javaScriptEnabled: true, supportZoom: false, ), onWebViewCreated: (controller) { webViewController = controller; }, onLoadStop: (controller, url) async { setState(() { isLoading = false; }); // Appena caricata, nascondiamo la grafica del sito await _preparaPagina(); }, onProgressChanged: (controller, progress) { // Ogni volta che la pagina cambia (es. dopo il click su Cerca), controlliamo se c'è il risultato if (progress == 100) { _cercaRisultato(); } }, ), // 2. LOADER / SCHERMATA SUCCESSO (Copre la WebView quando serve) if (isLoading || dataScadenzaTrovata != null || ricercaFallita) Container( color: Colors.white, width: double.infinity, height: double.infinity, child: Center( child: _buildOverlayContent(), ), ), ], ), ); } Widget _buildOverlayContent() { if (ricercaFallita) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, color: Colors.red, size: 60), const SizedBox(height: 20), const Text("Veicolo non assicurato\no targa errata", textAlign: TextAlign.center, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 30), ElevatedButton( onPressed: () { setState(() { ricercaFallita = false; isLoading = false; // Rimostra la webview per riprovare webViewController?.reload(); }); }, child: const Text("Riprova"), ) ], ); } if (dataScadenzaTrovata != null) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.check_circle, color: Colors.green, size: 80), const SizedBox(height: 20), const Text("Scadenza Trovata!", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)), const SizedBox(height: 10), Text(dataScadenzaTrovata!, style: TextStyle(fontSize: 30, color: Colors.blue[900], fontWeight: FontWeight.bold)), const SizedBox(height: 40), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue[900], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15), ), onPressed: () { Navigator.pop(context, dataScadenzaTrovata); }, child: const Text("USA QUESTA DATA", style: TextStyle(fontSize: 18)), ) ], ); } return const CircularProgressIndicator(); } // --- FUNZIONI DI SCRAPING --- Future _preparaPagina() async { if (webViewController == null) return; // 1. INIEZIONE CSS: Stile "App Mobile" await webViewController!.evaluateJavascript(source: """ // Nascondi tutto il contorno inutile var elementsToHide = document.querySelectorAll('header, footer, .navbar, .breadcrumb, #cookie-bar, .portlet-title, .lfr-meta-actions'); elementsToHide.forEach(el => el.style.display = 'none'); var mainForm = document.querySelector('form'); if(mainForm) { document.body.innerHTML = ''; document.body.appendChild(mainForm); document.body.style.backgroundColor = '#ffffff'; document.body.style.padding = '20px'; document.body.style.fontFamily = 'sans-serif'; } """); // 2. INIEZIONE JS: Compilazione e UX await webViewController!.evaluateJavascript(source: """ // A. Compila Targa var campoTarga = document.getElementById('targa'); if (campoTarga) { campoTarga.value = '${widget.targa}'; campoTarga.style.fontSize = '24px'; campoTarga.style.fontWeight = 'bold'; campoTarga.style.textAlign = 'center'; campoTarga.style.border = '2px solid #1565C0'; campoTarga.readOnly = true; } // B. Seleziona "Autoveicolo" var selectVeicolo = document.querySelector('select'); if (selectVeicolo) { for (var i = 0; i < selectVeicolo.options.length; i++) { if (selectVeicolo.options[i].text.toLowerCase().includes('auto')) { selectVeicolo.selectedIndex = i; break; } } // Nascondi la select e la sua label selectVeicolo.style.display = 'none'; if(selectVeicolo.previousElementSibling) selectVeicolo.previousElementSibling.style.display = 'none'; } // C. Restyling Bottone Ricerca var btn = document.querySelector("input[name='ricercaCoperturaVeicolo']"); if (btn) { btn.style.cssText = ''; btn.style.width = '100%'; btn.style.height = '60px'; btn.style.backgroundColor = '#1565C0'; btn.style.color = 'white'; btn.style.border = 'none'; btn.style.borderRadius = '12px'; btn.style.fontSize = '20px'; btn.style.marginTop = '30px'; btn.value = 'CERCA SCADENZA'; } // D. Focus sul Captcha var campoCaptcha = document.querySelector("input[name*='captcha']"); if(campoCaptcha) { campoCaptcha.placeholder = 'Inserisci i caratteri qui'; campoCaptcha.style.height = '50px'; campoCaptcha.style.fontSize = '20px'; campoCaptcha.style.textAlign = 'center'; campoCaptcha.style.marginTop = '10px'; campoCaptcha.scrollIntoView(); } """); } Future _cercaRisultato() async { if (webViewController == null) return; // Leggiamo tutto l'HTML della pagina String? html = await webViewController!.getHtml(); if (html == null) return; // CASO 1: SUCCESSO if (html.contains("Scadenza copertura")) { // Eseguiamo JS per estrarre la data precisa var dataEstratta = await webViewController!.evaluateJavascript(source: """ (function() { // Cerca tutti i 'td' (celle di tabella) var cells = document.querySelectorAll('td'); for (var i = 0; i < cells.length; i++) { // Se la cella contiene la label... if (cells[i].innerText.includes('Scadenza copertura')) { // ...prendi il testo della cella successiva (dove c'è la data) var nextCell = cells[i].nextElementSibling; return nextCell ? nextCell.innerText : null; } } return null; })(); """); if (dataEstratta != null && dataEstratta.toString().trim().isNotEmpty) { setState(() { dataScadenzaTrovata = dataEstratta.toString().trim(); }); } } // CASO 2: FALLIMENTO (Non assicurata o targa errata) else if (html.contains("dal controllo non risulta coperto") || html.contains("Targa non trovata")) { setState(() { ricercaFallita = true; }); } // CASO 3: ANCORA NEL FORM (Es. captcha errato) else { // Riapplichiamo lo stile grafico perché il reload della pagina potrebbe averlo resettato _preparaPagina(); } } } === FILE: lib/comp_10.dart === // Versione: FINAL - FIX CONTATORE VISIBILE import 'package:flutter/material.dart'; import 'global_data.dart'; import 'comp_12.dart'; class Comp10Screen extends StatefulWidget { const Comp10Screen({super.key}); @override _Comp10ScreenState createState() => _Comp10ScreenState(); } class _Comp10ScreenState extends State { // Set per gestire selezioni multiple uniche Set _selectedSectors = {}; final double _canvasWidth = 300.0; final double _canvasHeight = 220.0; late TextEditingController _controllerDanni; late TextEditingController _controllerOsservazioni; final bool isB = GlobalData.latoCorrente == 'B'; late Map _hitboxes; @override void initState() { super.initState(); _selectedSectors = isB ? Set.from(GlobalData.puntiUrtoB_List) : Set.from(GlobalData.puntiUrtoA_List); _controllerDanni = TextEditingController(text: isB ? GlobalData.danni_visibili_B : GlobalData.danni_visibili_A); _controllerOsservazioni = TextEditingController(text: isB ? GlobalData.osservazioni_B : GlobalData.osservazioni_A); _hitboxes = isB ? _getHitboxesB() : _getHitboxesA(); // MOSTRA IL POPUP INFORMATIVO ANIMATO AL CARICAMENTO WidgetsBinding.instance.addPostFrameCallback((_) { _mostraInfoPopup(context); }); } // --- POPUP INFORMATIVO --- 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.minor_crash, color: activeColor, size: 28), const SizedBox(width: 10), const Expanded(child: Text("Danni e Osservazioni", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text("In questa sezione indicherai i danni subiti dal Veicolo ${GlobalData.latoCorrente}.", style: const TextStyle(fontSize: 15)), const SizedBox(height: 16), _buildPopupRow(Icons.touch_app, "Punto d'urto", "Tocca direttamente sull'immagine per inserire una 'X' nel punto d'urto. Puoi selezionare più punti. Tocca di nuovo per rimuovere la 'X'."), const SizedBox(height: 12), _buildPopupRow(Icons.edit_document, "Danni Visibili", "Descrivi brevemente i danni visibili (es. 'Paraurti rotto')."), const SizedBox(height: 12), _buildPopupRow(Icons.comment, "Osservazioni", "Aggiungi eventuali commenti personali sulla dinamica (opzionale)."), ], ), ), 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( 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), ], ), ), ), ], ); } // --- MAPPATURA LATO A (BLU) --- Map _getHitboxesA() { return { '9': const Rect.fromLTWH(35, 85, 40, 40), '10': const Rect.fromLTWH(25, 110, 25, 50), '11': const Rect.fromLTWH(60, 110, 25, 50), '12': const Rect.fromLTWH(35, 175, 40, 40), '1': const Rect.fromLTWH(90, 50, 35, 40), '8': const Rect.fromLTWH(127, 50, 35, 40), '2': const Rect.fromLTWH(87, 90, 30, 60), '7': const Rect.fromLTWH(138, 90, 30, 60), '3': const Rect.fromLTWH(80, 150, 35, 40), '6': const Rect.fromLTWH(130, 150, 30, 40), '4': const Rect.fromLTWH(100, 190, 25, 30), '5': const Rect.fromLTWH(135, 190, 25, 30), '13': const Rect.fromLTWH(180, 35, 30, 40), '14': const Rect.fromLTWH(210, 35, 30, 40), '15': const Rect.fromLTWH(240, 35, 30, 40), '16': const Rect.fromLTWH(172, 75, 30, 70), '17': const Rect.fromLTWH(240, 75, 30, 70), '19': const Rect.fromLTWH(165, 145, 40, 60), '18': const Rect.fromLTWH(238, 145, 40, 60), '20': const Rect.fromLTWH(200, 195, 50, 25), }; } // --- MAPPATURA LATO B (GIALLO) --- Map _getHitboxesB() { return { 'M9': const Rect.fromLTWH(35, 78, 40, 40), 'M10': const Rect.fromLTWH(25, 110, 25, 50), 'M11': const Rect.fromLTWH(60, 110, 25, 50), 'M12': const Rect.fromLTWH(35, 175, 40, 40), '21': const Rect.fromLTWH(88, 50, 35, 40), '22': const Rect.fromLTWH(130, 50, 35, 40), '23': const Rect.fromLTWH(85, 90, 30, 60), '24': const Rect.fromLTWH(140, 90, 30, 60), '25': const Rect.fromLTWH(95, 190, 25, 30), '26': const Rect.fromLTWH(138, 190, 25, 30), '27': const Rect.fromLTWH(180, 35, 30, 40), '34': const Rect.fromLTWH(210, 35, 30, 40), '28': const Rect.fromLTWH(240, 35, 30, 40), '29': const Rect.fromLTWH(170, 75, 30, 90), '30': const Rect.fromLTWH(245, 75, 30, 90), '31': const Rect.fromLTWH(168, 171, 40, 60), '33': const Rect.fromLTWH(208, 178, 31, 60), '32': const Rect.fromLTWH(239, 171, 40, 60), }; } void _handleTap(TapUpDetails details) { Offset touchPosition = details.localPosition; String? foundSector; _hitboxes.forEach((key, rect) { if (rect.contains(touchPosition)) { foundSector = key; } }); if (foundSector != null) { setState(() { if (_selectedSectors.contains(foundSector)) { _selectedSectors.remove(foundSector); } else { _selectedSectors.add(foundSector!); } }); } } void _salvaEProsegui() { setState(() { if (isB) { GlobalData.puntiUrtoB_List = _selectedSectors.toList(); GlobalData.danni_visibili_B = _controllerDanni.text.toUpperCase(); GlobalData.osservazioni_B = _controllerOsservazioni.text.toUpperCase(); } else { GlobalData.puntiUrtoA_List = _selectedSectors.toList(); GlobalData.danni_visibili_A = _controllerDanni.text.toUpperCase(); GlobalData.osservazioni_A = _controllerOsservazioni.text.toUpperCase(); } }); Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp12Screen())); } @override Widget build(BuildContext context) { Color mainCol = isB ? Colors.amber.shade700 : Colors.blue.shade900; Color bgCol = isB ? const Color(0xFFFFF9C4) : const Color(0xFFE3F2FD); return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( backgroundColor: bgCol, appBar: AppBar( title: Text("10, 11, 14. Dettagli (${GlobalData.latoCorrente})"), backgroundColor: mainCol, foregroundColor: isB ? Colors.black : Colors.white, ), // --- SLIVER LAYOUT --- body: SafeArea( child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverList( delegate: SliverChildListDelegate([ _buildTitle("10. PUNTO D'URTO INIZIALE (Seleziona anche più di uno)", mainCol), const SizedBox(height: 10), Center( child: Card( elevation: 4, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), child: GestureDetector( onTapUp: _handleTap, child: Container( width: _canvasWidth, height: _canvasHeight, decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15)), child: Stack( children: [ Center(child: Image.asset( isB ? 'assets/punti_danni_B.png' : 'assets/punti_danni_A.png', width: _canvasWidth, height: _canvasHeight, fit: BoxFit.contain, )), ..._selectedSectors.map((sectorId) { if (_hitboxes.containsKey(sectorId)) { return CustomPaint( painter: XPainter(_hitboxes[sectorId]!), size: Size(_canvasWidth, _canvasHeight), ); } return Container(); }), ], ), ), ), ), ), if (_selectedSectors.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text("Punti: ${_selectedSectors.join(', ')}", style: TextStyle(color: mainCol, fontWeight: FontWeight.bold)), ), const Divider(height: 30), _buildTitle("11. DANNI VISIBILI", mainCol), TextField( controller: _controllerDanni, // Max 45 caratteri per stare nelle 2 righe del PDF (20+25) maxLength: 45, maxLines: 2, keyboardType: TextInputType.multiline, textCapitalization: TextCapitalization.characters, decoration: _inputDeco("Es: Paraurti ant, Faro sx..."), ), const SizedBox(height: 10), _buildTitle("14. OSSERVAZIONI", mainCol), TextField( controller: _controllerOsservazioni, maxLength: 55, maxLines: 2, keyboardType: TextInputType.multiline, textCapitalization: TextCapitalization.sentences, decoration: _inputDeco("Es: Ho ragione io perché..."), ), ]), ), ), // --- STICKY FOOTER --- SliverFillRemaining( hasScrollBody: false, child: Align( alignment: Alignment.bottomCenter, child: Container( padding: const EdgeInsets.all(16), child: _navButtons(context, mainCol), ), ), ), ], ), ), ), ); } Widget _buildTitle(String text, Color color) { return Align(alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only(bottom: 8.0, top: 10.0), child: Text(text, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: color)), )); } InputDecoration _inputDeco(String hint) { return InputDecoration( hintText: hint, filled: true, fillColor: Colors.white, border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), contentPadding: const EdgeInsets.all(15), // counterText: "", <-- RIMOSSO: Ora il contatore si vede! ); } Widget _navButtons(BuildContext context, Color color) { return Row( children: [ Expanded( flex: 4, child: OutlinedButton( onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom( minimumSize: const Size(0, 55), padding: const EdgeInsets.symmetric(horizontal: 5), ), child: const FittedBox( child: Text("INDIETRO", style: TextStyle(fontWeight: FontWeight.bold)) ) ) ), const SizedBox(width: 15), Expanded( flex: 6, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: color, foregroundColor: isB ? Colors.black : Colors.white, minimumSize: const Size(0, 55) ), onPressed: _salvaEProsegui, child: const FittedBox( child: Text("SALVA E PROSEGUI", style: TextStyle(fontWeight: FontWeight.bold)) ) ) ), ], ); } } class XPainter extends CustomPainter { final Rect targetRect; XPainter(this.targetRect); @override void paint(Canvas canvas, Size size) { final Paint paint = Paint()..color = Colors.red..strokeWidth = 3.0..strokeCap = StrokeCap.round..style = PaintingStyle.stroke; double cx = targetRect.left + targetRect.width / 2; double cy = targetRect.top + targetRect.height / 2; double iconSize = 10.0; canvas.drawLine(Offset(cx - iconSize, cy - iconSize), Offset(cx + iconSize, cy + iconSize), paint); canvas.drawLine(Offset(cx + iconSize, cy - iconSize), Offset(cx - iconSize, cy + iconSize), paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } === FILE: lib/comp_8.dart === import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'comp_9.dart'; import 'global_data.dart'; class DateInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { var text = newValue.text.replaceAll('/', ''); if (text.length > 8) text = text.substring(0, 8); var result = ""; for (int i = 0; i < text.length; i++) { if (i == 2 || i == 4) result += "/"; result += text[i]; } return newValue.copyWith( text: result, selection: TextSelection.collapsed(offset: result.length) ); } } class Comp8Screen extends StatefulWidget { const Comp8Screen({super.key}); @override _Comp8ScreenState createState() => _Comp8ScreenState(); } class _Comp8ScreenState extends State { late TextEditingController _polizza, _cartaVerde, _validoDal, _validoAl; late TextEditingController _agenzia, _denomAgenzia, _indirizzoAgenzia, _statoAgenzia, _telAgenzia; late TextEditingController _compagniaManuale; String? _selectedAssicurazione; bool _isManuale = false; late bool _danniMaterialiAssicurati; bool get isB => GlobalData.latoCorrente == 'B'; String? _erroreValidoDal; String? _erroreValidoAl; @override void initState() { super.initState(); _initControllers(); WidgetsBinding.instance.addPostFrameCallback((_) { if (_validoDal.text.isNotEmpty) _validaDataDal(_validoDal.text); if (_validoAl.text.isNotEmpty) _validaDataAl(_validoAl.text); // MOSTRA IL POPUP ANIMATO ALL'AVVIO _mostraInfoPopup(context); }); } void _initControllers() { String denominazioneSalvata = isB ? GlobalData.Denominazione_B : GlobalData.Denominazione_A; _polizza = TextEditingController(text: isB ? GlobalData.Numero_Polizza_B : GlobalData.Numero_Polizza_A); _cartaVerde = TextEditingController(text: isB ? GlobalData.N_carta_verde_B : GlobalData.N_carta_verde_A); _validoDal = TextEditingController(text: isB ? GlobalData.Data_Inizio_Dal_B : GlobalData.Data_Inizio_Dal_A); _validoAl = TextEditingController(text: isB ? GlobalData.Data_Scadenza_Al_B : GlobalData.Data_Scadenza_Al_A); _agenzia = TextEditingController(text: isB ? GlobalData.Agenzia_B : GlobalData.Agenzia_A); _denomAgenzia = TextEditingController(text: isB ? GlobalData.Denominazione_agenzia_B : GlobalData.Denominazione_agenzia_A); _indirizzoAgenzia = TextEditingController(text: isB ? GlobalData.Indirizzo_agenzia_B : GlobalData.Indirizzo_agenzia_A); _statoAgenzia = TextEditingController(text: (isB ? GlobalData.Stato_agenzia_B : GlobalData.Stato_agenzia_A).isEmpty ? "ITALIA" : (isB ? GlobalData.Stato_agenzia_B : GlobalData.Stato_agenzia_A)); _telAgenzia = TextEditingController(text: isB ? GlobalData.N_tel_mail_agenzia_B : GlobalData.N_tel_mail_agenzia_A); _danniMaterialiAssicurati = isB ? GlobalData.FLAG_danni_mat_assicurati_B : GlobalData.FLAG_danni_mat_assicurati_A; _compagniaManuale = TextEditingController(); if (denominazioneSalvata.isNotEmpty) { if (GlobalData.assicurazioni.containsKey(denominazioneSalvata)) { _selectedAssicurazione = denominazioneSalvata; _isManuale = (denominazioneSalvata == "ALTRO (Inserimento manuale)"); } else { _selectedAssicurazione = "ALTRO (Inserimento manuale)"; _compagniaManuale.text = denominazioneSalvata; _isManuale = true; } } else { _selectedAssicurazione = null; _isManuale = false; } } // --- POPUP INFORMATIVO ANIMATO --- 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.business, color: activeColor, size: 28), const SizedBox(width: 10), const Expanded(child: Text("Compagnia di Assicurazione", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text("Recupera i dati assicurativi per il Veicolo ${GlobalData.latoCorrente}.", style: const TextStyle(fontSize: 15)), const SizedBox(height: 16), _buildPopupRow(Icons.description, "Polizza", "Il numero di polizza e le date di validità sono obbligatorie."), const SizedBox(height: 12), _buildPopupRow(Icons.calendar_today, "Date", "Usa l'icona del calendario per inserire rapidamente la data o scrivila manualmente nel formato GG/MM/AAAA."), const SizedBox(height: 12), _buildPopupRow(Icons.store, "Agenzia", "I dati dell'Agenzia sono facoltativi ma aiutano a velocizzare la pratica."), ], ), ), 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( 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? _calcolaErroreData(String data) { if (data.isEmpty) return null; // Se vuoto non diamo errore immediato (ci pensa il tasto Salva) if (data.length < 10) return "Formato: GG/MM/AAAA"; // Avvisa subito se mancano cifre List parti = data.split('/'); if (parti.length != 3) return "Formato errato"; int? giorno = int.tryParse(parti[0]); int? mese = int.tryParse(parti[1]); int? anno = int.tryParse(parti[2]); if (giorno == null || mese == null || anno == null) return "Data non valida"; if (mese < 1 || mese > 12) return "Mese errato"; int giorniMax = _giorniInMese(mese, anno); if (giorno < 1 || giorno > giorniMax) return "Giorno errato per questo mese"; if (anno < 1900 || anno > 2100) return "Anno non valido"; return null; // La data è perfetta } Future _selezionaData(BuildContext context, TextEditingController controller, Function(String) onChanged) async { DateTime initialDate = DateTime.now(); if (controller.text.length == 10) { try { List parti = controller.text.split('/'); initialDate = DateTime(int.parse(parti[2]), int.parse(parti[1]), int.parse(parti[0])); } catch (_) {} } final DateTime? picked = await showDatePicker( context: context, initialDate: initialDate, firstDate: DateTime(1990), lastDate: DateTime(2100), locale: const Locale('it', 'IT'), ); if (picked != null) { String dataFormattata = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}"; setState(() { controller.text = dataFormattata; onChanged(dataFormattata); }); } } void _validaDataDal(String val) => setState(() => _erroreValidoDal = _calcolaErroreData(val)); void _validaDataAl(String val) => setState(() => _erroreValidoAl = _calcolaErroreData(val)); int _giorniInMese(int mese, int anno) { if (mese == 2) { bool bisestile = (anno % 4 == 0 && anno % 100 != 0) || (anno % 400 == 0); return bisestile ? 29 : 28; } if ([4, 6, 9, 11].contains(mese)) return 30; return 31; } // --------------------------------- void _salvaTutto() { bool nomeCompagniaMancante = (_selectedAssicurazione == null || (_selectedAssicurazione == "ALTRO (Inserimento manuale)" && _compagniaManuale.text.trim().isEmpty)); // MODIFICA: Rimossi i controlli sui campi Agenzia, Denominazione, Indirizzo, Stato e Telefono if (nomeCompagniaMancante || _polizza.text.trim().isEmpty || _validoDal.text.trim().isEmpty || _validoAl.text.trim().isEmpty) { // MODIFICA: Testo aggiornato ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Compagnia, Polizza e Date sono obbligatorie"), backgroundColor: Colors.red)); return; } if (_erroreValidoDal != null || _erroreValidoAl != null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Correggi le date prima di proseguire!"), backgroundColor: Colors.red)); return; } if ((_validoDal.text.length < 10) || (_validoAl.text.length < 10)) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Le date devono essere complete (GG/MM/AAAA)"), backgroundColor: Colors.orange)); return; } String denomFinale = (_selectedAssicurazione == "ALTRO (Inserimento manuale)") ? _compagniaManuale.text.toUpperCase() : (_selectedAssicurazione ?? ""); if (isB) { GlobalData.Denominazione_B = denomFinale; GlobalData.Numero_Polizza_B = _polizza.text.toUpperCase(); GlobalData.N_carta_verde_B = _cartaVerde.text.toUpperCase(); GlobalData.Data_Inizio_Dal_B = _validoDal.text; GlobalData.Data_Scadenza_Al_B = _validoAl.text; GlobalData.Agenzia_B = _agenzia.text.toUpperCase(); GlobalData.Denominazione_agenzia_B = _denomAgenzia.text.toUpperCase(); GlobalData.Indirizzo_agenzia_B = _indirizzoAgenzia.text.toUpperCase(); GlobalData.Stato_agenzia_B = _statoAgenzia.text.toUpperCase(); GlobalData.N_tel_mail_agenzia_B = _telAgenzia.text.toUpperCase(); GlobalData.FLAG_danni_mat_assicurati_B = _danniMaterialiAssicurati; } else { GlobalData.Denominazione_A = denomFinale; GlobalData.Numero_Polizza_A = _polizza.text.toUpperCase(); GlobalData.N_carta_verde_A = _cartaVerde.text.toUpperCase(); GlobalData.Data_Inizio_Dal_A = _validoDal.text; GlobalData.Data_Scadenza_Al_A = _validoAl.text; GlobalData.Agenzia_A = _agenzia.text.toUpperCase(); GlobalData.Denominazione_agenzia_A = _denomAgenzia.text.toUpperCase(); GlobalData.Indirizzo_agenzia_A = _indirizzoAgenzia.text.toUpperCase(); GlobalData.Stato_agenzia_A = _statoAgenzia.text.toUpperCase(); GlobalData.N_tel_mail_agenzia_A = _telAgenzia.text.toUpperCase(); GlobalData.FLAG_danni_mat_assicurati_A = _danniMaterialiAssicurati; } Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp9Screen())); } @override Widget build(BuildContext context) { Color mainCol = isB ? Colors.amber.shade700 : Colors.blue.shade900; Color bgCol = isB ? const Color(0xFFFFF9C4) : const Color(0xFFE3F2FD); return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( backgroundColor: bgCol, resizeToAvoidBottomInset: true, appBar: AppBar( title: Text('8. Assicurazione (${GlobalData.latoCorrente})'), backgroundColor: mainCol, foregroundColor: isB ? Colors.black : Colors.white, ), // --- LAYOUT SLIVER --- body: SafeArea( child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ // 1. Contenuto Scrollabile SliverPadding( padding: const EdgeInsets.all(16), sliver: SliverList( delegate: SliverChildListDelegate([ _buildCard( titolo: "8. IMPRESA DI ASSICURAZIONE", accentColor: mainCol, child: Column( children: [ Padding( padding: const EdgeInsets.only(bottom: 10), child: DropdownButtonFormField( value: _selectedAssicurazione, isExpanded: true, decoration: InputDecoration( labelText: "Seleziona Compagnia *", prefixIcon: Icon(Icons.business, color: isB ? Colors.orange.shade800 : Colors.blue.shade700), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), filled: true, fillColor: Colors.white, ), items: GlobalData.assicurazioni.keys.map((String key) { return DropdownMenuItem(value: key, child: Text(key, style: const TextStyle(fontSize: 13))); }).toList(), onChanged: (val) { setState(() { _selectedAssicurazione = val; _isManuale = (val == "ALTRO (Inserimento manuale)"); if (!_isManuale) _compagniaManuale.clear(); }); }, ), ), if (_isManuale) _buildField(_compagniaManuale, "Inserisci Nome Compagnia *", Icons.edit_note, mainCol), _buildField(_polizza, "N. di polizza *", Icons.description, mainCol), _buildField(_cartaVerde, "N. di Carta Verde (Opz)", Icons.language, mainCol), Row(children: [ Expanded( child: _buildField(_validoDal, "Valida dal *", Icons.date_range, mainCol, isDate: true, errorText: _erroreValidoDal, onChanged: _validaDataDal, // USIAMO INKWELL PER STRINGERE I MARGINI customPrefix: InkWell( onTap: () => _selezionaData(context, _validoDal, _validaDataDal), borderRadius: BorderRadius.circular(20), child: Container( width: 38, alignment: Alignment.center, child: Icon(Icons.calendar_today, size: 20, color: isB ? Colors.orange.shade800 : Colors.blue.shade700), ), ), ), ), const SizedBox(width: 10), Expanded( child: _buildField(_validoAl, "Fino al *", Icons.event_available, mainCol, isDate: true, errorText: _erroreValidoAl, onChanged: _validaDataAl, // USIAMO INKWELL PER STRINGERE I MARGINI customPrefix: InkWell( onTap: () => _selezionaData(context, _validoAl, _validaDataAl), borderRadius: BorderRadius.circular(20), child: Container( width: 38, alignment: Alignment.center, child: Icon(Icons.calendar_today, size: 20, color: isB ? Colors.orange.shade800 : Colors.blue.shade700), ), ), ), ), ]), ], ), ), _buildCard( titolo: "AGENZIA (OPZIONALE)", // Modificato titolo per chiarezza accentColor: mainCol, child: Column( children: [ // MODIFICA: Rimossi asterischi dalle label _buildField(_agenzia, "Ufficio/Agenzia", Icons.store, mainCol), _buildField(_denomAgenzia, "Denominazione", Icons.info_outline, mainCol), _buildField(_indirizzoAgenzia, "Indirizzo", Icons.place, mainCol), Row(children: [ Expanded(child: _buildField(_statoAgenzia, "Stato", Icons.flag, mainCol)), const SizedBox(width: 10), Expanded(child: _buildField(_telAgenzia, "Tel. / E-mail", Icons.contact_phone, mainCol)), ]), ], ), ), _buildCard( titolo: "DANNI PROPRI", accentColor: mainCol, child: _buildSwitch( "La polizza copre anche i danni materiali al proprio veicolo?", _danniMaterialiAssicurati, (val) => setState(() => _danniMaterialiAssicurati = val), mainCol ), ), ]), ), ), // 2. Piè di pagina elastico SliverFillRemaining( hasScrollBody: false, child: Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.all(16), child: _navButtons(context, mainCol), ), ), ), ], ), ), ), ); } // --- NUOVO WIDGET SWITCH PERSONALIZZATO (NO / SÌ) --- Widget _buildSwitch(String label, bool value, Function(bool) onChanged, Color activeColor) { return InkWell( onTap: () => onChanged(!value), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( children: [ Expanded( child: Text( label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), ), ), const SizedBox(width: 8), Text( "NO", style: TextStyle( fontWeight: !value ? FontWeight.bold : FontWeight.normal, color: !value ? Colors.red.shade700 : Colors.grey.shade400, fontSize: 14, ), ), Switch( value: value, onChanged: onChanged, activeColor: activeColor, activeTrackColor: activeColor.withOpacity(0.4), inactiveThumbColor: Colors.grey, inactiveTrackColor: Colors.grey.shade300, ), Text( "SÌ", style: TextStyle( fontWeight: value ? FontWeight.bold : FontWeight.normal, color: value ? activeColor : Colors.grey.shade400, fontSize: 14, ), ), ], ), ), ); } Widget _buildField(TextEditingController controller, String label, IconData icon, Color iconColor, {bool isDate = false, String? errorText, Function(String)? onChanged, Widget? customPrefix}) { return Padding( padding: const EdgeInsets.only(bottom: 10), child: TextField( controller: controller, keyboardType: isDate ? TextInputType.number : TextInputType.text, inputFormatters: isDate ? [FilteringTextInputFormatter.digitsOnly, DateInputFormatter()] : [], textCapitalization: TextCapitalization.characters, onChanged: onChanged, // Rimpiccioliamo leggermente il testo se è una data per farlo stare più comodo style: TextStyle(fontSize: isDate ? 14 : 16), decoration: InputDecoration( labelText: label, hintText: isDate ? "GG/MM/AAAA" : null, errorText: errorText, errorStyle: const TextStyle(fontSize: 10), // Rende il testo di errore più compatto prefixIcon: customPrefix ?? Icon(icon, size: 20, color: isB ? Colors.orange.shade800 : Colors.blue.shade700), // --- IL TRUCCO È QUI: Diciamo all'icona di occupare meno spazio --- prefixIconConstraints: const BoxConstraints(minWidth: 38, minHeight: 38), contentPadding: const EdgeInsets.only(left: 0, right: 8, top: 15, bottom: 15), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), filled: true, fillColor: Colors.white, ), ), ); } Widget _buildCard({required String titolo, required Widget child, required Color accentColor}) { return Card( elevation: 2, margin: const EdgeInsets.only(bottom: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(titolo, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold, color: accentColor)), const Divider(height: 20), child, ]), ), ); } Widget _navButtons(BuildContext context, Color color) { 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: color, foregroundColor: isB ? Colors.black : Colors.white, minimumSize: const Size(0, 55)), onPressed: _salvaTutto, child: const Text("SALVA E PROSEGUI"), )), ], ); } @override void dispose() { _polizza.dispose(); _cartaVerde.dispose(); _validoDal.dispose(); _validoAl.dispose(); _agenzia.dispose(); _denomAgenzia.dispose(); _indirizzoAgenzia.dispose(); _statoAgenzia.dispose(); _telAgenzia.dispose(); _compagniaManuale.dispose(); super.dispose(); } } === FILE: lib/ps.dart === import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // Fondamentale per leggere il JSON import 'package:geolocator/geolocator.dart'; import 'package:url_launcher/url_launcher.dart'; class ProntoSoccorsoScreen extends StatefulWidget { const ProntoSoccorsoScreen({super.key}); @override State createState() => _ProntoSoccorsoScreenState(); } class _ProntoSoccorsoScreenState extends State { List _ospedali = []; bool _isLoading = true; String _statusMessage = "Caricamento archivio ospedali..."; Position? _posizioneUtente; @override void initState() { super.initState(); _inizializzaDati(); } Future _inizializzaDati() async { try { // 1. CARICA IL FILE JSON LOCALE // Assicurati che il nome del file in assets sia esattamente questo final String jsonString = await rootBundle.loadString('assets/ospedali_completo.json'); List datiGrezzi = json.decode(jsonString); setState(() => _statusMessage = "Ricerca posizione GPS..."); // 2. CHIEDI PERMESSI E TROVA POSIZIONE Position? posizione = await _determinaPosizione(); if (posizione != null) { _posizioneUtente = posizione; // 3. CALCOLA DISTANZA PER OGNI OSPEDALE for (var ospedale in datiGrezzi) { // Gestione sicura dei numeri (a volte arrivano come stringhe o int) double lat = (ospedale['lat'] is String) ? double.parse(ospedale['lat']) : ospedale['lat'].toDouble(); double lng = (ospedale['lng'] is String) ? double.parse(ospedale['lng']) : ospedale['lng'].toDouble(); double distanzaInMetri = Geolocator.distanceBetween( posizione.latitude, posizione.longitude, lat, lng, ); ospedale['distanza'] = distanzaInMetri; ospedale['lat_num'] = lat; // Salviamo il double pulito per dopo ospedale['lng_num'] = lng; } // 4. ORDINA DAL PIÙ VICINO datiGrezzi.sort((a, b) => (a['distanza'] as double).compareTo(b['distanza'] as double)); // (Opzionale) Prendi solo i primi 50 per non appesantire la lista, o tienili tutti // datiGrezzi = datiGrezzi.take(50).toList(); } if (mounted) { setState(() { _ospedali = datiGrezzi; _isLoading = false; }); } } catch (e) { debugPrint("Errore: $e"); if (mounted) { setState(() { _statusMessage = "Errore: Impossibile caricare gli ospedali.\n$e"; _isLoading = false; }); } } } Future _determinaPosizione() async { bool serviceEnabled; LocationPermission permission; serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) return null; permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) return null; } if (permission == LocationPermission.deniedForever) return null; return await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); } Future _apriMappa(double lat, double lng) async { // URL universale che funziona su Android e iOS aprendo l'app di mappe predefinita final Uri url = Uri.parse("http://maps.google.com/maps?q=$lat,$lng"); if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { throw Exception('Could not launch $url'); } } Future _chiama112() async { final Uri url = Uri.parse("tel:112"); if (await canLaunchUrl(url)) await launchUrl(url); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Pronto Soccorso Vicini"), backgroundColor: Colors.green.shade800, foregroundColor: Colors.white, ), body: Column( children: [ // HEADER EMERGENZA Container( padding: const EdgeInsets.all(16), color: Colors.red.shade50, child: Row( children: [ Icon(Icons.warning_amber_rounded, color: Colors.red.shade800, size: 30), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text("EMERGENZA GRAVE?", style: TextStyle(color: Colors.red.shade900, fontWeight: FontWeight.bold)), const Text("Non andare da solo, chiama il 112."), ], ), ), ElevatedButton( onPressed: _chiama112, style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white), child: const Text("CHIAMA 112"), ) ], ), ), Expanded( child: _isLoading ? Center(child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: Colors.green.shade800), const SizedBox(height: 20), Text(_statusMessage, textAlign: TextAlign.center), ], )) : _ospedali.isEmpty ? const Center(child: Text("Nessun ospedale trovato nel database.")) : ListView.builder( padding: const EdgeInsets.all(10), itemCount: _ospedali.length, itemBuilder: (context, index) { final ospedale = _ospedali[index]; final double km = (ospedale['distanza'] ?? 0) / 1000; final String livello = ospedale['livello'] ?? ""; // Colore badge livello Color badgeColor = Colors.grey; if (livello.contains("DEA2")) badgeColor = Colors.red.shade700; else if (livello.contains("DEA1")) badgeColor = Colors.orange.shade700; else if (livello.contains("PS")) badgeColor = Colors.green.shade700; return Card( elevation: 3, margin: const EdgeInsets.only(bottom: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.green.shade100) ), child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.place, color: Colors.green, size: 20), Text("${km.toStringAsFixed(1)} km", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11)), ], ), ), title: Text( ospedale['nome'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), maxLines: 2, overflow: TextOverflow.ellipsis, ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Text("${ospedale['indirizzo']}, ${ospedale['citta']}", style: TextStyle(color: Colors.grey.shade700, fontSize: 13)), const SizedBox(height: 6), if (livello != "nan" && livello.isNotEmpty) Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: badgeColor.withOpacity(0.1), borderRadius: BorderRadius.circular(4), border: Border.all(color: badgeColor.withOpacity(0.3)) ), child: Text( "Livello: $livello", style: TextStyle(color: badgeColor, fontSize: 11, fontWeight: FontWeight.bold), ), ) ], ), trailing: const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey), onTap: () { if (ospedale['lat_num'] != null && ospedale['lng_num'] != null) { _apriMappa(ospedale['lat_num'], ospedale['lng_num']); } }, ), ); }, ), ), ], ), ); } } === FILE: lib/scambio_dati_screen.dart === import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'global_data.dart'; import 'cid_data_manager.dart'; import 'security_service.dart'; class ScambioDatiScreen extends StatefulWidget { const ScambioDatiScreen({super.key}); @override State createState() => _ScambioDatiScreenState(); } class _ScambioDatiScreenState extends State with SingleTickerProviderStateMixin { late TabController _tabController; final MobileScannerController _cameraController = MobileScannerController(); final TextEditingController _manualCodeController = TextEditingController(); String? _codiceSessione; String? _chiaveSegreta; String? _shortCode; // IL PIN DI 6 LETTERE StreamSubscription? _streamSubscription; bool _isLoading = false; bool _scambioConcluso = false; @override void initState() { super.initState(); // LOGICA DI APERTURA INTELLIGENTE: // Lato A -> Inizia con la Fotocamera (Indice 1) // Lato B -> Inizia mostrando il QR (Indice 0) int initialIndex = (GlobalData.latoCorrente == 'A') ? 1 : 0; _tabController = TabController(length: 2, vsync: this, initialIndex: initialIndex); // Avvio automatico Host (prepara il QR in background a prescindere dalla Tab aperta) _avviaHost(); } @override void dispose() { // Cancello la sessione se esco senza concludere, a meno che non sia l'ID della firma if (!_scambioConcluso && _codiceSessione != null && _codiceSessione != GlobalData.idSessione) { FirebaseFirestore.instance.collection('scambi_cid').doc(_codiceSessione).delete(); } _streamSubscription?.cancel(); _tabController.dispose(); _cameraController.dispose(); _manualCodeController.dispose(); super.dispose(); } // --- GENERATORE DI PIN A 6 LETTERE MAIUSCOLE --- String _generaShortCode() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; Random rnd = Random(); return String.fromCharCodes(Iterable.generate(6, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); } // --- 1. LOGICA HOST (CHI MOSTRA IL QR) --- Future _avviaHost() async { if (_codiceSessione != null) return; if ((GlobalData.latoCorrente == 'A' && GlobalData.puntiFirmaA.isEmpty) || (GlobalData.latoCorrente == 'B' && GlobalData.puntiFirmaB.isEmpty)) { return; } setState(() => _isLoading = true); try { String sessionId = GlobalData.idSessione ?? "CID_${DateTime.now().millisecondsSinceEpoch}"; String shortCode = _generaShortCode(); GlobalData.idSessione = sessionId; GlobalData.idScambioTemporaneo = sessionId; String chiave = GlobalData.chiaveSegretaCorrente ?? SecurityService.generaChiaveSessione(); GlobalData.chiaveSegretaCorrente = chiave; Map mieiDati = CidDataManager.estraiDatiPerExport(); String payloadCriptato = SecurityService.criptaDati(mieiDati, chiave); await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).set({ "timestamp_scambio": FieldValue.serverTimestamp(), "host_lato": GlobalData.latoCorrente, "secure_payload_host": payloadCriptato, "secure_payload_guest": null, "status": "waiting_guest", "short_code": shortCode, "chiave_temporanea": chiave }, SetOptions(merge: true)); if (mounted) { setState(() { _codiceSessione = sessionId; _chiaveSegreta = chiave; _shortCode = shortCode; _isLoading = false; }); } // Ascolto risposta dell'altro telefono _streamSubscription = FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).snapshots().listen((snapshot) { if (!snapshot.exists) { if (mounted) setState(() => _codiceSessione = null); return; } var data = snapshot.data(); if (data != null && data['secure_payload_guest'] != null) { _streamSubscription?.cancel(); _completaSync(data['secure_payload_guest'], chiave); } }); } catch (e) { debugPrint("Err Host: $e"); if (mounted) setState(() => _isLoading = false); } } void _completaSync(String encryptedData, String chiave) { try { Map dati = SecurityService.decriptaDati(encryptedData, chiave); CidDataManager.importaDati(dati); _scambioConcluso = true; _showSuccessAndExit(); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Errore decriptazione"))); } } // --- 2. LOGICA GUEST: VIA FOTOCAMERA --- void _onDetect(BarcodeCapture capture) { if (_isLoading || _scambioConcluso) return; final List barcodes = capture.barcodes; for (final barcode in barcodes) { if (barcode.rawValue != null) { _partecipaGuestScansione(barcode.rawValue!); break; } } } Future _partecipaGuestScansione(String qrRawData) async { if (qrRawData.isEmpty) return; setState(() => _isLoading = true); try { if (!qrRawData.contains('|')) throw Exception("QR non valido"); var parts = qrRawData.split('|'); String sessionId = parts[0]; String chiave = parts[1]; await _eseguiPartecipazione(sessionId, chiave); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore Scansione: $e"))); await Future.delayed(const Duration(seconds: 2)); setState(() => _isLoading = false); } } } // --- 3. LOGICA GUEST: INSERIMENTO MANUALE --- Future _partecipaGuestManuale() async { String codiceInserito = _manualCodeController.text.trim().toUpperCase(); if (codiceInserito.length < 5) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Inserisci un codice valido!"), backgroundColor: Colors.orange)); return; } setState(() => _isLoading = true); try { // 1. Cerca la sessione tramite codice corto su Firebase final querySnapshot = await FirebaseFirestore.instance .collection('scambi_cid') .where('short_code', isEqualTo: codiceInserito) .limit(1) .get(); if (querySnapshot.docs.isEmpty) { throw Exception("Codice non trovato o scaduto."); } final doc = querySnapshot.docs.first; String sessionId = doc.id; String chiave = doc['chiave_temporanea'] ?? "CHIAVE_DI_BACKUP"; await _eseguiPartecipazione(sessionId, chiave, manualHostData: doc.data()); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: $e"), backgroundColor: Colors.red)); setState(() => _isLoading = false); } } } // --- FUNZIONE CENTRALE PER GUEST (USATA DA FOTOCAMERA E MANUALE) --- Future _eseguiPartecipazione(String sessionId, String chiave, {Map? manualHostData}) async { if (GlobalData.idSessione != null && GlobalData.idSessione != sessionId) { debugPrint("🗑️ Cancello vecchio file firma orfano: ${GlobalData.idSessione}"); await FirebaseFirestore.instance.collection('scambi_cid').doc(GlobalData.idSessione).delete(); GlobalData.idSessione = null; } Map data; if (manualHostData != null) { data = manualHostData; } else { DocumentSnapshot doc = await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).get(); if (!doc.exists) throw Exception("Sessione scaduta."); data = doc.data() as Map; } String? hostDataEnc = data['secure_payload_host']; if (hostDataEnc == null) throw Exception("Dati host mancanti."); Map datiHost = SecurityService.decriptaDati(hostDataEnc, chiave); CidDataManager.importaDati(datiHost); Map mieiDati = CidDataManager.estraiDatiPerExport(); String myDataEnc = SecurityService.criptaDati(mieiDati, chiave); await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).update({ "secure_payload_guest": myDataEnc, "status": "completed" }); GlobalData.idSessione = sessionId; GlobalData.idScambioTemporaneo = sessionId; GlobalData.chiaveSegretaCorrente = chiave; _scambioConcluso = true; _showSuccessAndExit(); } void _showSuccessAndExit() { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("SCAMBIO EFFETTUATO!"), backgroundColor: Colors.green, duration: Duration(seconds: 1)) ); Navigator.pop(context); } @override Widget build(BuildContext context) { String qrString = ""; if (_codiceSessione != null && _chiaveSegreta != null) { qrString = "$_codiceSessione|$_chiaveSegreta"; } return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: const Text("Scambio Dati"), backgroundColor: Colors.blue.shade900, foregroundColor: Colors.white, bottom: TabBar( controller: _tabController, indicatorColor: Colors.white, labelColor: Colors.white, unselectedLabelColor: Colors.white70, tabs: const [ Tab(icon: Icon(Icons.qr_code), text: "IL TUO QR"), Tab(icon: Icon(Icons.camera_alt), text: "SCANSIONA / CODICE") ] ), ), body: TabBarView( controller: _tabController, // Impedisce di strusciare accidentalmente e chiudere lo scanner physics: const NeverScrollableScrollPhysics(), children: [ // ================= TAB 1: HOST (MOSTRA QR E PIN) ================= Center( child: SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_codiceSessione == null) ...[ const CircularProgressIndicator(), const SizedBox(height: 20), const Text("Generazione in corso...") ] else ...[ const Text("Fai scansionare questo QR:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), const SizedBox(height: 20), Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration(color: Colors.white, border: Border.all(color: Colors.black12)), child: QrImageView(data: qrString, size: 240) ), const SizedBox(height: 30), const Text("Oppure fai inserire questo PIN:", style: TextStyle(color: Colors.black54)), const SizedBox(height: 5), Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), decoration: BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.orange.shade800, width: 2) ), child: Text( _shortCode ?? "---", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 32, letterSpacing: 6, color: Colors.orange.shade900) ), ), const SizedBox(height: 30), const CircularProgressIndicator(), const SizedBox(height: 10), const Text("In attesa dell'altro utente...", style: TextStyle(color: Colors.grey)) ] ] ) ) ), // ================= TAB 2: GUEST (SCANNER SPLIT SCREEN) ================= Stack( children: [ Column( children: [ // METÀ SUPERIORE: FOTOCAMERA Expanded( flex: 5, child: Stack( children: [ MobileScanner( controller: _cameraController, onDetect: _onDetect, ), // Overlay mirino per far capire all'utente dove inquadrare Center( child: Container( width: 200, height: 200, decoration: BoxDecoration( border: Border.all(color: Colors.greenAccent, width: 3), borderRadius: BorderRadius.circular(20) ), ), ), Positioned( bottom: 10, left: 0, right: 0, child: Container( color: Colors.black54, padding: const EdgeInsets.symmetric(vertical: 8), child: const Text( "Inquadra il QR Code", textAlign: TextAlign.center, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), ) ) ], ), ), // DIVISORIO Container(height: 5, color: Colors.blue.shade900), // METÀ INFERIORE: INSERIMENTO MANUALE Expanded( flex: 5, child: Container( color: Colors.grey.shade100, width: double.infinity, child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.keyboard, size: 40, color: Colors.blueGrey), const SizedBox(height: 10), const Text( "La fotocamera non funziona?\nInserisci qui il PIN a 6 lettere dell'altro utente:", textAlign: TextAlign.center, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87), ), const SizedBox(height: 20), TextField( controller: _manualCodeController, textCapitalization: TextCapitalization.characters, maxLength: 6, textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24, letterSpacing: 8), decoration: InputDecoration( hintText: "PIN", counterText: "", border: OutlineInputBorder(borderRadius: BorderRadius.circular(15)), filled: true, fillColor: Colors.white, prefixIcon: const Icon(Icons.password, color: Colors.blueGrey), ), ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: Colors.orange.shade800, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), ), onPressed: _partecipaGuestManuale, icon: const Icon(Icons.download), label: const Text("SCARICA DATI", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), ), ) ], ), ), ), ) ], ), // OVERLAY CARICAMENTO if (_isLoading) const ColoredBox(color: Colors.black54, child: Center(child: CircularProgressIndicator(color: Colors.white))), ], ) ], ), ); } } === FILE: lib/pdf_inspector.dart === import 'dart:io'; import 'package:syncfusion_flutter_pdf/pdf.dart'; void main() { // 1. Leggi il file PDF (Assicurati del percorso corretto!) // Se sei nella root del progetto, il percorso dovrebbe essere: final File file = File('assets/CAI.pdf'); if (!file.existsSync()) { print('ERRORE: File non trovato in ${file.path}'); return; } final List bytes = file.readAsBytesSync(); final PdfDocument document = PdfDocument(inputBytes: bytes); final PdfForm form = document.form; print('\n--- LISTA CAMPI TROVATI NEL PDF ---'); print('Totale campi: ${form.fields.count}\n'); for (int i = 0; i < form.fields.count; i++) { final field = form.fields[i]; String tipo = 'Sconosciuto'; if (field is PdfTextBoxField) tipo = 'TEXT'; if (field is PdfCheckBoxField) tipo = 'CHECKBOX'; // Stampa in formato facile da copiare print('Key: "${field.name}" \t Tipo: $tipo'); } print('-----------------------------------\n'); document.dispose(); } === FILE: lib/carro_attr.dart === import 'dart:convert'; import 'dart:async'; // Necessario per il Timeout import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:http/http.dart' as http; import 'package:url_launcher/url_launcher.dart'; class CarroAttrezziScreen extends StatefulWidget { const CarroAttrezziScreen({super.key}); @override State createState() => _CarroAttrezziScreenState(); } class _CarroAttrezziScreenState extends State { List _officine = []; bool _isLoading = true; String _statusMessage = "Attivazione GPS..."; @override void initState() { super.initState(); _avviaRicercaSicura(); } Future _avviaRicercaSicura() async { // Piccolo ritardo iniziale per dare tempo alla UI di disegnarsi await Future.delayed(const Duration(milliseconds: 500)); if (!mounted) return; _cercaSoccorsiVicini(); } Future _cercaSoccorsiVicini() async { try { // 1. GESTIONE PERMESSI ROBUSTA bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { if (!mounted) return; setState(() { _statusMessage = "Attiva il GPS per trovare i soccorsi."; _isLoading = false; }); return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { if (!mounted) return; setState(() { _statusMessage = "Permesso GPS negato."; _isLoading = false; }); return; } } if (permission == LocationPermission.deniedForever) { if (!mounted) return; setState(() { _statusMessage = "Permessi GPS bloccati permanentemente."; _isLoading = false; }); return; } // 2. CICLO WHILE PER OTTENERE LA POSIZIONE (WAIT FOR GPS) if (mounted) setState(() => _statusMessage = "Ricerca posizione in corso..."); Position? position; int tentativi = 0; const int maxTentativi = 3; // Prova 3 volte prima di arrendersi // FINCHÉ non ho la posizione E non ho superato i tentativi... while (position == null && tentativi < maxTentativi) { try { if (tentativi > 0) { if (mounted) setState(() => _statusMessage = "Aggancio satelliti (Tentativo ${tentativi + 1}/$maxTentativi)..."); } // Prova a prendere la posizione con un timeout di 6 secondi per tentativo position = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, timeLimit: const Duration(seconds: 6), ); } catch (e) { // Se fallisce, aspetta 1 secondo e riprova tentativi++; await Future.delayed(const Duration(seconds: 1)); } } // Se dopo il ciclo while è ancora null, proviamo l'ultima posizione nota if (position == null) { if (mounted) setState(() => _statusMessage = "Segnale debole, uso ultima posizione..."); position = await Geolocator.getLastKnownPosition(); } // Se è ancora null, alziamo bandiera bianca if (position == null) { throw "Impossibile ottenere la posizione GPS dopo vari tentativi."; } // 3. RICERCA SU OVERPASS API (OSM) if (mounted) setState(() => _statusMessage = "Ricerca soccorsi nei paraggi..."); String query = """ [out:json][timeout:25]; ( node["craft"="car_repair"](around:15000,${position.latitude},${position.longitude}); node["shop"="car_repair"](around:15000,${position.latitude},${position.longitude}); node["service"="vehicle_recovery"](around:15000,${position.latitude},${position.longitude}); ); out body; """; final url = Uri.parse('https://overpass-api.de/api/interpreter?data=${Uri.encodeComponent(query)}'); final response = await http.get( url, headers: {'User-Agent': 'CidApp_Flutter/1.0'}, ).timeout(const Duration(seconds: 20)); if (response.statusCode == 200) { final data = json.decode(response.body); List elements = data['elements']; List> listaElaborata = []; for (var element in elements) { if (element['tags'] != null && element['tags']['name'] != null) { double distanzaMetri = Geolocator.distanceBetween( position!.latitude, position.longitude, element['lat'], element['lon'] ); listaElaborata.add({ 'name': element['tags']['name'], 'phone': element['tags']['phone'] ?? element['tags']['contact:phone'] ?? element['tags']['mobile'], 'street': element['tags']['addr:street'] ?? "", 'city': element['tags']['addr:city'] ?? "", 'distance': distanzaMetri, 'lat': element['lat'], 'lon': element['lon'], }); } } listaElaborata.sort((a, b) => (a['distance'] as double).compareTo(b['distance'] as double)); if (mounted) { setState(() { _officine = listaElaborata; _isLoading = false; }); } } else { throw "Errore server OSM: ${response.statusCode}"; } } catch (e) { if (mounted) { setState(() { _statusMessage = "Nessun soccorso trovato o errore GPS.\nRiprova tra poco."; _isLoading = false; }); } } } Future _chiamaNumero(String? numero) async { if (numero == null) return; // Pulisce il numero da spazi o caratteri strani final pulito = numero.replaceAll(RegExp(r'[^0-9+]'), ''); final Uri url = Uri.parse("tel:$pulito"); if (await canLaunchUrl(url)) await launchUrl(url); } Future _apriMappa(double lat, double lon) async { // Link universale per aprire la navigazione final Uri url = Uri.parse("https://www.google.com/maps/search/?api=1&query=$lat,$lon"); try { if (await canLaunchUrl(url)) { await launchUrl(url, mode: LaunchMode.externalApplication); } else { throw 'Impossibile aprire la mappa'; } } catch (e) { debugPrint("Errore apertura mappa: $e"); } } Future _cercaSuGoogle(String nome) async { final Uri url = Uri.parse("https://www.google.com/search?q=soccorso stradale $nome telefono"); if (await canLaunchUrl(url)) await launchUrl(url, mode: LaunchMode.externalApplication); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Soccorso Stradale"), backgroundColor: Colors.red.shade800, foregroundColor: Colors.white, ), body: _isLoading ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(color: Colors.red), const SizedBox(height: 20), Text(_statusMessage, style: const TextStyle(color: Colors.grey)), ], ), ) : _officine.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.car_crash, size: 60, color: Colors.grey), const SizedBox(height: 10), const Text("Nessun soccorso trovato nei paraggi (15km).", textAlign: TextAlign.center), const SizedBox(height: 20), ElevatedButton( onPressed: () => _cercaSuGoogle("vicino a me"), child: const Text("Cerca su Google"), ) ], ), ), ) : ListView.builder( padding: const EdgeInsets.all(10), itemCount: _officine.length, itemBuilder: (context, index) { final officina = _officine[index]; double km = (officina['distance'] as double) / 1000; String indirizzo = officina['street']; if (officina['city'] != "") indirizzo += (indirizzo.isNotEmpty ? ", " : "") + officina['city']; if (indirizzo.isEmpty) indirizzo = "Posizione GPS"; return Card( elevation: 3, margin: const EdgeInsets.only(bottom: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: ListTile( // KM a sinistra leading: Container( width: 55, height: 55, decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.shade100) ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.location_on, color: Colors.red.shade700, size: 24), Text("${km.toStringAsFixed(1)} km", style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), maxLines: 1), ], ), ), title: Text(officina['name'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), subtitle: Text(indirizzo, style: const TextStyle(fontSize: 13), maxLines: 2, overflow: TextOverflow.ellipsis), // DUE TASTI A DESTRA: MAPPA e CHIAMA trailing: Row( mainAxisSize: MainAxisSize.min, children: [ // Tasto MAPPA IconButton( icon: const Icon(Icons.directions, color: Colors.blue, size: 32), onPressed: () => _apriMappa(officina['lat'], officina['lon']), tooltip: "Naviga", ), // Tasto CHIAMA officina['phone'] != null ? IconButton( icon: const Icon(Icons.phone_in_talk, color: Colors.green, size: 32), onPressed: () => _chiamaNumero(officina['phone']), ) : IconButton( icon: const Icon(Icons.search, color: Colors.orange, size: 32), onPressed: () => _cercaSuGoogle(officina['name']), tooltip: "Cerca Web", ), ], ), ), ), ); }, ), ); } } === FILE: lib/comp_9.dart === // Versione: 2.3.5 - Popup Automatico + Calendari Cliccabili import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'global_data.dart'; import 'comp_10.dart'; class DateInputFormatter extends TextInputFormatter { @override TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) { var text = newValue.text.replaceAll('/', ''); if (text.length > 8) text = text.substring(0, 8); var result = ""; for (int i = 0; i < text.length; i++) { if (i == 2 || i == 4) result += "/"; result += text[i]; } return newValue.copyWith( text: result, selection: TextSelection.collapsed(offset: result.length) ); } } class Comp9Screen extends StatefulWidget { const Comp9Screen({super.key}); @override _Comp9ScreenState createState() => _Comp9ScreenState(); } class _Comp9ScreenState extends State { late TextEditingController _cognome, _nome, _cf, _nascita, _indirizzo, _stato, _tel, _patente, _categoriaAltro, _scadenza; String _selectedCat = "B"; String? _erroreNascita; String? _erroreScadenza; bool get isB => GlobalData.latoCorrente == 'B'; @override void initState() { super.initState(); _initControllers(); // Appena la pagina è costruita, lancia il controllo per i popup WidgetsBinding.instance.addPostFrameCallback((_) { // PRIMA mostra le info della pagina... _mostraInfoPopup(context); // ...POI controlla se ci sono date pre-compilate da validare if (_nascita.text.isNotEmpty) _validaNascita(_nascita.text); if (_scadenza.text.isNotEmpty) _validaScadenza(_scadenza.text); }); } void _initControllers() { if (isB) { _cognome = TextEditingController(text: GlobalData.Cognome_cond_B); _nome = TextEditingController(text: GlobalData.Nome_cond_B); _cf = TextEditingController(text: GlobalData.Cod_fiscale_cond_B); _nascita = TextEditingController(text: GlobalData.Data_nascita_cond_B); _indirizzo = TextEditingController(text: GlobalData.Indirizzo_cond_B); _stato = TextEditingController(text: GlobalData.Stato_cond_B.isEmpty ? "ITALIA" : GlobalData.Stato_cond_B); _tel = TextEditingController(text: GlobalData.N_tel_mail_cond_B); _patente = TextEditingController(text: GlobalData.N_Patente_cond_B); _scadenza = TextEditingController(text: GlobalData.Scadenza_cond_B); _setupCategoria(GlobalData.Categoria_cond_B); } else { _cognome = TextEditingController(text: GlobalData.Cognome_cond_A); _nome = TextEditingController(text: GlobalData.Nome_cond_A); _cf = TextEditingController(text: GlobalData.Cod_fiscale_cond_A); _nascita = TextEditingController(text: GlobalData.Data_nascita_cond_A); _indirizzo = TextEditingController(text: GlobalData.Indirizzo_cond_A); _stato = TextEditingController(text: GlobalData.Stato_cond_A.isEmpty ? "ITALIA" : GlobalData.Stato_cond_A); _tel = TextEditingController(text: GlobalData.N_tel_mail_cond_A); _patente = TextEditingController(text: GlobalData.N_Patente_cond_A); _scadenza = TextEditingController(text: GlobalData.Scadenza_cond_A); _setupCategoria(GlobalData.Categoria_cond_A); } } // --- POPUP INFORMATIVO ANIMATO --- 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.person_pin, color: activeColor, size: 28), const SizedBox(width: 10), const Expanded(child: Text("Dati Conducente", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text("In questa sezione inseriremo i dati della persona che era alla guida del Veicolo ${GlobalData.latoCorrente}.", style: const TextStyle(fontSize: 15)), const SizedBox(height: 16), _buildPopupRow(Icons.file_download, "Importazione", "Se il conducente è la stessa persona del contraente (l'assicurato), potrai importare i suoi dati in un tocco."), const SizedBox(height: 12), _buildPopupRow(Icons.badge, "Patente", "Assicurati di inserire correttamente il numero, la categoria e la data di scadenza della patente di guida."), ], ), ), 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); // Chiude l'info popup // Dopo aver chiuso questo, controlla se deve mostrare l'altro popup per l'importazione _mostraDialogoImportazione(); }, 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(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), ], ), ), ), ], ); } // LOGICA POPUP AUTOMATICO void _mostraDialogoImportazione() { // 1. Se i campi sono già compilati (es. torno indietro per modificare), NON mostrare nulla if (_cognome.text.trim().isNotEmpty && _nome.text.trim().isNotEmpty) return; // 2. Controllo se esistono i dati del contraente da importare String contraenteNome = isB ? GlobalData.Nome_contraente_B : GlobalData.Nome_contraente_A; String contraenteCognome = isB ? GlobalData.Cognome_contraente_B : GlobalData.Cognome_contraente_A; if (contraenteNome.isEmpty && contraenteCognome.isEmpty) return; // 3. Mostra il Popup showDialog( context: context, barrierDismissible: false, // L'utente deve fare una scelta builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), title: Row( children: [ Icon(Icons.person_add_alt_1, color: isB ? Colors.amber[800] : Colors.blue[800]), const SizedBox(width: 10), const Text("Conducente"), ], ), content: Text( "Il conducente è la stessa persona del contraente ($contraenteNome $contraenteCognome)?\n\nVuoi usare i suoi dati?", style: const TextStyle(fontSize: 16), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: Text("NO, scrivo a mano", style: TextStyle(color: Colors.grey[600])), ), ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: isB ? Colors.amber[700] : Colors.blue.shade900, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), onPressed: () { _importaDati(); Navigator.pop(ctx); }, child: const Text("SÌ, importa"), ), ], ), ); } void _importaDati() { setState(() { if (isB) { _cognome.text = GlobalData.Cognome_contraente_B; _nome.text = GlobalData.Nome_contraente_B; _cf.text = GlobalData.Codice_Fiscale_contraente_B; String addr = GlobalData.Indirizzo_contraente_B; if (GlobalData.CAP_contraente_B.isNotEmpty) addr += " ${GlobalData.CAP_contraente_B}"; _indirizzo.text = addr.trim(); _stato.text = GlobalData.Stato_contraente_B.isNotEmpty ? GlobalData.Stato_contraente_B : "ITALIA"; _tel.text = GlobalData.N_telefono_mail_contraente_B; } else { _cognome.text = GlobalData.Cognome_contraente_A; _nome.text = GlobalData.Nome_contraente_A; _cf.text = GlobalData.Codice_Fiscale_contraente_A; String addr = GlobalData.Indirizzo_contraente_A; if (GlobalData.CAP_contraente_A.isNotEmpty) addr += " ${GlobalData.CAP_contraente_A}"; _indirizzo.text = addr.trim(); _stato.text = GlobalData.Stato_contraente_A.isNotEmpty ? GlobalData.Stato_contraente_A : "ITALIA"; _tel.text = GlobalData.N_telefono_mail_contraente_A; } }); } void _setupCategoria(String catEsistente) { if (["A", "B", "C", "D", "E"].contains(catEsistente)) { _selectedCat = catEsistente; _categoriaAltro = TextEditingController(); } else if (catEsistente.isNotEmpty) { _selectedCat = "Altro"; _categoriaAltro = TextEditingController(text: catEsistente); } else { _selectedCat = "B"; _categoriaAltro = TextEditingController(); } } // --- LOGICA CALENDARIO --- Future _selezionaData(BuildContext context, TextEditingController controller, Function(String) onChanged) async { DateTime initialDate = DateTime.now(); if (controller.text.length == 10) { try { List parti = controller.text.split('/'); initialDate = DateTime(int.parse(parti[2]), int.parse(parti[1]), int.parse(parti[0])); } catch (_) {} } final DateTime? picked = await showDatePicker( context: context, initialDate: initialDate, firstDate: DateTime(1900), // Anno minimo sensato per la nascita lastDate: DateTime(2100), locale: const Locale('it', 'IT'), ); if (picked != null) { String dataFormattata = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}"; setState(() { controller.text = dataFormattata; onChanged(dataFormattata); }); } } String? _calcolaErroreData(String data) { if (data.isEmpty) return null; if (data.length < 10) return "Formato: GG/MM/AAAA"; List parti = data.split('/'); if (parti.length != 3) return "Formato errato"; int? giorno = int.tryParse(parti[0]); int? mese = int.tryParse(parti[1]); int? anno = int.tryParse(parti[2]); if (giorno == null || mese == null || anno == null) return "Data non valida"; if (giorno < 1 || giorno > 31) return "Giorno errato"; if (mese < 1 || mese > 12) return "Mese errato"; int giorniMax = _giorniInMese(mese, anno); if (giorno > giorniMax) { String nomeMese = _getNomeMese(mese); return "$nomeMese $anno ha solo $giorniMax gg"; } return null; } int _giorniInMese(int mese, int anno) { if (mese == 2) { bool bisestile = (anno % 4 == 0 && anno % 100 != 0) || (anno % 400 == 0); return bisestile ? 29 : 28; } if ([4, 6, 9, 11].contains(mese)) return 30; return 31; } String _getNomeMese(int mese) { const mesi = ["Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic"]; if (mese >= 1 && mese <= 12) return mesi[mese - 1]; return "Mese"; } void _validaNascita(String val) => setState(() => _erroreNascita = _calcolaErroreData(val)); void _validaScadenza(String val) => setState(() => _erroreScadenza = _calcolaErroreData(val)); void _salvaEProsegui() { String catFinale = (_selectedCat == "Altro") ? _categoriaAltro.text.toUpperCase() : _selectedCat; bool categoriaMancante = (_selectedCat == "Altro" && _categoriaAltro.text.trim().isEmpty); if (_cognome.text.trim().isEmpty || _nome.text.trim().isEmpty || _cf.text.trim().isEmpty || _nascita.text.trim().isEmpty || _indirizzo.text.trim().isEmpty || _stato.text.trim().isEmpty || _patente.text.trim().isEmpty || _scadenza.text.trim().isEmpty || categoriaMancante) { _mostraErrore("Tutti i dati del conducente sono obbligatori!", Colors.red); return; } if (_erroreNascita != null || _erroreScadenza != null) { _mostraErrore("Correggi le date in rosso!", Colors.red); return; } if (_nascita.text.length < 10 || _scadenza.text.length < 10) { _mostraErrore("Formato data incompleto (GG/MM/AAAA)", Colors.orange); return; } if (isB) { GlobalData.Cognome_cond_B = _cognome.text.toUpperCase(); GlobalData.Nome_cond_B = _nome.text.toUpperCase(); GlobalData.Cod_fiscale_cond_B = _cf.text.toUpperCase(); GlobalData.Data_nascita_cond_B = _nascita.text; GlobalData.Indirizzo_cond_B = _indirizzo.text.toUpperCase(); GlobalData.Stato_cond_B = _stato.text.toUpperCase(); GlobalData.N_tel_mail_cond_B = _tel.text.toUpperCase(); GlobalData.N_Patente_cond_B = _patente.text.toUpperCase(); GlobalData.Categoria_cond_B = catFinale; GlobalData.Scadenza_cond_B = _scadenza.text; } else { GlobalData.Cognome_cond_A = _cognome.text.toUpperCase(); GlobalData.Nome_cond_A = _nome.text.toUpperCase(); GlobalData.Cod_fiscale_cond_A = _cf.text.toUpperCase(); GlobalData.Data_nascita_cond_A = _nascita.text; GlobalData.Indirizzo_cond_A = _indirizzo.text.toUpperCase(); GlobalData.Stato_cond_A = _stato.text.toUpperCase(); GlobalData.N_tel_mail_cond_A = _tel.text.toUpperCase(); GlobalData.N_Patente_cond_A = _patente.text.toUpperCase(); GlobalData.Categoria_cond_A = catFinale; GlobalData.Scadenza_cond_A = _scadenza.text; } Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp10Screen())); } void _mostraErrore(String msg, Color colore) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg), backgroundColor: colore, duration: const Duration(seconds: 3)), ); } @override Widget build(BuildContext context) { Color accentColor = isB ? Colors.amber.shade700 : Colors.blue.shade900; Color bgColor = isB ? const Color(0xFFFFF9C4) : const Color(0xFFE3F2FD); return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( backgroundColor: bgColor, resizeToAvoidBottomInset: true, appBar: AppBar( title: Text("9. Conducente (${GlobalData.latoCorrente})"), backgroundColor: accentColor, foregroundColor: isB ? Colors.black : Colors.white, ), body: SafeArea( child: SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( children: [ _buildSectionCard( titolo: "DATI PERSONALI", accentColor: accentColor, children: [ _buildTextField(_cognome, "Cognome *", Icons.person, accentColor), _buildTextField(_nome, "Nome *", Icons.person_outline, accentColor), // --- MODIFICATO: DATA DI NASCITA --- _buildTextField(_nascita, "Data di Nascita *", Icons.cake, accentColor, isDate: true, hint: "GG/MM/AAAA", errorText: _erroreNascita, onChanged: _validaNascita, customPrefix: InkWell( onTap: () => _selezionaData(context, _nascita, _validaNascita), borderRadius: BorderRadius.circular(20), child: Container( width: 38, alignment: Alignment.center, child: Icon(Icons.cake, size: 20, color: accentColor), ), ), ), _buildTextField(_cf, "Codice Fiscale *", Icons.badge, accentColor, isUpper: true), _buildTextField(_indirizzo, "Indirizzo (Via, Cap, Città) *", Icons.home, accentColor), _buildTextField(_stato, "Stato *", Icons.flag, accentColor), _buildTextField(_tel, "Tel/Email *", Icons.contact_mail, accentColor), ], ), _buildSectionCard( titolo: "PATENTE DI GUIDA", accentColor: accentColor, children: [ _buildTextField(_patente, "N. Patente *", Icons.credit_card, accentColor, isUpper: true), Padding( padding: const EdgeInsets.only(bottom: 12), child: DropdownButtonFormField( value: _selectedCat, decoration: InputDecoration( labelText: "Categoria *", prefixIcon: Icon(Icons.category, color: accentColor), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), filled: true, fillColor: Colors.white, ), items: ["A", "B", "C", "D", "E", "Altro"].map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(), onChanged: (v) => setState(() => _selectedCat = v!), ), ), if (_selectedCat == "Altro") _buildTextField(_categoriaAltro, "Specifica Categoria *", Icons.edit, accentColor, isUpper: true), // --- MODIFICATO: SCADENZA PATENTE --- _buildTextField(_scadenza, "Valida fino al *", Icons.event_available, accentColor, isDate: true, hint: "GG/MM/AAAA", errorText: _erroreScadenza, onChanged: _validaScadenza, customPrefix: InkWell( onTap: () => _selezionaData(context, _scadenza, _validaScadenza), borderRadius: BorderRadius.circular(20), child: Container( width: 38, alignment: Alignment.center, child: Icon(Icons.event_available, size: 20, color: accentColor), ), ), ), ], ), _navButtons(accentColor), const SizedBox(height: 10), ], ), ), ), ), ); } // --- MODIFICATO IL METODO PER ACCETTARE customPrefix E RESTRINGERE LA UI --- Widget _buildTextField(TextEditingController c, String label, IconData icon, Color color, {bool isDate = false, bool isUpper = false, bool isNumeric = false, String? hint, String? errorText, Function(String)? onChanged, Widget? customPrefix}) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: TextField( controller: c, keyboardType: isDate || isNumeric ? TextInputType.number : TextInputType.text, textCapitalization: isUpper ? TextCapitalization.characters : TextCapitalization.sentences, onChanged: onChanged, style: TextStyle(fontSize: isDate ? 14 : 16), // Rimpicciolisce un po' le date inputFormatters: [ if (isDate) DateInputFormatter(), if (isDate) LengthLimitingTextInputFormatter(10), if (isNumeric) FilteringTextInputFormatter.digitsOnly, if (isNumeric) LengthLimitingTextInputFormatter(5), ], decoration: InputDecoration( labelText: label, hintText: hint, errorText: errorText, errorStyle: const TextStyle(fontSize: 10), prefixIcon: customPrefix ?? Icon(icon, color: color), prefixIconConstraints: isDate ? const BoxConstraints(minWidth: 38, minHeight: 38) : null, contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), filled: true, fillColor: Colors.white, ), ), ); } Widget _buildSectionCard({required String titolo, required List children, required Color accentColor}) => Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)]), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(titolo, style: TextStyle(fontWeight: FontWeight.bold, color: accentColor, fontSize: 17)), const Divider(height: 25), ...children, ]), ); Widget _navButtons(Color btnColor) => Row(children: [ Expanded(flex: 4, child: OutlinedButton(onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom(minimumSize: const Size(0, 55), side: BorderSide(color: btnColor, width: 1.5), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), child: const FittedBox(child: Text("INDIETRO", style: TextStyle(fontWeight: FontWeight.bold))))), const SizedBox(width: 15), Expanded(flex: 7, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: btnColor, foregroundColor: isB ? Colors.black : Colors.white, minimumSize: const Size(0, 55), elevation: 5, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: _salvaEProsegui, child: const FittedBox(child: Text("SALVA E PROSEGUI", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))))), ]); @override void dispose() { _cognome.dispose(); _nome.dispose(); _cf.dispose(); _nascita.dispose(); _indirizzo.dispose(); _stato.dispose(); _tel.dispose(); _patente.dispose(); _categoriaAltro.dispose(); _scadenza.dispose(); super.dispose(); } } === FILE: lib/scelta_lato.dart === import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; // <-- AGGIUNTA LIBRERIA PER IL DEBUG import 'global_data.dart'; import 'comp_1-5.dart'; import 'comp_6-7.dart'; class SceltaLatoScreen extends StatefulWidget { const SceltaLatoScreen({super.key}); @override State createState() => _SceltaLatoScreenState(); } class _SceltaLatoScreenState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _mostraGuidaSicurezza(context); }); } void _mostraGuidaSicurezza(BuildContext context) { showDialog( context: context, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(10)), child: const Icon(Icons.health_and_safety, color: Colors.red), ), const SizedBox(width: 10), const Text("GUIDA RAPIDA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: const [ _StepGuida(icon: Icons.back_hand, text: "FERMATI in sicurezza e non intralciare il traffico."), Divider(), _StepGuida(icon: Icons.engineering, text: "Se sei fuori dal centro abitato indossa il GILET arancione e posiziona il TRIANGOLO."), Divider(), _StepGuida(icon: Icons.local_hospital, text: "Ci sono feriti? NON muoverli e chiama il 118."), Divider(), _StepGuida(icon: Icons.file_copy, text: "Prepara la patente e i dati della polizza, ti serviranno per compilare il modulo."), Divider(), _StepGuida( icon: Icons.camera_alt, text: "FAI LE FOTO ORA! 📸\n1. Targhe veicoli.\n2. Danni da vicino e lontano.\n3. Posizione auto sulla strada.\n\nLe allegherai alla fine nella mail!", isBold: true ), ], ), ), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue.shade800, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)) ), onPressed: () => Navigator.pop(ctx), child: const Text("HO CAPITO, INIZIAMO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Compilazione CAI", style: TextStyle(fontWeight: FontWeight.bold)), backgroundColor: Colors.blue.shade900, foregroundColor: Colors.white, elevation: 0, actions: [ if (kDebugMode) // <-- AGGIUNTO: NASCONDE IL TASTO NEGLI STORE IconButton( icon: const Icon(Icons.flash_on, color: Colors.orangeAccent), tooltip: "POPOLA DATI TEST", onPressed: () { GlobalData.popolaDatiDiTest(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("⚡ Dati di test caricati! Vai a generare il PDF."), backgroundColor: Colors.green, duration: Duration(seconds: 2), ) ); }, ), IconButton( icon: const Icon(Icons.help_outline, color: Colors.yellowAccent, size: 28), onPressed: () => _mostraGuidaSicurezza(context), tooltip: "Guida Rapida Sicurezza", ) ], ), body: Stack( children: [ // 1. BASE CROMATICA DIVISA (Blu/Giallo) Row( children: [ Expanded(child: Container(color: const Color(0xFFE3F2FD))), // Blu chiaro Expanded(child: Container(color: const Color(0xFFFFFDE7))), // Giallo chiaro ], ), // 2. FILIGRANA STILIZZATA (Sfondo CID) Positioned.fill( child: Opacity( opacity: 0.12, child: Image.asset( 'assets/sfondo_cid.jpg', width: double.infinity, height: double.infinity, fit: BoxFit.cover, errorBuilder: (c, e, s) => const SizedBox(), ), ), ), // 3. CONTENUTO ATTIVO (SEMPLIFICATO E OTTIMIZZATO) SafeArea( child: SingleChildScrollView( // Aggiungiamo un po' di padding per non attaccare il tutto ai bordi dello schermo padding: const EdgeInsets.all(16.0), child: Column( children: [ // Il box informativo ora è diretto figlio della colonna _buildInfoBox(context), const SizedBox(height: 20), // Spazio ridotto tra box e bottoni // PULSANTI AFFIANCATI (Responsive) IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // LATO A (BLU) Expanded( child: _buildBtnConRilievo( context, "VEICOLO A", "LATO BLU", Colors.blue.shade800, 'A', Icons.directions_car_rounded, ), ), const SizedBox(width: 20), // LATO B (GIALLO) Expanded( child: _buildBtnConRilievo( context, "VEICOLO B", "LATO GIALLO", Colors.amber.shade600, 'B', Icons.directions_car_filled_rounded, ), ), ], ), ), const SizedBox(height: 20), // Un po' di spazio extra in fondo ], ), ), ), ], ), ); } // FUNZIONE AGGIORNATA PER EFFETTO 3D POTENZIATO Widget _buildBtnConRilievo(BuildContext context, String titolo, String sottotitolo, Color color, String lato, IconData icon) { bool isB = lato == 'B'; // Definiamo i colori per il gradiente (luce in alto a sx, ombra in basso a dx) Color colorLight, colorDark; if (isB) { // Per il giallo colorLight = Colors.amber.shade400; // Luce colorDark = Colors.amber.shade700; // Ombra } else { // Per il blu colorLight = Colors.blue.shade600; // Luce colorDark = Colors.blue.shade900; // Ombra } return Container( // Decorazione complessa del Container per l'effetto 3D decoration: BoxDecoration( borderRadius: BorderRadius.circular(24), // Arrotondamento leggermente aumentato // 1. GRADIENTE DI SUPERFICIE (Simula la luce che colpisce un oggetto curvo) gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [colorLight, colorDark], stops: const [0.1, 0.9], // Regola i punti di luce e ombra ), // 2. OMBRE STRATIFICATE (Doppia ombra per profondità realistica) boxShadow: [ // Ombra 1: "Spessore" (scura, nitida, vicina) BoxShadow( color: colorDark.withOpacity(0.6), blurRadius: 8, offset: const Offset(0, 8), spreadRadius: 1, // Espande leggermente l'ombra scura ), // Ombra 2: "Sollevamento" (morbida, ampia, lontana) BoxShadow( color: colorDark.withOpacity(0.3), blurRadius: 25, offset: const Offset(0, 18), spreadRadius: -5, // Contrae l'ombra diffusa per non sporcare troppo ), ], ), child: Material( color: Colors.transparent, // Necessario per far vedere il gradiente sottostante child: InkWell( // L'InkWell gestisce il tocco e l'effetto "splash" borderRadius: BorderRadius.circular(24), onTap: () { GlobalData.latoCorrente = lato; if (lato == 'B') { Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp6_7Screen())); } else { Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp1_5Screen())); } }, child: Padding( padding: const EdgeInsets.symmetric(vertical: 30), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Aggiunto un leggero effetto ombra anche all'icona per farla "uscire" Icon( icon, size: 48, color: isB ? Colors.black87 : Colors.white, shadows: [ Shadow( color: Colors.black.withOpacity(0.3), offset: const Offset(2, 2), blurRadius: 4, ) ], ), const SizedBox(height: 15), Text( titolo, style: TextStyle( fontSize: 20, // Leggermente più grande fontWeight: FontWeight.w800, // Più grassetto color: isB ? Colors.black87 : Colors.white, // Leggera ombra sul testo per contrasto shadows: [ Shadow(color: Colors.black.withOpacity(0.2), offset: const Offset(1, 1), blurRadius: 2) ] ) ), const SizedBox(height: 5), Text( sottotitolo, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: isB ? Colors.black54 : Colors.white70 ) ), ], ), ), ), ), ); } Widget _buildInfoBox(BuildContext context) { return Container( padding: const EdgeInsets.all(16), // Rimosso il margine verticale che creava spazio extra inutile decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(15), border: Border.all(color: Colors.blue.shade200, width: 1.5), // Aggiunta una leggera ombra per staccarlo dallo sfondo boxShadow: [ BoxShadow(color: Colors.blue.shade100.withOpacity(0.5), blurRadius: 8, offset: const Offset(0, 4)) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.lightbulb_outline, color: Colors.blue.shade800), const SizedBox(width: 10), Expanded( child: Text( "Come funziona la scelta del lato?", style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.blue.shade900 ), ), ), ], ), const SizedBox(height: 12), const Text( "Nel modulo CAI, i veicoli vengono chiamati A e B. La lettera non indica chi ha ragione o torto, serve solo per distinguere le due auto.", style: TextStyle(fontSize: 14, height: 1.4), ), const SizedBox(height: 16), _buildInfoRow("🤝", "1. Accordati", "Scegli con l'altro conducente chi sarà il Veicolo A e chi il B."), const SizedBox(height: 12), _buildInfoRow("✍️", "2. Compila", "Seleziona il tuo lato e inserisci i dati con attenzione per non dover ricominciare."), const SizedBox(height: 12), _buildInfoRow("📱", "3. Scambia e Genera", "Firma e scambia i dati con la controparte per ottenere il PDF per l'assicurazione."), ], ), ); } Widget _buildInfoRow(String emoji, String title, String desc) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(emoji, style: const TextStyle(fontSize: 18)), const SizedBox(width: 10), Expanded( child: RichText( text: TextSpan( style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4), children: [ TextSpan(text: "$title\n", style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: desc), ], ), ), ), ], ); } } // Widget Helper interno class _StepGuida extends StatelessWidget { final IconData icon; final String text; final bool isBold; const _StepGuida({required this.icon, required this.text, this.isBold = false}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: isBold ? Colors.red : Colors.blueGrey, size: 24), const SizedBox(width: 15), Expanded( child: Text( text, style: TextStyle( fontSize: 15, height: 1.4, fontWeight: isBold ? FontWeight.bold : FontWeight.normal, color: isBold ? Colors.red.shade900 : Colors.black87, ), ), ), ], ), ); } } === FILE: lib/comp_16.dart === import 'dart:io'; import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:path_provider/path_provider.dart'; import 'package:printing/printing.dart'; import 'package:share_plus/share_plus.dart'; import 'scambio_dati_screen.dart'; import 'pdf_engine.dart'; import 'global_data.dart'; import 'main.dart'; import 'comp_6-7.dart'; import 'comp_1-5.dart'; class Comp16Screen extends StatefulWidget { const Comp16Screen({super.key}); @override State createState() => _Comp16ScreenState(); } class _Comp16ScreenState extends State with WidgetsBindingObserver { bool _scambioEffettuato = false; bool _datiPresenti = false; bool _ioHoApprovato = false; bool _tuttiHannoApprovato = false; bool _staCancellando = false; bool _cancellazioneAvviataDaMe = false; String _statusText = "Esegui lo Scambio Dati per iniziare."; Color _statusColor = Colors.orange.shade800; IconData _statusIcon = Icons.warning_amber_rounded; File? _filePdfReale; Uint8List? _immagineAnteprima; bool _isLoading = false; StreamSubscription? _roomSubscription; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); _puliziaIngresso(); WidgetsBinding.instance.addPostFrameCallback((_) { _mostraInfoPopup(context); }); } void _mostraInfoPopup(BuildContext context) { Color activeColor = 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.sync_alt, color: activeColor, size: 28), const SizedBox(width: 10), const Expanded(child: Text("Scambio e Invio", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))), ], ), content: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ const Text("Questa è la fase finale. Segui questi tre passaggi per concludere il modulo:", style: TextStyle(fontSize: 15)), const SizedBox(height: 16), _buildPopupRow(Icons.qr_code_scanner, "1. Scambio Dati", "Inquadra il QR Code dell'altro conducente oppure inserisci a mano il suo codice PIN."), const SizedBox(height: 12), _buildPopupRow(Icons.visibility, "2. Anteprima", "Apri l'anteprima per verificare che i dati di entrambi siano impaginati correttamente sul documento."), const SizedBox(height: 12), _buildPopupRow(Icons.check_circle, "3. Approvazione", "Se tutto è esatto, clicca su Approva. Quando entrambi avrete approvato, il file sarà pronto per l'invio."), ], ), ), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: activeColor, foregroundColor: 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(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), ], ), ), ), ], ); } Future _puliziaIngresso() async { if (GlobalData.idScambioTemporaneo == null && GlobalData.idSessione != null) { GlobalData.idScambioTemporaneo = GlobalData.idSessione; } if (GlobalData.idScambioTemporaneo == null) { if (GlobalData.latoCorrente == 'A') GlobalData.resetB(); else GlobalData.resetA(); } if (mounted) _verificaStatoPostScambio(); } void _verificaStatoPostScambio() { bool datiOk = false; if (GlobalData.latoCorrente == 'A') { datiOk = GlobalData.Cognome_contraente_B.trim().isNotEmpty && GlobalData.Targa_B.trim().isNotEmpty; } else { datiOk = GlobalData.Cognome_contraente_A.trim().isNotEmpty && GlobalData.Targa_A.trim().isNotEmpty; } if (mounted) { setState(() { if (datiOk) { _scambioEffettuato = true; _datiPresenti = true; _statusText = "Dati ricevuti. Generazione anteprima..."; _statusColor = Colors.blue.shade800; _statusIcon = Icons.pending_actions; _generaDocumenti(); _attivaAscoltoStanza(); } else { _resetStatiUI(); } }); } } void _resetStatiUI() { _scambioEffettuato = false; _datiPresenti = false; _ioHoApprovato = false; _tuttiHannoApprovato = false; _statusText = "Esegui lo Scambio Dati per iniziare."; _statusColor = Colors.orange.shade800; _statusIcon = Icons.warning_amber_rounded; _filePdfReale = null; _immagineAnteprima = null; } void _attivaAscoltoStanza() { String? idDaAscoltare = GlobalData.idScambioTemporaneo ?? GlobalData.idSessione; if (idDaAscoltare == null || _roomSubscription != null) return; _roomSubscription = FirebaseFirestore.instance .collection('scambi_cid') .doc(idDaAscoltare) .snapshots() .listen((snapshot) async { if (!snapshot.exists) { if (_ioHoApprovato) { _roomSubscription?.cancel(); _roomSubscription = null; return; } if (!_staCancellando && !_cancellazioneAvviataDaMe && mounted) { _gestisciCancellazioneAltrui(); } return; } final data = snapshot.data(); if (data == null) return; if (data['status'] == 'retry') { if (!_cancellazioneAvviataDaMe) _gestisciCancellazioneAltrui(); return; } bool appA = data['approved_A'] == true; bool appB = data['approved_B'] == true; if (appA && appB) { if (mounted) { setState(() { _tuttiHannoApprovato = true; _ioHoApprovato = true; _statusText = "DATI APPROVATI!\nPDF creato, procedi con il salvataggio o l'invio"; _statusColor = Colors.green.shade800; _statusIcon = Icons.check_circle; }); String? id = GlobalData.idSessione ?? GlobalData.idScambioTemporaneo; if (id != null) FirebaseFirestore.instance.collection('scambi_cid').doc(id).delete().catchError((_){}); } } else if (_ioHoApprovato) { if (mounted) { setState(() { _statusText = "Hai approvato. In attesa dell'altro utente..."; _statusColor = Colors.amber.shade800; _statusIcon = Icons.hourglass_top; }); } } }); } Future _eseguiPuliziaFirebase({required bool notificaAltri}) async { setState(() { _isLoading = true; _cancellazioneAvviataDaMe = true; }); await _roomSubscription?.cancel(); _roomSubscription = null; Set idsDaCancellare = {}; if (GlobalData.idScambioTemporaneo != null) idsDaCancellare.add(GlobalData.idScambioTemporaneo!); if (GlobalData.idSessione != null) idsDaCancellare.add(GlobalData.idSessione!); for (String id in idsDaCancellare) { if (notificaAltri) { try { await FirebaseFirestore.instance.collection('scambi_cid').doc(id).update({'status': 'retry'}) .timeout(const Duration(seconds: 2)); await Future.delayed(const Duration(milliseconds: 300)); } catch (_) {} } try { await FirebaseFirestore.instance.collection('scambi_cid').doc(id).delete(); } catch (_) {} } } Future _tornaIndietroConPulizia() async { await _eseguiPuliziaFirebase(notificaAltri: true); _resetDatiLocali(); if (mounted) { setState(() => _isLoading = false); if (GlobalData.latoCorrente == 'A') { Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp1_5Screen())); } else { Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp6_7Screen())); } } } Future _abbandonaScambioEHome() async { await _eseguiPuliziaFirebase(notificaAltri: true); GlobalData.reset(); if (mounted) { setState(() => _isLoading = false); Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (c) => const HomeScreen()), (route) => false ); } } Future _ioApprovo() async { String? id = GlobalData.idSessione ?? GlobalData.idScambioTemporaneo; if (id != null) { try { String field = (GlobalData.latoCorrente == 'A') ? 'approved_A' : 'approved_B'; await FirebaseFirestore.instance.collection('scambi_cid').doc(id).update({field: true}); } catch (_) {} } if (mounted) { setState(() { _ioHoApprovato = true; }); } } Future _concludiEHome() async { await _eseguiPuliziaFirebase(notificaAltri: false); GlobalData.reset(); if (mounted) { Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (c) => const HomeScreen()), (r) => false); } } void _resetDatiLocali() { if (GlobalData.latoCorrente == 'A') GlobalData.resetB(); else GlobalData.resetA(); GlobalData.idScambioTemporaneo = null; GlobalData.idSessione = null; } void _gestisciCancellazioneAltrui() { _roomSubscription?.cancel(); _roomSubscription = null; if (mounted) { Navigator.of(context).popUntil((route) => route.isFirst || route.settings.name == null); showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.0)), backgroundColor: Colors.white, surfaceTintColor: Colors.transparent, icon: Icon(Icons.warning_amber_rounded, size: 60, color: Colors.amber.shade800), iconPadding: const EdgeInsets.only(top: 24, bottom: 16), title: Text("Attenzione", textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22, color: Colors.amber.shade900)), content: const Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Text( "L'altro utente ha deciso di modificare i propri dati o non ha accettato i tuoi.\n\nSarai riportato alla schermata iniziale dove potrai eventualmente apporre modifiche.", textAlign: TextAlign.center, style: TextStyle(fontSize: 16, height: 1.4, color: Colors.black87), ), ), actionsPadding: const EdgeInsets.fromLTRB(24, 0, 24, 24), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { Navigator.pop(ctx); _resetDatiLocali(); if (GlobalData.latoCorrente == 'A') { Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp1_5Screen())); } else { Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp6_7Screen())); } }, style: ElevatedButton.styleFrom(backgroundColor: Colors.amber.shade800, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0), child: const Text("HO CAPITO", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)) ), ) ], ), ); } } Future _generaDocumenti() async { if (!mounted) return; setState(() => _isLoading = true); try { final List pdfBytes = await PdfEngine.generaDocumentoCai(); if (pdfBytes.isEmpty) throw Exception("PDF vuoto"); final appDocDir = await getApplicationDocumentsDirectory(); final file = File('${appDocDir.path}/CID_${DateTime.now().millisecondsSinceEpoch}.pdf'); await file.writeAsBytes(pdfBytes, flush: true); Uint8List? anteprima; await for (final page in Printing.raster(Uint8List.fromList(pdfBytes), pages: [0], dpi: 150)) { anteprima = await page.toPng(); break; } if (mounted) { setState(() { _filePdfReale = file; _immagineAnteprima = anteprima; _isLoading = false; }); } } catch (e) { if (mounted) setState(() => _isLoading = false); } } Future _vaiAScambioDati() async { await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScambioDatiScreen())); _verificaStatoPostScambio(); } void _apriAnteprimaSchermoIntero() { if (!_scambioEffettuato || !_datiPresenti || _immagineAnteprima == null || _filePdfReale == null) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Dati non pronti!"))); return; } Navigator.push(context, MaterialPageRoute(builder: (context) => ImageViewerScreen( imageBytes: _immagineAnteprima!, pdfFile: _filePdfReale!, isAlreadyApproved: _ioHoApprovato, onConfirmCorrection: _tornaIndietroConPulizia, onConfirmApproval: _ioApprovo ))); } Future _inviaMailConAllegato(BuildContext context) async { if (_filePdfReale == null) return; try { bool isA = GlobalData.latoCorrente == 'A'; String polizzaChiScrive = (isA ? GlobalData.Numero_Polizza_A : GlobalData.Numero_Polizza_B).trim(); String targaChiScrive = (isA ? GlobalData.Targa_A : GlobalData.Targa_B).trim(); String firmaChiScrive = "${isA ? GlobalData.Nome_contraente_A : GlobalData.Nome_contraente_B} ${isA ? GlobalData.Cognome_contraente_A : GlobalData.Cognome_contraente_B}"; String contattoChiScrive = (isA ? GlobalData.N_telefono_mail_contraente_A : GlobalData.N_telefono_mail_contraente_B).trim(); String compagniaUtente = (isA ? GlobalData.Denominazione_A : GlobalData.Denominazione_B).trim().toUpperCase(); String emailDestinatario = ""; if (GlobalData.assicurazioni.containsKey(compagniaUtente)) { emailDestinatario = GlobalData.assicurazioni[compagniaUtente]!; } else { for (var key in GlobalData.assicurazioni.keys) { if (key.isNotEmpty && (compagniaUtente.contains(key) || key.contains(compagniaUtente))) { emailDestinatario = GlobalData.assicurazioni[key]!; break; } } } List listaCC = []; if (contattoChiScrive.contains("@")) listaCC.add(contattoChiScrive); String oggetto = "DENUNCIA SINISTRO - Polizza n. $polizzaChiScrive - Targa $targaChiScrive"; String corpo = "Spett.le Compagnia,\n\n" "Con la presente inoltro in allegato il modulo CAI relativo al sinistro avvenuto in data ${GlobalData.data_incidente} alle ore ${GlobalData.ora} nel comune di ${GlobalData.luogo}.\n\n" "Rimaniamo in attesa dell'apertura del fascicolo.\n\n" "Cordiali saluti,\n$firmaChiScrive\nContatto: $contattoChiScrive"; final Email email = Email( subject: oggetto, body: corpo, recipients: emailDestinatario.isNotEmpty ? [emailDestinatario] : [], cc: listaCC, attachmentPaths: [_filePdfReale!.path], isHTML: false, ); await FlutterEmailSender.send(email); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Nessuna app Mail predefinita trovata. Apro la condivisione..."), duration: Duration(seconds: 3), backgroundColor: Colors.orange)); _apriCondivisione(context); } } } Future _apriCondivisione(BuildContext context) async { if (_filePdfReale == null) return; final box = context.findRenderObject() as RenderBox?; await Share.shareXFiles( [XFile(_filePdfReale!.path, mimeType: 'application/pdf')], subject: 'Modulo CAI', text: 'Ecco il modulo CAI compilato.', sharePositionOrigin: box != null ? (box.localToGlobal(Offset.zero) & box.size) : null, ); } Future _salvaPdfLocale(BuildContext context) async { await _apriCondivisione(context); } @override void dispose() { _roomSubscription?.cancel(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override Widget build(BuildContext context) { bool pdfPronto = !_isLoading && _filePdfReale != null && _immagineAnteprima != null; bool abilitaAnteprima = _scambioEffettuato && _datiPresenti && pdfPronto; bool abilitaFinali = _tuttiHannoApprovato && pdfPronto; String testoAnteprima = !_scambioEffettuato ? "2. ANTEPRIMA (Prima fai Scambio)" : (_ioHoApprovato ? "ANTEPRIMA (IN ATTESA...)" : "2. APRI ANTEPRIMA E APPROVA"); if (_tuttiHannoApprovato) testoAnteprima = "ANTEPRIMA (COMPLETATA)"; return Container( width: double.infinity, height: double.infinity, decoration: BoxDecoration( color: const Color(0xFFF0F4F8), image: DecorationImage( image: const AssetImage('assets/sfondo_mappa.jpg'), fit: BoxFit.cover, colorFilter: ColorFilter.mode( const Color(0xFFF0F4F8).withOpacity(0.6), BlendMode.lighten, ), ), ), child: PopScope( canPop: false, onPopInvoked: (didPop) async { if (didPop) return; if (_ioHoApprovato) _concludiEHome(); else _tornaIndietroConPulizia(); }, child: Scaffold( backgroundColor: Colors.transparent, extendBodyBehindAppBar: true, appBar: AppBar( title: const Text("Invio e Salvataggio", style: TextStyle(fontWeight: FontWeight.w800, fontSize: 20)), centerTitle: true, backgroundColor: Colors.blue.shade900.withOpacity(0.95), foregroundColor: Colors.white, elevation: 10, leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: _ioHoApprovato ? _concludiEHome : _tornaIndietroConPulizia ), shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(20))), ), body: Stack(children: [ SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20), child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildStatusCard(), const SizedBox(height: 20), // VA DIRETTO ALLA PAGINA DI SCAMBIO _btn("1. SCAMBIO DATI", Icons.sync_alt, Colors.orange.shade800, onTap: _vaiAScambioDati, disabled: _ioHoApprovato), const SizedBox(height: 20), _btn(testoAnteprima, Icons.visibility, _statusColor, onTap: abilitaAnteprima ? _apriAnteprimaSchermoIntero : null, disabled: !abilitaAnteprima), const SizedBox(height: 8), Divider(color: Colors.white.withOpacity(0.5), thickness: 1), const SizedBox(height: 8), Builder(builder: (ctx) => _btn("SALVA SUL DISPOSITIVO", Icons.save_alt, Colors.green.shade700, onTap: abilitaFinali ? () => _salvaPdfLocale(ctx) : null, disabled: !abilitaFinali)), const SizedBox(height: 20), Builder(builder: (ctx) => _btn("INVIA ALL'ASSICURAZIONE", Icons.send_rounded, Colors.green.shade700, onTap: abilitaFinali ? () => _inviaMailConAllegato(ctx) : null, disabled: !abilitaFinali)), const SizedBox(height: 40), _btn( _tuttiHannoApprovato ? "TORNA ALLA HOME" : "CANCELLA TUTTO E ESCI", _tuttiHannoApprovato ? Icons.home : Icons.delete_sweep, _tuttiHannoApprovato ? Colors.green.shade800 : Colors.red.shade900, onTap: _tuttiHannoApprovato ? _concludiEHome : _abbandonaScambioEHome, disabled: false ), const SizedBox(height: 30) ]))), if (_isLoading) Container(color: Colors.black54, child: const Center(child: Column(mainAxisSize: MainAxisSize.min, children: [CircularProgressIndicator(color: Colors.white), SizedBox(height: 20), Text("Elaborazione in corso...", style: TextStyle(color: Colors.white))]))), ]), ), ), ); } Widget _btn(String label, IconData icon, Color color, {VoidCallback? onTap, bool disabled = false}) { bool on = onTap != null && !disabled; return Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), boxShadow: on ? [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(0, 4), blurRadius: 5)] : [], ), child: ElevatedButton( onPressed: on ? onTap : null, style: ElevatedButton.styleFrom( backgroundColor: on ? color : Colors.grey, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), ), child: Row(children: [ Icon(icon, size: 28), const SizedBox(width: 20), Expanded(child: Text(label, textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))), const Icon(Icons.lock, size: 20, color: Colors.transparent) ]) ), ); } Widget _buildStatusCard() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(16), border: Border.all(color: _statusColor, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 6, offset: const Offset(0, 3))]), child: Row(children: [ Icon(_statusIcon, color: _statusColor, size: 36), const SizedBox(width: 15), Expanded(child: Text(_statusText, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: _statusColor))) ]) ); } } class ImageViewerScreen extends StatelessWidget { final Uint8List imageBytes; final File pdfFile; final bool isAlreadyApproved; final Function onConfirmCorrection; final Function onConfirmApproval; const ImageViewerScreen({super.key, required this.imageBytes, required this.pdfFile, required this.isAlreadyApproved, required this.onConfirmCorrection, required this.onConfirmApproval}); Future _askCorrection(BuildContext context) async { String titolo = isAlreadyApproved ? "Chiudere?" : "Richiedere correzione?"; String testo = isAlreadyApproved ? "Hai già approvato. Uscendo tornerai alla schermata precedente in attesa dell'altro utente." : "Questo annullerà lo scambio per entrambi e vi riporterà alla modifica."; String tasto = isAlreadyApproved ? "CHIUDI" : "CORREGGI"; bool? conf = await showDialog(context: context, builder: (c) => AlertDialog( title: Text(titolo), content: Text(testo), actions: [ TextButton(onPressed: () => Navigator.pop(c, false), child: const Text("ANNULLA")), ElevatedButton(onPressed: () => Navigator.pop(c, true), child: Text(tasto)) ] )); if (conf == true) { Navigator.pop(context); if (!isAlreadyApproved) onConfirmCorrection(); } } Future _askApproval(BuildContext context) async { Navigator.pop(context); onConfirmApproval(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar(title: const Text("Verifica Dati"), backgroundColor: Colors.black, foregroundColor: Colors.white), body: Column(children: [ Expanded(child: InteractiveViewer(minScale: 0.5, maxScale: 4.0, child: Center(child: Container(color: Colors.white, child: Image.memory(imageBytes, fit: BoxFit.contain))))), Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration(color: Colors.white, boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: const Offset(0, -2))]), child: SafeArea( child: Row(children: [ Expanded( child: ElevatedButton.icon( onPressed: () => _askCorrection(context), icon: Icon(isAlreadyApproved ? Icons.arrow_back : Icons.edit), label: Text(isAlreadyApproved ? "INDIETRO" : "CORREGGI"), style: ElevatedButton.styleFrom(backgroundColor: Colors.orange.shade800, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), ), ), const SizedBox(width: 16), Expanded( child: ElevatedButton.icon( onPressed: isAlreadyApproved ? null : () => _askApproval(context), icon: isAlreadyApproved ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.check_circle), label: Text(isAlreadyApproved ? "IN ATTESA..." : "APPROVA"), style: ElevatedButton.styleFrom(backgroundColor: isAlreadyApproved ? Colors.grey : Colors.green.shade700, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))), ), ), ]), ), ) ]), ); } } === FILE: lib/models.dart === import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'dart:math' as math; // Serve per i calcoli della freccia // --- PAINTER PER IL DISEGNO (A SCHERMO E SU PDF) --- class PainterV40 extends CustomPainter { final List tr; final List el; PainterV40(this.tr, this.el); @override void paint(Canvas canvas, Size size) { // Stile della penna (Strada/Linee) Paint pStrada = Paint() ..color = Colors.black ..strokeWidth = 3.0 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; // 1. DISEGNO TRATTI (PENNA E FRECCE) for (var t in tr) { if (t.punti.length > 1) { Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy); for (var pt in t.punti) path.lineTo(pt.dx, pt.dy); canvas.drawPath(path, pStrada); // Se è una freccia, disegna la punta alla fine if (t.tipo == 'freccia') { // Prendi gli ultimi due punti per calcolare l'angolazione _disegnaPunta(canvas, t.punti[t.punti.length - 2], t.punti.last, pStrada); } } } // 2. DISEGNO ELEMENTI (AUTO E TESTO) for (var e in el) { canvas.save(); // Sposta e ruota il canvas nella posizione dell'elemento canvas.translate(e.posizione.dx, e.posizione.dy); canvas.rotate(e.rotazione); if (e.tipo == 'testo') { _disegnaTesto(canvas, e.label ?? ""); } else { _disegnaAuto(canvas, e.tipo == 'autoA' ? 'A' : 'B', e.tipo == 'autoA' ? Colors.blue : Colors.orange); } canvas.restore(); } } // Disegna il rettangolo dell'auto con la lettera void _disegnaAuto(Canvas canvas, String lettera, Color colore) { Paint p = Paint()..color = colore; // Auto centrata (70x40 px) Rect r = Rect.fromCenter(center: Offset.zero, width: 70, height: 40); canvas.drawRect(r, p); // Bordo nero auto canvas.drawRect(r, Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 2); // Lettera centrata final tp = TextPainter( text: TextSpan(text: lettera, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2)); } // Disegna etichette di testo void _disegnaTesto(Canvas canvas, String txt) { final tp = TextPainter( text: TextSpan(text: txt, style: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold, backgroundColor: Colors.white70)), textDirection: TextDirection.ltr )..layout(); tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2)); } // Calcola e disegna la punta della freccia void _disegnaPunta(Canvas canvas, Offset p1, Offset p2, Paint paint) { double angle = (p2 - p1).direction; // Disegna due linee inclinate rispetto alla direzione finale canvas.drawLine(p2, p2 - Offset.fromDirection(angle - 0.5, 15), paint); canvas.drawLine(p2, p2 - Offset.fromDirection(angle + 0.5, 15), paint); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; } // --- MODELLO DATI: TRATTO PENNA --- class TrattoPenna { List punti; String tipo; // 'penna' o 'freccia' TrattoPenna(this.punti, {this.tipo = 'penna'}); // Metodo utile per la cancellazione (hit test) bool contiene(Offset p) { for (var punto in punti) { if ((p - punto).distance < 20.0) return true; } return false; } Map toMap() => { 'punti': punti.map((p) => {'dx': p.dx, 'dy': p.dy}).toList(), 'tipo': tipo, }; factory TrattoPenna.fromMap(Map map) => TrattoPenna( (map['punti'] as List).map((p) => Offset(p['dx'], p['dy'])).toList(), tipo: map['tipo'] ?? 'penna', ); } // --- MODELLO DATI: ELEMENTO GRAFICO (AUTO/TESTO) --- class ElementoGrafico { Offset posizione; String tipo; // 'autoA', 'autoB', 'testo' double rotazione; String? label; ElementoGrafico(this.posizione, this.tipo, {this.rotazione = 0, this.label}); Map toMap() => { 'pos': {'dx': posizione.dx, 'dy': posizione.dy}, 'tipo': tipo, 'rot': rotazione, 'label': label, }; factory ElementoGrafico.fromMap(Map map) => ElementoGrafico( Offset(map['pos']['dx'], map['pos']['dy']), map['tipo'], rotazione: (map['rot'] as num?)?.toDouble() ?? 0, label: map['label'], ); bool contiene(Offset p) => (p - posizione).distance < 35; // --- METODO CRUCIALE PER IL PDF --- // Genera un'immagine PNG ritagliata e ottimizzata del grafico static Future fondiGraficoDinamica(List trattiRaw, List elementiRaw) async { // Conversione sicura dei tipi (nel caso arrivino come dynamic da GlobalData) List tratti = trattiRaw.cast(); List elementi = elementiRaw.cast(); if (tratti.isEmpty && elementi.isEmpty) return null; // 1. Calcolo Bounding Box (i confini del disegno) double minX = double.infinity, minY = double.infinity; double maxX = double.negativeInfinity, maxY = double.negativeInfinity; void checkPoint(Offset p) { if (p.dx < minX) minX = p.dx; if (p.dx > maxX) maxX = p.dx; if (p.dy < minY) minY = p.dy; if (p.dy > maxY) maxY = p.dy; } for (var t in tratti) { for (var p in t.punti) checkPoint(p); } for (var e in elementi) checkPoint(e.posizione); // Se non ci sono dimensioni valide, esci if (minX == double.infinity) return null; // Aggiungiamo margine (padding) bianco intorno const double pad = 40.0; double width = (maxX - minX) + (pad * 2); double height = (maxY - minY) + (pad * 2); // 2. Disegno su Canvas off-screen final recorder = ui.PictureRecorder(); // Crea canvas delle dimensioni esatte final canvas = ui.Canvas(recorder, Rect.fromLTWH(0, 0, width, height)); // Sfondo Bianco (Copre la griglia del modulo sottostante) canvas.drawRect(Rect.fromLTWH(0, 0, width, height), Paint()..color = Colors.white); // Sposta l'origine del canvas per centrare il disegno ed eliminare lo spazio vuoto in alto/sinistra canvas.translate(-minX + pad, -minY + pad); // Usa il painter esistente per ridisegnare tutto final painter = PainterV40(tratti, elementi); painter.paint(canvas, Size(width, height)); // 3. Conversione in PNG final picture = recorder.endRecording(); final img = await picture.toImage(width.toInt(), height.toInt()); final pngBytes = await img.toByteData(format: ui.ImageByteFormat.png); return pngBytes?.buffer.asUint8List(); } }