=== 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