cid_app/cai_facile_memory.txt

9572 lines
No EOL
372 KiB
Text

=== 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 ===
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSMinimumSystemVersion</key>
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
<key>NSHumanReadableCopyright</key>
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
=== MAC OS CONFIG: Entitlements ===
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
</dict>
</plist>
=== 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 ===
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>CAI App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>cid_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>mailto</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>La fotocamera è necessaria per scansionare il QR Code e importare i dati del sinistro.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>La tua posizione serve per compilare automaticamente il luogo dell'incidente nel modulo.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>La tua posizione serve per compilare il luogo dell'incidente anche se l'app è in background.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Serve per salvare il modulo PDF o i disegni dei danni nel rullino.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Serve per allegare le foto dei danni al veicolo.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
=== 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 ===
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.amastra.cid_app">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<queries>
<intent>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:mimeType="application/pdf" />
</intent>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
</queries>
<application
android:label="CAI Facile"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"
android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>
=== 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<Comp1_5Screen> {
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<void> _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<Offset>(
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<void> _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<Placemark> 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<void> _selezionaData(BuildContext context) async {
DateTime initialDate = DateTime.now();
if (_data.text.length == 10) {
try {
List<String> 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<Comp15Screen> createState() => _Comp15ScreenState();
}
class _Comp15ScreenState extends State<Comp15Screen> {
late List<Offset?> _puntiFirma;
late bool isB;
bool _isNavigating = false;
@override
void initState() {
super.initState();
isB = GlobalData.latoCorrente == 'B';
_puntiFirma = isB
? List<Offset?>.from(GlobalData.puntiFirmaB)
: List<Offset?>.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<void> _tornaIndietro() async {
_salvaInMemoria();
if (mounted) Navigator.pop(context);
}
void _salvaInMemoria() {
if (isB) {
GlobalData.puntiFirmaB = List.from(_puntiFirma);
} else {
GlobalData.puntiFirmaA = List.from(_puntiFirma);
}
}
Future<void> _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<Offset?> 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<Comp6_7Screen> {
late TextEditingController _cognome, _nome, _cf, _indirizzo, _cap, _stato, _tel;
late TextEditingController _marca, _rimorchio, _targa, _statoImm, _statoImm2;
// Il tuo FocusNode originale
late FocusNode _cfFocusNode;
final bool isB = GlobalData.latoCorrente == 'B';
bool _isReady = false;
@override
void initState() {
super.initState();
// Ripristino del tuo listener originale sull'uscita dal campo
_cfFocusNode = FocusNode();
_cfFocusNode.addListener(_onCfFocusChange);
_forzaVerticaleEInizializza();
}
// La tua funzione originale per l'uscita dal campo
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<void> _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<Offset>(begin: const Offset(0.0, 0.4), end: Offset.zero).animate(curvePosizione), child: FadeTransition(opacity: curveOpacita, child: child));
},
);
}
Widget _buildPopupRow(IconData icon, String title, String desc) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 24, color: Colors.blueGrey),
const SizedBox(width: 12),
Expanded(child: RichText(text: TextSpan(style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4), children: [TextSpan(text: "$title: ", style: const TextStyle(fontWeight: FontWeight.bold)), TextSpan(text: desc)]))),
],
);
}
String _calcolaCodiceCognome(String cognome) {
cognome = cognome.toUpperCase().replaceAll(RegExp(r'[^A-Z]'), '');
if (cognome.isEmpty) return "XXX";
String consonanti = cognome.replaceAll(RegExp(r'[AEIOU]'), '');
String vocali = cognome.replaceAll(RegExp(r'[^AEIOU]'), '');
return (consonanti + vocali + "XXX").substring(0, 3);
}
String _calcolaCodiceNome(String nome) {
nome = nome.toUpperCase().replaceAll(RegExp(r'[^A-Z]'), '');
if (nome.isEmpty) return "XXX";
String consonanti = nome.replaceAll(RegExp(r'[AEIOU]'), '');
String vocali = nome.replaceAll(RegExp(r'[^AEIOU]'), '');
if (consonanti.length >= 4) {
return consonanti[0] + consonanti[2] + consonanti[3];
} else {
return (consonanti + vocali + "XXX").substring(0, 3);
}
}
bool _isCodiceFiscaleValido(String cf) {
cf = cf.toUpperCase().trim();
if (cf.isEmpty) return false;
if (!RegExp(r"^[A-Z0-9]{16}$").hasMatch(cf)) return false;
final String setDispari = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
final String setPari = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
final List<int> valoriDispari = [1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 1, 0, 5, 7, 9, 13, 15, 17, 19, 21, 2, 4, 18, 20, 11, 3, 6, 8, 12, 14, 16, 10, 22, 25, 24, 23];
final List<int> valoriPari = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25];
int somma = 0;
for (int i = 0; i < 15; i++) {
String char = cf[i];
int val;
if ((i + 1) % 2 != 0) {
int index = setDispari.indexOf(char);
if (index == -1) return false;
val = valoriDispari[index];
} else {
int index = setPari.indexOf(char);
if (index == -1) return false;
val = valoriPari[index];
}
somma += val;
}
int resto = somma % 26;
String carattereControlloCalcolato = String.fromCharCode(65 + resto);
return carattereControlloCalcolato == cf[15];
}
void _salvaTutto() {
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<TextInputFormatter> formatters = [];
if (isNumeric) {
formatters.add(FilteringTextInputFormatter.digitsOnly);
formatters.add(LengthLimitingTextInputFormatter(5));
} else if (isCF) {
formatters.add(FilteringTextInputFormatter.allow(RegExp("[a-zA-Z0-9]")));
formatters.add(UpperCaseTextFormatter());
formatters.add(LengthLimitingTextInputFormatter(16));
} else if (isUpper) {
formatters.add(UpperCaseTextFormatter());
formatters.add(FilteringTextInputFormatter.deny(RegExp(r'\s')));
}
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<Widget> children, required Color accentColor}) {
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
titolo,
style: TextStyle(fontWeight: FontWeight.bold, color: accentColor, fontSize: 16)
),
const Divider(height: 25),
...children,
]
),
),
);
}
Widget _navButtons(Color col) {
return Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(minimumSize: const Size(0, 55)),
child: const Text("INDIETRO")
)
),
const SizedBox(width: 15),
Expanded(
flex: 2,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: col,
foregroundColor: isB ? Colors.black : Colors.white,
minimumSize: const Size(0, 55)
),
onPressed: _salvaTutto,
child: const Text("SALVA E PROSEGUI", style: TextStyle(fontWeight: FontWeight.bold))
)
),
]
);
}
@override
void dispose() {
_cfFocusNode.removeListener(_onCfFocusChange);
_cfFocusNode.dispose();
_cognome.dispose(); _nome.dispose(); _cf.dispose(); _indirizzo.dispose();
_cap.dispose(); _stato.dispose(); _tel.dispose();
_marca.dispose(); _targa.dispose(); _rimorchio.dispose();
_statoImm.dispose(); _statoImm2.dispose();
super.dispose();
}
}
=== FILE: lib/cai_mapping.dart ===
class CaiMapping {
static const Map<String, String> 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<String> circostanzeA = [
'',
'A01', 'A02', 'A03', 'A04', 'A05', 'A06', 'A07', 'A08', 'A09',
'A10', 'A11', 'A12', 'A13', 'A14', 'A15', 'A16', 'A17'
];
static const List<String> 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<List<int>> generaDocumentoCai() async {
PdfDocument? document;
try {
final ByteData data = await rootBundle.load('assets/CAI_p1.pdf');
// 1. Caricamento e Copia
final List<int> 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<String, PdfField> 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<String> 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<TrattoPenna>().toList(), GlobalData.elementi.cast<ElementoGrafico>().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<int> 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<int> bytesFinali = await docFinale.save();
docFinale.dispose();
return bytesFinali;
} catch (e) {
debugPrint("ERRORE GENERAZIONE PDF: $e");
return [];
}
}
// --- HELPERS (Standard) ---
static bool _scriviX(Map<String, PdfField> mappa, List<String> 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<String, PdfField> mappa, List<String> 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<String, PdfField> mappa, List<String> 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<String, PdfField> mappa, List<String> 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<void> _disegnaInBox(PdfPage page, Map<String, PdfField> 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<Uint8List?> _renderFirmaTight(List<Offset?> 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<Uint8List?> _renderGraficoV40(List<TrattoPenna> tratti, List<ElementoGrafico> 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<TrattoPenna> tr;
final List<ElementoGrafico> el;
PainterV40(this.tr, this.el);
final List<Color> 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<Comp16Screen> createState() => _Comp16ScreenState();
}
class _Comp16ScreenState extends State<Comp16Screen> 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<void> _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<void> _eseguiPuliziaFirebase({required bool notificaAltri}) async {
setState(() {
_isLoading = true;
_cancellazioneAvviataDaMe = true;
});
await _roomSubscription?.cancel();
_roomSubscription = null;
Set<String> 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<void> _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<void> _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<void> _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<void> _generaDocumenti() async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
final List<int> 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<void> _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<void> _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<String> 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<void> _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<void> _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<void> _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<String> puntiUrtoA_List = []; static String danni_visibili_A = ""; static String osservazioni_A = ""; static Map<int, bool> circostanzeA = {}; static int totaleCrocetteA = 0; static List<Offset?> 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<String> puntiUrtoB_List = []; static String danni_visibili_B = ""; static String osservazioni_B = ""; static Map<int, bool> circostanzeB = {}; static int totaleCrocetteB = 0; static List<Offset?> puntiFirmaB = [];
// --- DATI GRAFICI ---
static List<dynamic> tratti = [];
static List<dynamic> elementi = [];
static Map<String, String> 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<Map<String, dynamic>> avviaSessioneRealTime() async {
String sessionId = _generaCodiceUnivoco();
String mioLato = GlobalData.latoCorrente;
// Estrazione dati
Map<String, dynamic> mieiDati = CidDataManager.estraiDatiPerExport();
Map<String, dynamic> payload = {
'created_at': FieldValue.serverTimestamp(),
'expires_at': DateTime.now().add(const Duration(minutes: 10)).millisecondsSinceEpoch,
'dati_$mioLato': mieiDati,
};
// Scrittura su Firebase
await _db.collection(collectionName).doc(sessionId).set(payload);
return {
'sessionId': sessionId,
'stream': _db.collection(collectionName).doc(sessionId).snapshots()
};
}
// --- LATO CHE SCANSIONA (Guest) ---
static Future<bool> completaScambio(String sessionId) async {
try {
sessionId = sessionId.trim().toUpperCase();
DocumentReference docRef = _db.collection(collectionName).doc(sessionId);
DocumentSnapshot doc = await docRef.get();
if (!doc.exists) return false;
Map<String, dynamic> data = doc.data() as Map<String, dynamic>;
String mioLato = GlobalData.latoCorrente;
String latoAltro = mioLato == 'A' ? 'B' : 'A';
// 1. Importo i dati dell'altro
if (data['dati_$latoAltro'] != null) {
CidDataManager.importaDati(data['dati_$latoAltro']);
} else {
return false;
}
// 2. Invio i miei dati per completare lo scambio
Map<String, dynamic> mieiDati = CidDataManager.estraiDatiPerExport();
await docRef.update({
'dati_$mioLato': mieiDati
});
return true;
} catch (e) {
print("Errore durante lo scambio: $e");
return false;
}
}
static String _generaCodiceUnivoco() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
Random rnd = Random();
return String.fromCharCodes(Iterable.generate(
6, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
}
}
=== FILE: lib/security_service.dart ===
// FILE: lib/security_service.dart
import 'package:encrypt/encrypt.dart' as encrypt;
import 'dart:convert';
import 'dart:math';
class SecurityService {
// 1. GENERATORE DI CHIAVI
static String generaChiaveSessione() {
const chars = 'AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz1234567890';
Random rnd = Random.secure();
return String.fromCharCodes(Iterable.generate(32, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
}
// 2. CRIPTAZIONE
static String criptaDati(Map<String, dynamic> dati, String chiaveSegreta) {
try {
final key = encrypt.Key.fromUtf8(chiaveSegreta);
final iv = encrypt.IV.fromLength(16);
final encrypter = encrypt.Encrypter(encrypt.AES(key));
String jsonString = jsonEncode(dati);
final encrypted = encrypter.encrypt(jsonString, iv: iv);
return "${iv.base64}:${encrypted.base64}";
} catch (e) {
return "Errore Criptazione: $e";
}
}
// 3. DECRIPTAZIONE
static Map<String, dynamic> decriptaDati(String pacchettoCriptato, String chiaveSegreta) {
try {
final parts = pacchettoCriptato.split(':');
if (parts.length != 2) throw Exception("Formato dati non valido");
final iv = encrypt.IV.fromBase64(parts[0]);
final encryptedData = encrypt.Encrypted.fromBase64(parts[1]);
final key = encrypt.Key.fromUtf8(chiaveSegreta);
final encrypter = encrypt.Encrypter(encrypt.AES(key));
final decrypted = encrypter.decrypt(encryptedData, iv: iv);
return jsonDecode(decrypted);
} catch (e) {
return {"errore": "Impossibile decriptare: $e"};
}
}
}
=== FILE: lib/pdf_engine.dart ===
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/services.dart' show rootBundle;
import 'package:syncfusion_flutter_pdf/pdf.dart';
import 'package:flutter/material.dart';
import 'global_data.dart';
import 'models.dart';
import 'cai_mapping.dart';
class PdfEngine {
static Future<List<int>> generaDocumentoCai() async {
PdfDocument? document;
try {
final ByteData data = await rootBundle.load('assets/CAI_p1.pdf');
// 1. Caricamento e Copia
final List<int> bytesCopia = data.buffer.asUint8List().toList();
document = PdfDocument(inputBytes: bytesCopia);
final PdfForm form = document.form;
final PdfPage page = document.pages[0];
// form.setDefaultAppearance(false); // REMOVED to fix empty fields issue
// Mappatura Campi
Map<String, PdfField> mappaCampi = {};
for (int i = 0; i < form.fields.count; i++) {
if (form.fields[i].name != null) {
mappaCampi[form.fields[i].name!.trim().toUpperCase()] = form.fields[i];
}
}
// --- COMPILAZIONE ---
// 1. TESTI STANDARD (Escludiamo i Danni per gestirli dopo con split a 20 char)
CaiMapping.testi.forEach((keyGlobal, keyPdf) {
if (keyGlobal.contains("danni_visibili")) return;
String valore = _valoreDaGlobal(keyGlobal);
String keyPdfNorm = keyPdf.trim().toUpperCase();
if (mappaCampi.containsKey(keyPdfNorm) && valore.isNotEmpty) {
final field = mappaCampi[keyPdfNorm];
if (field is PdfTextBoxField) {
field.font = PdfStandardFont(PdfFontFamily.helvetica, 8); // Font 8
field.foreColor = PdfColor(0, 0, 0);
field.textAlignment = PdfTextAlignment.left;
field.text = valore.toUpperCase();
}
}
});
// 1.1 GESTIONE SPECIALE DANNI (Split forzato a 20 caratteri)
_riempiCampoSplit(mappaCampi, GlobalData.danni_visibili_A, "DANNI_VIS_A1", "DANNI_VIS_A2");
_riempiCampoSplit(mappaCampi, GlobalData.danni_visibili_B, "DANNI_VIS_B1", "DANNI_VIS_B2");
// 2. CHECKBOX
_scriviX(mappaCampi, [GlobalData.feriti ? CaiMapping.feriti_SI : CaiMapping.feriti_NO]);
_scriviX(mappaCampi, [GlobalData.Veicoli_danni_materiali_oltre ? CaiMapping.danni_veicoli_SI : CaiMapping.danni_veicoli_NO]);
_scriviX(mappaCampi, [GlobalData.Oggetti_diversi_danni_materiali ? CaiMapping.danni_oggetti_SI : CaiMapping.danni_oggetti_NO]);
_scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_A ? CaiMapping.danni_mat_A_SI : CaiMapping.danni_mat_A_NO]);
_scriviX(mappaCampi, [GlobalData.FLAG_danni_mat_assicurati_B ? CaiMapping.danni_mat_B_SI : CaiMapping.danni_mat_B_NO]);
String catA = GlobalData.Categoria_cond_A.toUpperCase().trim();
if (catA == 'A') _scriviX(mappaCampi, ['cat_a_A']);
else if (catA == 'B') _scriviX(mappaCampi, ['cat_b_A']);
else if (catA.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_A'], catA);
String catB = GlobalData.Categoria_cond_B.toUpperCase().trim();
if (catB == 'A') _scriviX(mappaCampi, ['cat_a_B']);
else if (catB == 'B') _scriviX(mappaCampi, ['cat_b_B']);
else if (catB.isNotEmpty) _scriviTesto(mappaCampi, ['cat_altro_B'], catB);
// 3. CIRCOSTANZE
int countA = 0;
int countB = 0;
for (int i = 1; i <= 17; i++) {
if (GlobalData.circostanzeA[i] == true) {
if (_scriviX(mappaCampi, [i < 10 ? "A0$i" : "A$i"])) countA++;
}
if (GlobalData.circostanzeB[i] == true) {
List<String> nomiTarget = [];
if (i == 9) nomiTarget = ["Check Box 26", "CheckBox26", "26"];
else if (i == 10) nomiTarget = ["Check Box 27", "CheckBox27", "27"];
else if (i == 11) nomiTarget = ["Check Box 28", "CheckBox28", "28"];
else if (i == 12) nomiTarget = ["Check Box 29", "CheckBox29", "29"];
else nomiTarget = [i < 10 ? "B0$i" : "B$i"];
if (_scriviX(mappaCampi, nomiTarget)) countB++;
}
}
_scriviTestoTotale(mappaCampi, ['A_TOT', 'A_tot'], countA.toString());
_scriviTestoTotale(mappaCampi, ['B_TOT', 'B_tot'], countB.toString());
// 4. PUNTI URTO
for (String punto in GlobalData.puntiUrtoA_List) _scriviXRossa(mappaCampi, [punto]);
for (String punto in GlobalData.puntiUrtoB_List) _scriviXRossa(mappaCampi, [punto]);
// 5. IMMAGINI E GRAFICO
// Render the updated graph (with new vehicle types) to an image
Uint8List? graficoBytes = await _renderGraficoV40(
GlobalData.tratti.cast<TrattoPenna>().toList(),
GlobalData.elementi.cast<ElementoGrafico>().toList()
);
// Draw the graph image into the specific PDF box
await _disegnaInBox(page, mappaCampi, CaiMapping.box_grafico, graficoBytes);
await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_A, await _renderFirmaTight(GlobalData.puntiFirmaA, Colors.black));
await _disegnaInBox(page, mappaCampi, CaiMapping.box_firma_B, await _renderFirmaTight(GlobalData.puntiFirmaB, Colors.black));
// =================================================================
// SALVATAGGIO SICURO
// =================================================================
List<int> bytesTemporanei = await document.save();
document.dispose();
PdfDocument docFinale = PdfDocument(inputBytes: bytesTemporanei);
try {
docFinale.form.flattenAllFields();
} catch (e) {
debugPrint("⚠️ Errore Flattening: $e");
docFinale.form.readOnly = true;
}
List<int> bytesFinali = await docFinale.save();
docFinale.dispose();
return bytesFinali;
} catch (e) {
debugPrint("ERRORE GENERAZIONE PDF: $e");
return [];
}
}
// ... (Keep existing helper methods: _riempiCampoSplit, _scriviX, _scriviTesto, _scriviXRossa, _scriviTestoTotale, _disegnaInBox, _renderFirmaTight, _valoreDaGlobal)
// Re-pasting them here for completeness to ensure no missing dependencies
static void _riempiCampoSplit(Map<String, PdfField> mappa, String testoCompleto, String key1, String key2) {
if (testoCompleto.isEmpty) return;
String riga1 = "";
String riga2 = "";
int limite = 20;
if (testoCompleto.length <= limite) {
riga1 = testoCompleto;
} else {
int splitIndex = testoCompleto.lastIndexOf(" ", limite);
if (splitIndex == -1) splitIndex = limite;
riga1 = testoCompleto.substring(0, splitIndex).trim();
riga2 = testoCompleto.substring(splitIndex).trim();
}
if (mappa.containsKey(key1)) {
final f1 = mappa[key1] as PdfTextBoxField;
f1.font = PdfStandardFont(PdfFontFamily.helvetica, 8);
f1.textAlignment = PdfTextAlignment.left;
f1.text = riga1.toUpperCase();
}
if (mappa.containsKey(key2)) {
final f2 = mappa[key2] as PdfTextBoxField;
f2.font = PdfStandardFont(PdfFontFamily.helvetica, 8);
f2.textAlignment = PdfTextAlignment.left;
f2.text = riga2.toUpperCase();
}
}
static bool _scriviX(Map<String, PdfField> mappa, List<String> nomiPossibili) {
for (String nome in nomiPossibili) {
String key = nome.trim().toUpperCase();
if (mappa.containsKey(key)) {
final field = mappa[key]!;
if (field is PdfTextBoxField) {
field.font = PdfStandardFont(PdfFontFamily.helvetica, 14);
field.foreColor = PdfColor(0, 0, 0);
field.textAlignment = PdfTextAlignment.center;
field.text = "X";
} else if (field is PdfCheckBoxField) {
field.isChecked = true;
}
return true;
}
}
return false;
}
static void _scriviTesto(Map<String, PdfField> mappa, List<String> nomiPossibili, String testo) {
for (String nome in nomiPossibili) {
String key = nome.trim().toUpperCase();
if (mappa.containsKey(key)) {
final field = mappa[key]!;
if (field is PdfTextBoxField) {
field.font = PdfStandardFont(PdfFontFamily.helvetica, 8);
field.foreColor = PdfColor(0, 0, 0);
field.text = testo;
}
return;
}
}
}
static bool _scriviXRossa(Map<String, PdfField> mappa, List<String> nomiPossibili) {
for (String nome in nomiPossibili) {
String key = nome.trim().toUpperCase();
if (mappa.containsKey(key)) {
final field = mappa[key]!;
if (field is PdfTextBoxField) {
field.font = PdfStandardFont(PdfFontFamily.helvetica, 16, style: PdfFontStyle.bold);
field.foreColor = PdfColor(255, 0, 0);
field.textAlignment = PdfTextAlignment.center;
field.text = "X";
return true;
}
}
}
return false;
}
static void _scriviTestoTotale(Map<String, PdfField> mappa, List<String> 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<void> _disegnaInBox(PdfPage page, Map<String, PdfField> 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<Uint8List?> _renderFirmaTight(List<Offset?> 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<Uint8List?> _renderGraficoV40(List<TrattoPenna> tratti, List<ElementoGrafico> 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<TrattoPenna> tr;
final List<ElementoGrafico> el;
PainterV40(this.tr, this.el);
final List<Color> palette = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red];
@override
void paint(Canvas canvas, Size size) {
// 1. SFONDO BIANCO
final Paint backgroundPaint = Paint()..color = Colors.white;
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint);
// 2. GRIGLIA
final Paint gridPaint = Paint()..color = Colors.grey.shade300..strokeWidth = 2.0;
double step = 40.0;
for (double x = 0; x <= size.width; x += step) {
canvas.drawLine(Offset(x, 0), Offset(x, size.height), gridPaint);
}
for (double y = 0; y <= size.height; y += step) {
canvas.drawLine(Offset(0, y), Offset(size.width, y), gridPaint);
}
if (tr.isEmpty && el.isEmpty) return;
// --- BOUNDING BOX ---
double minX = double.infinity, minY = double.infinity;
double maxX = double.negativeInfinity, maxY = double.negativeInfinity;
for (var t in tr) {
for (var p in t.punti) {
if (p.dx < minX) minX = p.dx;
if (p.dx > maxX) maxX = p.dx;
if (p.dy < minY) minY = p.dy;
if (p.dy > maxY) maxY = p.dy;
}
}
for (var e in el) {
if (e.posizione.dx - 30 < minX) minX = e.posizione.dx - 30;
if (e.posizione.dx + 30 > maxX) maxX = e.posizione.dx + 30;
if (e.posizione.dy - 30 < minY) minY = e.posizione.dy - 30;
if (e.posizione.dy + 30 > maxY) maxY = e.posizione.dy + 30;
}
if (minX == double.infinity) { minX = 0; maxX = 100; minY = 0; maxY = 100; }
double padding = 40.0;
double drawingW = maxX - minX + (padding * 2);
double drawingH = maxY - minY + (padding * 2);
if (drawingW <= 0) drawingW = 100;
if (drawingH <= 0) drawingH = 100;
double scaleX = size.width / drawingW;
double scaleY = size.height / drawingH;
double scale = (scaleX < scaleY) ? scaleX : scaleY;
double offsetX = (size.width - (drawingW * scale)) / 2;
double offsetY = (size.height - (drawingH * scale)) / 2;
canvas.save();
canvas.translate(offsetX, offsetY);
canvas.scale(scale);
canvas.translate(-minX + padding, -minY + padding);
// --- DISEGNO STRADE E FRECCE ---
Paint pStrada = Paint()
..color = Colors.black
..strokeWidth = 4.0 / scale
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
for (var t in tr) {
if (t.punti.length > 1) {
Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy);
for (var pt in t.punti) path.lineTo(pt.dx, pt.dy);
canvas.drawPath(path, pStrada);
if (t.tipo == 'freccia') {
double a = (t.punti.last - t.punti[t.punti.length - 2]).direction;
canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a - 0.5, 15), pStrada);
canvas.drawLine(t.punti.last, t.punti.last - Offset.fromDirection(a + 0.5, 15), pStrada);
}
}
}
// --- DISEGNO ELEMENTI ---
for (var e in el) {
canvas.save();
canvas.translate(e.posizione.dx, e.posizione.dy);
canvas.rotate(e.rotazione);
if (e.tipo == 'testo') {
final tp = TextPainter(
text: TextSpan(text: e.label ?? "", style: const TextStyle(color: Colors.black, fontSize: 24, fontWeight: FontWeight.bold)),
textDirection: TextDirection.ltr
)..layout();
tp.paint(canvas, Offset(-tp.width/2, -tp.height/2));
}
else {
String lettera = e.label ?? "A";
int idx = (lettera.isNotEmpty) ? (lettera.codeUnitAt(0) - 65) % palette.length : 0;
Color colore = palette[idx];
if (e.tipo.startsWith('auto')) {
_disegnaAuto(canvas, colore, lettera);
} else if (e.tipo.startsWith('moto')) {
_disegnaMoto(canvas, colore, lettera);
} else if (e.tipo.startsWith('furgone')) {
_disegnaFurgone(canvas, colore, lettera);
}
}
canvas.restore();
}
canvas.restore();
}
// --- METODI DI DISEGNO SPECIFICI ---
void _disegnaAuto(Canvas canvas, Color colore, String lettera) {
double w = 48.0; double h = 24.0;
Paint pBody = Paint()..color = colore;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5;
// Corpo
RRect bodyRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset.zero, width: w, height: h), const Radius.circular(5));
canvas.drawRRect(bodyRect, pBody);
// Cabina
Paint pCabin = Paint()..color = Colors.black.withOpacity(0.2);
canvas.drawRect(Rect.fromLTRB(-w/2 + 6, -h/2 + 3, w/4, h/2 - 3), pCabin);
// Fari
Paint pLights = Paint()..color = Colors.yellow;
canvas.drawCircle(Offset(w/2 - 2, h/2 - 4), 2.5, pLights);
canvas.drawCircle(Offset(w/2 - 2, -h/2 + 4), 2.5, pLights);
// Bordo
canvas.drawRRect(bodyRect, pBorder);
// Lettera
_disegnaLettera(canvas, lettera);
}
void _disegnaMoto(Canvas canvas, Color colore, String lettera) {
double w = 34.0; double h = 12.0;
Paint pBody = Paint()..color = colore;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.0;
Paint pBlack = Paint()..color = Colors.black;
// Ruote (piene)
canvas.drawCircle(Offset(-w/2 + 4, 0), 5.0, pBlack);
canvas.drawCircle(Offset(w/2 - 4, 0), 5.0, pBlack);
// Corpo
Rect bodyRect = Rect.fromCenter(center: Offset.zero, width: w - 10, height: h - 4);
canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBody);
canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBorder);
// Sella
canvas.drawRect(Rect.fromCenter(center: Offset(-5, 0), width: 12, height: 8), pBlack);
// Manubrio
Paint pManubrio = Paint()..color = Colors.black..strokeWidth = 3.0..strokeCap = StrokeCap.round;
canvas.drawLine(Offset(w/2 - 12, -10), Offset(w/2 - 12, 10), pManubrio);
// Faro
canvas.drawCircle(Offset(w/2, 0), 3.0, Paint()..color = Colors.yellow);
_disegnaLettera(canvas, lettera, fontSize: 10);
}
void _disegnaFurgone(Canvas canvas, Color colore, String lettera) {
double w = 60.0; double h = 26.0;
Paint pBody = Paint()..color = colore;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5;
// Vano Carico (Box posteriore)
Rect caricoRect = Rect.fromLTRB(-w/2, -h/2, w/4, h/2);
canvas.drawRect(caricoRect, pBody);
canvas.drawRect(caricoRect, pBorder);
// Cabina (Box anteriore)
Rect cabinaRect = Rect.fromLTRB(w/4, -h/2 + 1, w/2, h/2 - 1);
canvas.drawRect(cabinaRect, pBody);
canvas.drawRect(cabinaRect, pBorder);
// Parabrezza
Paint pVetro = Paint()..color = Colors.black.withOpacity(0.3);
canvas.drawRect(Rect.fromLTRB(w/4 + 2, -h/2 + 3, w/2 - 2, h/2 - 3), pVetro);
// Fari
Paint pLights = Paint()..color = Colors.yellow;
canvas.drawRect(Rect.fromLTWH(w/2 - 2, -h/2 + 2, 2, 4), pLights);
canvas.drawRect(Rect.fromLTWH(w/2 - 2, h/2 - 6, 2, 4), pLights);
_disegnaLettera(canvas, lettera);
}
void _disegnaLettera(Canvas canvas, String txt, {double fontSize = 16}) {
final tp = TextPainter(
text: TextSpan(text: txt, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize)),
textDirection: TextDirection.ltr
)..layout();
tp.paint(canvas, Offset(-tp.width/2, -tp.height/2));
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
=== FILE: lib/comp_13.dart ===
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'global_data.dart';
import 'models.dart';
import 'comp_15.dart';
class Comp13Screen extends StatefulWidget {
const Comp13Screen({super.key});
@override
State<Comp13Screen> createState() => _Comp13ScreenState();
}
class _Comp13ScreenState extends State<Comp13Screen> {
List<ElementoGrafico> _elementi = [];
List<TrattoPenna> _tratti = [];
String modo = 'penna';
@override
void initState() {
super.initState();
_elementi = List.from(GlobalData.elementi);
_tratti = List.from(GlobalData.tratti);
// Proviamo a chiedere il landscape al sistema (non sempre funziona su iPad)
_setLandscape();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _mostraIstruzioni();
});
}
Future<void> _setLandscape() async {
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
Future<void> _setPortrait() async {
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
}
Future<void> _esci() async {
GlobalData.tratti = List.from(_tratti);
GlobalData.elementi = List.from(_elementi);
await _setPortrait();
if (mounted) Navigator.pop(context);
}
Future<void> _vaiAvanti() async {
GlobalData.tratti = List.from(_tratti);
GlobalData.elementi = List.from(_elementi);
// Prima di cambiare pagina, rimettiamo in verticale
// await _setPortrait();
if (mounted) {
await Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp15Screen()));
// Al ritorno, forziamo di nuovo orizzontale
await _setLandscape();
}
}
// --- FUNZIONE HELPER PER RUOTARE I DIALOGHI ---
// Se l'iPad è verticale, ruota il contenuto del dialogo di 90 gradi
Widget _ruotaSeNecessario(BuildContext context, Widget child) {
return OrientationBuilder(
builder: (context, orientation) {
return orientation == Orientation.portrait
? RotatedBox(quarterTurns: 1, child: child)
: child;
},
);
}
void _mostraIstruzioni() {
showDialog(
context: context,
builder: (ctx) => _ruotaSeNecessario(ctx, AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Row(
children: [
Icon(Icons.help_outline, color: Colors.blue.shade900),
const SizedBox(width: 10),
const Text("Guida al Disegno", style: TextStyle(fontWeight: FontWeight.bold)),
],
),
content: SizedBox(
width: 500, // Larghezza fissa per evitare problemi di layout ruotato
height: 300, // Altezza fissa per scrollare comodamente
child: Scrollbar(
thumbVisibility: true,
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: const Text(
"• 🖊 Usa la penna per disegnare le strade\n"
"• 🚗 🏍️ 🚛 Seleziona e tocca per aggiungere veicoli\n"
"• ↗️ Inserisci frecce di direzione\n"
"• 📝 Aggiungi testo (es. nomi vie)\n"
"• 🔄 Tocca un elemento per ruotarlo\n"
"• ❌ Premi a lungo un oggetto per eliminarlo\n"
"• 🗑️ Usa il tasto Cestino per resettare tutto",
style: TextStyle(fontSize: 16, height: 1.6, color: Colors.black87),
),
),
),
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade900,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
onPressed: () => Navigator.pop(ctx),
child: const Text("HO CAPITO", style: TextStyle(fontWeight: FontWeight.bold)),
),
)
],
)),
);
}
void _gestisciInserimento(Offset pos) async {
if (modo == 'testo') {
TextEditingController tc = TextEditingController();
await showDialog(
context: context,
builder: (c) => _ruotaSeNecessario(c, Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
width: 300,
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text("Inserisci Testo", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
TextField(
controller: tc,
autofocus: true,
decoration: const InputDecoration(hintText: "Nome via...", border: OutlineInputBorder())
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (tc.text.isNotEmpty) {
setState(() => _elementi.add(ElementoGrafico(pos, 'testo', label: tc.text)));
}
Navigator.pop(c);
},
child: const Text("INSERISCI")
)
],
),
),
)),
);
} else if (['auto', 'moto', 'furgone'].contains(modo)) {
int numeroVeicoli = _elementi.where((e) =>
e.tipo.startsWith('auto') ||
e.tipo.startsWith('moto') ||
e.tipo.startsWith('furgone')).length;
String prossimaLettera = String.fromCharCode(65 + numeroVeicoli);
setState(() {
_elementi.add(ElementoGrafico(pos, '$modo$prossimaLettera', label: prossimaLettera));
});
}
}
@override
Widget build(BuildContext context) {
// 1. Logica di orientamento principale (Body)
// Se siamo verticali, ruotiamo tutto di 90 gradi.
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (!didPop) await _esci();
},
child: OrientationBuilder(
builder: (context, orientation) {
final bool isPortrait = orientation == Orientation.portrait;
return Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
// Se Portrait -> Ruota Body. Se Landscape -> Body normale.
body: isPortrait
? RotatedBox(quarterTurns: 1, child: _buildBodyContent(context))
: _buildBodyContent(context),
);
},
),
);
}
// Contenuto principale estratto per facilitare la rotazione
Widget _buildBodyContent(BuildContext context) {
// Calcoliamo dimensioni sicure
final size = MediaQuery.of(context).size;
final double maxToolbarHeight = size.shortestSide * 0.8;
return SafeArea(
child: Stack(
children: [
Column(
children: [
Container(
height: 50,
color: Colors.blueGrey[50],
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black),
onPressed: _esci,
),
const Expanded(
child: Text(
"13. Grafico",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black, fontSize: 18, fontWeight: FontWeight.bold),
),
),
IconButton(
icon: const Icon(Icons.help_outline, color: Colors.black),
onPressed: _mostraIstruzioni
),
const SizedBox(width: 15),
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8)
),
icon: const Icon(Icons.check),
label: const Text("SALVA"),
onPressed: _vaiAvanti,
),
],
),
),
Expanded(
child: GestureDetector(
onLongPressStart: (d) {
setState(() {
_elementi.removeWhere((e) => e.contiene(d.localPosition));
_tratti.removeWhere((t) => t.contiene(d.localPosition));
});
},
onTapDown: (d) {
bool colpito = false;
for (var e in _elementi) {
if (e.contiene(d.localPosition)) {
setState(() => e.rotazione += 0.785);
colpito = true;
break;
}
}
if (!colpito && (['auto', 'moto', 'furgone', 'testo'].contains(modo))) {
_gestisciInserimento(d.localPosition);
}
},
onPanStart: (d) {
if (modo == 'penna' || modo == 'freccia') {
setState(() => _tratti.add(TrattoPenna([d.localPosition], tipo: modo)));
}
},
onPanUpdate: (d) {
if (modo == 'penna' || modo == 'freccia') {
setState(() => _tratti.last.punti.add(d.localPosition));
}
},
child: Container(
color: Colors.white,
width: double.infinity,
height: double.infinity,
child: CustomPaint(
painter: PainterV40(_tratti, _elementi),
size: Size.infinite,
),
),
),
),
],
),
Positioned(
left: 10, top: 60,
child: Container(
width: 55,
constraints: BoxConstraints(maxHeight: maxToolbarHeight),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.95),
borderRadius: BorderRadius.circular(25),
boxShadow: [const BoxShadow(color: Colors.black26, blurRadius: 4)],
border: Border.all(color: Colors.grey.shade300),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(25),
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_toolBtn(Icons.edit, 'penna', Colors.black),
_toolBtn(Icons.trending_flat, 'freccia', Colors.black),
_toolBtn(Icons.title, 'testo', Colors.black),
const Divider(indent: 8, endIndent: 8, height: 15),
_toolBtn(Icons.directions_car, 'auto', Colors.blue),
_toolBtn(Icons.two_wheeler, 'moto', Colors.orange.shade800),
_toolBtn(Icons.local_shipping, 'furgone', Colors.green),
const Divider(indent: 8, endIndent: 8, height: 15),
IconButton(
icon: const Icon(Icons.delete_forever, color: Colors.red),
tooltip: "Cancella tutto",
onPressed: () => setState(() { _tratti.clear(); _elementi.clear(); }),
),
],
),
),
),
),
),
],
),
);
}
Widget _toolBtn(IconData icon, String tool, Color activeColor) {
bool isSelected = modo == tool;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => setState(() => modo = tool),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: isSelected ? activeColor.withOpacity(0.15) : Colors.transparent,
border: isSelected ? Border.all(color: activeColor, width: 2) : null,
shape: BoxShape.circle,
),
child: Icon(
icon,
color: activeColor,
size: 24,
),
),
),
),
);
}
}
class PainterV40 extends CustomPainter {
final List<TrattoPenna> tr;
final List<ElementoGrafico> el;
PainterV40(this.tr, this.el);
final List<Color> palette = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red, Colors.teal];
@override
void paint(Canvas canvas, Size size) {
Paint pStrada = Paint()..color = Colors.black..strokeWidth = 3.0..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
for (var t in tr) {
if (t.punti.length > 1) {
// Disegno il tratto stradale
Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy);
for (var pt in t.punti) path.lineTo(pt.dx, pt.dy);
canvas.drawPath(path, pStrada);
// --- CORREZIONE FRECCIA ---
if (t.tipo == 'freccia') {
Offset pTip = t.punti.last;
Offset pBack = t.punti[t.punti.length - 2];
// STABILIZZAZIONE: Cerco un punto precedente che sia distante almeno 10 pixel
// dalla punta. Questo ignora i micro-movimenti finali del dito (jitter)
// che causavano l'inversione della freccia.
for (int i = t.punti.length - 2; i >= 0; i--) {
if ((t.punti[i] - pTip).distance > 10.0) {
pBack = t.punti[i];
break;
}
}
_disegnaPunta(canvas, pBack, pTip, pStrada);
}
// --------------------------
}
}
for (var e in el) {
canvas.save();
canvas.translate(e.posizione.dx, e.posizione.dy);
canvas.rotate(e.rotazione);
if (e.tipo == 'testo') {
_disegnaTesto(canvas, e.label ?? "");
} else if (e.tipo.startsWith('auto')) {
_disegnaAuto(canvas, e);
} else if (e.tipo.startsWith('moto')) {
_disegnaMoto(canvas, e);
} else if (e.tipo.startsWith('furgone')) {
_disegnaFurgone(canvas, e);
}
canvas.restore();
}
}
// ... (TUTTI GLI ALTRI METODI RESTANO IDENTICI) ...
Color _getColoreDaLettera(String lettera) {
if (lettera.isEmpty) return Colors.grey;
int idx = (lettera.codeUnitAt(0) - 65) % palette.length;
return palette[idx];
}
void _disegnaLettera(Canvas canvas, String lettera, {double fontSize = 16}) {
final tp = TextPainter(
text: TextSpan(text: lettera, style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: fontSize)),
textDirection: TextDirection.ltr
)..layout();
tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2));
}
void _disegnaAuto(Canvas canvas, ElementoGrafico e) {
String lettera = e.label ?? "A";
Color colore = _getColoreDaLettera(lettera);
double w = 48.0;
double h = 24.0;
Paint pBody = Paint()..color = colore;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5;
RRect bodyRect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset.zero, width: w, height: h), const Radius.circular(5));
canvas.drawRRect(bodyRect, pBody);
Paint pCabin = Paint()..color = Colors.black.withOpacity(0.2);
canvas.drawRect(Rect.fromLTRB(-w/2 + 6, -h/2 + 3, w/4, h/2 - 3), pCabin);
Paint pLights = Paint()..color = Colors.yellow;
canvas.drawCircle(Offset(w/2 - 2, h/2 - 4), 2.5, pLights);
canvas.drawCircle(Offset(w/2 - 2, -h/2 + 4), 2.5, pLights);
canvas.drawRRect(bodyRect, pBorder);
_disegnaLettera(canvas, lettera);
}
void _disegnaMoto(Canvas canvas, ElementoGrafico e) {
String lettera = e.label ?? "A";
Color colore = _getColoreDaLettera(lettera);
double len = 40.0;
double wid = 14.0;
Paint pBody = Paint()..color = colore;
Paint pBlack = Paint()..color = Colors.black;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.0;
canvas.drawCircle(Offset(-len/2 + 4, 0), 5.0, pBlack);
canvas.drawCircle(Offset(len/2 - 4, 0), 5.0, pBlack);
Rect bodyRect = Rect.fromCenter(center: Offset.zero, width: len - 10, height: wid - 4);
canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBody);
canvas.drawRRect(RRect.fromRectAndRadius(bodyRect, const Radius.circular(4)), pBorder);
Rect sellaRect = Rect.fromCenter(center: Offset(-5, 0), width: 12, height: 8);
canvas.drawRect(sellaRect, pBlack);
Paint pManubrio = Paint()..color = Colors.black..strokeWidth = 3.0..strokeCap = StrokeCap.round;
double xHandle = len/2 - 12;
canvas.drawLine(Offset(xHandle, -10), Offset(xHandle, 10), pManubrio);
Paint pLights = Paint()..color = Colors.yellow;
canvas.drawCircle(Offset(len/2, 0), 3.0, pLights);
_disegnaLettera(canvas, lettera, fontSize: 10);
}
void _disegnaFurgone(Canvas canvas, ElementoGrafico e) {
String lettera = e.label ?? "A";
Color colore = _getColoreDaLettera(lettera);
double w = 60.0;
double h = 26.0;
Paint pBody = Paint()..color = colore;
Paint pBorder = Paint()..style = PaintingStyle.stroke..color = Colors.black..strokeWidth = 1.5;
Rect caricoRect = Rect.fromLTRB(-w/2, -h/2, w/4, h/2);
canvas.drawRect(caricoRect, pBody);
canvas.drawRect(caricoRect, pBorder);
Rect cabinaRect = Rect.fromLTRB(w/4, -h/2 + 1, w/2, h/2 - 1);
canvas.drawRect(cabinaRect, pBody);
canvas.drawRect(cabinaRect, pBorder);
Paint pVetro = Paint()..color = Colors.black.withOpacity(0.3);
canvas.drawRect(Rect.fromLTRB(w/4 + 2, -h/2 + 3, w/2 - 2, h/2 - 3), pVetro);
Paint pLights = Paint()..color = Colors.yellow;
canvas.drawRect(Rect.fromLTWH(w/2 - 2, -h/2 + 2, 2, 4), pLights);
canvas.drawRect(Rect.fromLTWH(w/2 - 2, h/2 - 6, 2, 4), pLights);
_disegnaLettera(canvas, lettera);
}
void _disegnaTesto(Canvas canvas, String txt) {
final tp = TextPainter(
text: TextSpan(text: txt, style: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold)),
textDirection: TextDirection.ltr
)..layout();
tp.paint(canvas, Offset(-tp.width/2, -tp.height/2));
}
void _disegnaPunta(Canvas canvas, Offset p1, Offset p2, Paint paint) {
double angle = (p2 - p1).direction;
canvas.drawLine(p2, p2 - Offset.fromDirection(angle - 0.5, 10), paint);
canvas.drawLine(p2, p2 - Offset.fromDirection(angle + 0.5, 10), paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
=== FILE: lib/cid_data_manager.dart ===
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'global_data.dart';
import 'models.dart';
class CidDataManager {
// ===========================================================================
// METODI PER SCAMBIO DATI (QR/P2P)
// ===========================================================================
static Map<String, dynamic> estraiDatiPerExport() {
String lato = GlobalData.latoCorrente;
String val(String vA, String vB, String l) => l == 'A' ? vA : vB;
bool valB(bool bA, bool bB, String l) => l == 'A' ? bA : bB;
return {
'lato': lato,
'generali': {
'data': GlobalData.data_incidente,
'ora': GlobalData.ora,
'luogo': GlobalData.luogo,
'feriti': GlobalData.feriti,
'testimoni': GlobalData.testimoni,
'danni_materiali': GlobalData.Veicoli_danni_materiali_oltre,
'oggetti_diversi': GlobalData.Oggetti_diversi_danni_materiali,
},
'circostanze': _serializeCircostanze(lato == 'A' ? GlobalData.circostanzeA : GlobalData.circostanzeB),
'punti_urto': lato == 'A' ? GlobalData.puntiUrtoA_List : GlobalData.puntiUrtoB_List,
// CASSETTO 1: GRAFICO INCIDENTE (Strade, Auto)
'grafico': {
'tratti_dinamica': GlobalData.tratti.map((t) => t.toMap()).toList(),
'elementi_dinamica': GlobalData.elementi.map((e) => e.toMap()).toList(),
},
// CASSETTO 2: FIRMA (Dati grafici personali)
'firma': _serializePunti(lato == 'A' ? GlobalData.puntiFirmaA : GlobalData.puntiFirmaB),
'contraente': {
'cognome': val(GlobalData.Cognome_contraente_A, GlobalData.Cognome_contraente_B, lato),
'nome': val(GlobalData.Nome_contraente_A, GlobalData.Nome_contraente_B, lato),
'cf': val(GlobalData.Codice_Fiscale_contraente_A, GlobalData.Codice_Fiscale_contraente_B, lato),
'indirizzo': val(GlobalData.Indirizzo_contraente_A, GlobalData.Indirizzo_contraente_B, lato),
'cap': val(GlobalData.CAP_contraente_A, GlobalData.CAP_contraente_B, lato),
'stato': val(GlobalData.Stato_contraente_A, GlobalData.Stato_contraente_B, lato),
'tel': val(GlobalData.N_telefono_mail_contraente_A, GlobalData.N_telefono_mail_contraente_B, lato),
},
'veicolo': {
'marca': val(GlobalData.Marca_e_Tipo_A, GlobalData.Marca_e_Tipo_B, lato),
'targa': val(GlobalData.Targa_A, GlobalData.Targa_B, lato),
'stato_imm': val(GlobalData.Stato_immatricolazione_A, GlobalData.Stato_immatricolazione_B, lato),
'stato_imm2': val(GlobalData.Stato_immatricolazione2_A, GlobalData.Stato_immatricolazione2_B, lato),
'rimorchio': val(GlobalData.Rimorchio_A, GlobalData.Rimorchio_B, lato),
},
'assicurazione': {
'denominazione': val(GlobalData.Denominazione_A, GlobalData.Denominazione_B, lato),
'polizza': val(GlobalData.Numero_Polizza_A, GlobalData.Numero_Polizza_B, lato),
'agenzia': val(GlobalData.Agenzia_A, GlobalData.Agenzia_B, lato),
'denom_agenzia': val(GlobalData.Denominazione_agenzia_A, GlobalData.Denominazione_agenzia_B, lato),
'indirizzo_agenzia': val(GlobalData.Indirizzo_agenzia_A, GlobalData.Indirizzo_agenzia_B, lato),
'stato_agenzia': val(GlobalData.Stato_agenzia_A, GlobalData.Stato_agenzia_B, lato),
'tel_agenzia': val(GlobalData.N_tel_mail_agenzia_A, GlobalData.N_tel_mail_agenzia_B, lato),
'carta_verde': val(GlobalData.N_carta_verde_A, GlobalData.N_carta_verde_B, lato),
'validita_dal': val(GlobalData.Data_Inizio_Dal_A, GlobalData.Data_Inizio_Dal_B, lato),
'validita_al': val(GlobalData.Data_Scadenza_Al_A, GlobalData.Data_Scadenza_Al_B, lato),
'flag_danni': valB(GlobalData.FLAG_danni_mat_assicurati_A, GlobalData.FLAG_danni_mat_assicurati_B, lato),
},
'conducente': {
'cognome': val(GlobalData.Cognome_cond_A, GlobalData.Cognome_cond_B, lato),
'nome': val(GlobalData.Nome_cond_A, GlobalData.Nome_cond_B, lato),
'nascita': val(GlobalData.Data_nascita_cond_A, GlobalData.Data_nascita_cond_B, lato),
'cf': val(GlobalData.Cod_fiscale_cond_A, GlobalData.Cod_fiscale_cond_B, lato),
'indirizzo': val(GlobalData.Indirizzo_cond_A, GlobalData.Indirizzo_cond_B, lato),
'stato': val(GlobalData.Stato_cond_A, GlobalData.Stato_cond_B, lato),
'tel': val(GlobalData.N_tel_mail_cond_A, GlobalData.N_tel_mail_cond_B, lato),
'patente': val(GlobalData.N_Patente_cond_A, GlobalData.N_Patente_cond_B, lato),
'cat_patente': val(GlobalData.Categoria_cond_A, GlobalData.Categoria_cond_B, lato),
'scad_patente': val(GlobalData.Scadenza_cond_A, GlobalData.Scadenza_cond_B, lato),
},
'danni_osservazioni': {
'visibili': val(GlobalData.danni_visibili_A, GlobalData.danni_visibili_B, lato),
'osservazioni': val(GlobalData.osservazioni_A, GlobalData.osservazioni_B, lato),
}
};
}
static void importaDati(Map<String, dynamic> data) {
String latoRemoto = data['lato'] ?? 'B';
// 1. Dati Comuni
if (data['generali'] != null) {
var gen = data['generali'];
if ((gen['data'] ?? "").isNotEmpty) GlobalData.data_incidente = gen['data'];
if ((gen['ora'] ?? "").isNotEmpty) GlobalData.ora = gen['ora'];
if ((gen['luogo'] ?? "").isNotEmpty) GlobalData.luogo = gen['luogo'];
if (gen['feriti'] != null) GlobalData.feriti = gen['feriti'];
if (gen['testimoni'] != null) GlobalData.testimoni = gen['testimoni'];
if (gen['danni_materiali'] != null) GlobalData.Veicoli_danni_materiali_oltre = gen['danni_materiali'];
if (gen['oggetti_diversi'] != null) GlobalData.Oggetti_diversi_danni_materiali = gen['oggetti_diversi'];
}
// --- PROTEZIONE GRAFICO (CASSETTO 1) ---
// Solo se i dati arrivano da A, permettiamo di sovrascrivere il grafico comune.
// Se arrivano da B, IGNORIAMO questa chiave (così il disegno di A resta intatto).
if (latoRemoto == 'A' && data['grafico'] != null) {
var graf = data['grafico'];
if (graf['tratti_dinamica'] != null) {
List<dynamic> listRaw = graf['tratti_dinamica'];
GlobalData.tratti = listRaw.map((x) => TrattoPenna.fromMap(x)).toList();
}
if (graf['elementi_dinamica'] != null) {
List<dynamic> listRaw = graf['elementi_dinamica'];
GlobalData.elementi = listRaw.map((x) => ElementoGrafico.fromMap(x)).toList();
}
}
// --- PROTEZIONE FIRMA (CASSETTO 2) ---
// La firma NON viene bloccata. Ognuno ha la sua firma e deve poterla inviare.
// Viene salvata in variabili distinte (puntiFirmaA e puntiFirmaB).
if (data['firma'] != null) {
List<Offset?> puntiFirma = _deserializePunti(data['firma']);
if (latoRemoto == 'A') GlobalData.puntiFirmaA = puntiFirma;
else GlobalData.puntiFirmaB = puntiFirma; // Qui B salva la sua firma
}
// 3. Circostanze
if (data['circostanze'] != null) {
Map<String, dynamic> rawCirc = data['circostanze'];
Map<int, bool> mappaCirc = {};
rawCirc.forEach((k, v) => mappaCirc[int.tryParse(k) ?? 0] = v as bool);
if (latoRemoto == 'A') GlobalData.circostanzeA = mappaCirc;
else GlobalData.circostanzeB = mappaCirc;
}
// 4. Punti Urto
if (data['punti_urto'] != null) {
List<String> puntiRecuperati = List<String>.from(data['punti_urto']);
if (latoRemoto == 'A') {
GlobalData.puntiUrtoA_List = puntiRecuperati;
} else {
GlobalData.puntiUrtoB_List = puntiRecuperati;
}
}
void setVal(Function(String) setA, Function(String) setB, dynamic val) {
if (val == null) return;
if (latoRemoto == 'A') setA(val.toString()); else setB(val.toString());
}
void setBool(Function(bool) setA, Function(bool) setB, dynamic val) {
if (val == null) return;
if (latoRemoto == 'A') setA(val as bool); else setB(val as bool);
}
if (data['contraente'] != null) {
var c = data['contraente'];
setVal((v) => GlobalData.Cognome_contraente_A = v, (v) => GlobalData.Cognome_contraente_B = v, c['cognome']);
setVal((v) => GlobalData.Nome_contraente_A = v, (v) => GlobalData.Nome_contraente_B = v, c['nome']);
setVal((v) => GlobalData.Codice_Fiscale_contraente_A = v, (v) => GlobalData.Codice_Fiscale_contraente_B = v, c['cf']);
setVal((v) => GlobalData.Indirizzo_contraente_A = v, (v) => GlobalData.Indirizzo_contraente_B = v, c['indirizzo']);
setVal((v) => GlobalData.CAP_contraente_A = v, (v) => GlobalData.CAP_contraente_B = v, c['cap']);
setVal((v) => GlobalData.Stato_contraente_A = v, (v) => GlobalData.Stato_contraente_B = v, c['stato']);
setVal((v) => GlobalData.N_telefono_mail_contraente_A = v, (v) => GlobalData.N_telefono_mail_contraente_B = v, c['tel']);
}
if (data['veicolo'] != null) {
var v = data['veicolo'];
setVal((v) => GlobalData.Marca_e_Tipo_A = v, (v) => GlobalData.Marca_e_Tipo_B = v, v['marca']);
setVal((v) => GlobalData.Targa_A = v, (v) => GlobalData.Targa_B = v, v['targa']);
setVal((v) => GlobalData.Stato_immatricolazione_A = v, (v) => GlobalData.Stato_immatricolazione_B = v, v['stato_imm']);
setVal((v) => GlobalData.Stato_immatricolazione2_A = v, (v) => GlobalData.Stato_immatricolazione2_B = v, v['stato_imm2']);
setVal((v) => GlobalData.Rimorchio_A = v, (v) => GlobalData.Rimorchio_B = v, v['rimorchio']);
}
if (data['assicurazione'] != null) {
var a = data['assicurazione'];
setVal((v) => GlobalData.Denominazione_A = v, (v) => GlobalData.Denominazione_B = v, a['denominazione']);
setVal((v) => GlobalData.Numero_Polizza_A = v, (v) => GlobalData.Numero_Polizza_B = v, a['polizza']);
setVal((v) => GlobalData.Agenzia_A = v, (v) => GlobalData.Agenzia_B = v, a['agenzia']);
setVal((v) => GlobalData.Denominazione_agenzia_A = v, (v) => GlobalData.Denominazione_agenzia_B = v, a['denom_agenzia']);
setVal((v) => GlobalData.Indirizzo_agenzia_A = v, (v) => GlobalData.Indirizzo_agenzia_B = v, a['indirizzo_agenzia']);
setVal((v) => GlobalData.Stato_agenzia_A = v, (v) => GlobalData.Stato_agenzia_B = v, a['stato_agenzia']);
setVal((v) => GlobalData.N_tel_mail_agenzia_A = v, (v) => GlobalData.N_tel_mail_agenzia_B = v, a['tel_agenzia']);
setVal((v) => GlobalData.N_carta_verde_A = v, (v) => GlobalData.N_carta_verde_B = v, a['carta_verde']);
setVal((v) => GlobalData.Data_Inizio_Dal_A = v, (v) => GlobalData.Data_Inizio_Dal_B = v, a['validita_dal']);
setVal((v) => GlobalData.Data_Scadenza_Al_A = v, (v) => GlobalData.Data_Scadenza_Al_B = v, a['validita_al']);
setBool((v) => GlobalData.FLAG_danni_mat_assicurati_A = v, (v) => GlobalData.FLAG_danni_mat_assicurati_B = v, a['flag_danni']);
}
if (data['conducente'] != null) {
var c = data['conducente'];
setVal((v) => GlobalData.Cognome_cond_A = v, (v) => GlobalData.Cognome_cond_B = v, c['cognome']);
setVal((v) => GlobalData.Nome_cond_A = v, (v) => GlobalData.Nome_cond_B = v, c['nome']);
setVal((v) => GlobalData.Data_nascita_cond_A = v, (v) => GlobalData.Data_nascita_cond_B = v, c['nascita']);
setVal((v) => GlobalData.Cod_fiscale_cond_A = v, (v) => GlobalData.Cod_fiscale_cond_B = v, c['cf']);
setVal((v) => GlobalData.Indirizzo_cond_A = v, (v) => GlobalData.Indirizzo_cond_B = v, c['indirizzo']);
setVal((v) => GlobalData.Stato_cond_A = v, (v) => GlobalData.Stato_cond_B = v, c['stato']);
setVal((v) => GlobalData.N_tel_mail_cond_A = v, (v) => GlobalData.N_tel_mail_cond_B = v, c['tel']);
setVal((v) => GlobalData.N_Patente_cond_A = v, (v) => GlobalData.N_Patente_cond_B = v, c['patente']);
setVal((v) => GlobalData.Categoria_cond_A = v, (v) => GlobalData.Categoria_cond_B = v, c['cat_patente']);
setVal((v) => GlobalData.Scadenza_cond_A = v, (v) => GlobalData.Scadenza_cond_B = v, c['scad_patente']);
}
if (data['danni_osservazioni'] != null) {
var d = data['danni_osservazioni'];
setVal((v) => GlobalData.danni_visibili_A = v, (v) => GlobalData.danni_visibili_B = v, d['visibili']);
setVal((v) => GlobalData.osservazioni_A = v, (v) => GlobalData.osservazioni_B = v, d['osservazioni']);
}
}
// ===========================================================================
// METODI PER SALVATAGGIO CLOUD (FIREBASE)
// ===========================================================================
static Future<String> salvaDati(String sessionId, String lato) async {
final docRef = FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId);
// Estrae tutti i dati dalla memoria locale
Map<String, dynamic> datiExport = estraiDatiPerExport();
// Prepariamo i dati da inviare al server
Map<String, dynamic> updateData = {
'generali': datiExport['generali'],
'lato_$lato': {
...datiExport['contraente'],
...datiExport['veicolo'],
...datiExport['assicurazione'],
...datiExport['conducente'],
'danni_visibili': datiExport['danni_osservazioni']['visibili'],
'osservazioni': datiExport['danni_osservazioni']['osservazioni'],
'circostanze': datiExport['circostanze'],
'punti_urto': datiExport['punti_urto'],
'firma': datiExport['firma'], // <-- La firma è dentro "lato_A" o "lato_B", quindi è al sicuro
'completo': true,
},
'timestamp': FieldValue.serverTimestamp(),
};
// --- PROTEZIONE AGGIUNTIVA PER IL CLOUD ---
// Il GRAFICO lo aggiorniamo nel DB solo se siamo il LATO A.
// Il LATO B non ha diritto di toccare questa chiave.
if (lato == 'A') {
updateData['grafico'] = datiExport['grafico'];
}
await docRef.set(updateData, SetOptions(merge: true));
return sessionId;
}
// --- METODO RECUPERA DATI CLOUD ---
static Future<void> caricaDati(String sessionId, String latoDaCaricare) async {
final doc = await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).get();
if (!doc.exists) return;
final data = doc.data()!;
Map<String, dynamic> datiDaImportare = {};
datiDaImportare['lato'] = latoDaCaricare;
if (data['generali'] != null) datiDaImportare['generali'] = data['generali'];
if (data['grafico'] != null) datiDaImportare['grafico'] = data['grafico'];
if (data['lato_$latoDaCaricare'] != null) {
Map<String, dynamic> latoData = data['lato_$latoDaCaricare'];
datiDaImportare['contraente'] = latoData;
datiDaImportare['veicolo'] = latoData;
datiDaImportare['assicurazione'] = latoData;
datiDaImportare['conducente'] = latoData;
datiDaImportare['danni_osservazioni'] = {
'visibili': latoData['danni_visibili'],
'osservazioni': latoData['osservazioni'],
};
datiDaImportare['circostanze'] = latoData['circostanze'];
datiDaImportare['punti_urto'] = latoData['punti_urto'];
datiDaImportare['firma'] = latoData['firma']; // Recupera la firma specifica dal lato
}
importaDati(datiDaImportare);
}
// ===========================================================================
// HELPER SERIALIZZAZIONE
// ===========================================================================
static Map<String, bool> _serializeCircostanze(Map<int, bool> circ) {
return circ.map((key, value) => MapEntry(key.toString(), value));
}
static List<Map<String, double>> _serializePunti(List<Offset?> punti) {
List<Map<String, double>> res = [];
for (var p in punti) {
if (p != null) res.add({'dx': p.dx, 'dy': p.dy});
}
return res;
}
static List<Offset?> _deserializePunti(dynamic lista) {
if (lista is! List) return [];
return lista.map<Offset?>((item) {
if (item == null) return null;
return Offset((item['dx'] as num).toDouble(), (item['dy'] as num).toDouble());
}).toList();
}
}
=== FILE: lib/firebase_options.dart ===
// File: lib/firebase_options.dart
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, TargetPlatform, kIsWeb;
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web',
);
}
switch (defaultTargetPlatform) {
// =======================================================
// 🤖 CONFIGURAZIONE ANDROID (Compilata con i tuoi dati JSON)
// =======================================================
case TargetPlatform.android:
return const FirebaseOptions(
apiKey: 'AIzaSyB9f420xu5fu_aaaFgvRYMQVS2L8Ddudbo', // Presa dal tuo JSON
appId: '1:1060927868658:android:84884a85cd60c4e8084a6b', // Presa dal tuo JSON
messagingSenderId: '1060927868658',
projectId: 'cid-app-sincro',
storageBucket: 'cid-app-sincro.firebasestorage.app',
databaseURL: 'https://cid-app-sincro-default-rtdb.europe-west1.firebasedatabase.app',
);
// =======================================================
// 🍎 CONFIGURAZIONE iOS (Compilata con i tuoi dati PLIST)
// =======================================================
case TargetPlatform.iOS:
return const FirebaseOptions(
apiKey: 'AIzaSyBDEXTqdXbwNVeK4yy8m9uMwk1xzeSYWJ8',
appId: '1:1060927868658:ios:be05f4b773c220e3084a6b',
messagingSenderId: '1060927868658',
projectId: 'cid-app-sincro',
storageBucket: 'cid-app-sincro.firebasestorage.app',
databaseURL: 'https://cid-app-sincro-default-rtdb.europe-west1.firebasedatabase.app',
iosBundleId: 'com.example.cidApp',
);
case TargetPlatform.macOS:
throw UnsupportedError('MacOS not configured');
case TargetPlatform.windows:
throw UnsupportedError('Windows not configured');
case TargetPlatform.linux:
throw UnsupportedError('Linux not configured');
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
}
=== FILE: lib/build_cai_app.dart ===
import 'dart:io';
void main() async {
print("🛠️ Avvio build...");
// 1. Esegui la build
var process = await Process.start('flutter', ['build', 'appbundle']);
// Mostra l'output in tempo reale
stdout.addStream(process.stdout);
stderr.addStream(process.stderr);
var exitCode = await process.exitCode;
if (exitCode != 0) {
print("❌ Errore nella build.");
return;
}
// 2. Leggi la versione dal pubspec.yaml
var pubspec = await File('pubspec.yaml').readAsLines();
var versionLine = pubspec.firstWhere((line) => line.startsWith('version:'));
var version = versionLine.split('version: ')[1].trim();
// 3. DEFINIZIONE PERCORSI
// A. DOVE SI TROVA ORA (Sempre nella cartella del progetto)
var sourceFile = File('build/app/outputs/bundle/release/app-release.aab');
// B. DOVE LO VUOI METTERE (Il tuo disco esterno)
// Nota: Assicurati che la cartella "buid" esista o correggi in "build" se era un refuso
var targetDirectory = '/Volumes/NVME-2TB/cai/buid';
if (await sourceFile.exists()) {
// Crea la directory sul disco esterno se non esiste (per sicurezza)
await Directory(targetDirectory).create(recursive: true);
// Crea il percorso completo di destinazione
var newPath = '$targetDirectory/CAI_App_v$version.aab';
// Esegue la copia
await sourceFile.copy(newPath);
print("✅ File creato con successo!");
print("📂 Destinazione: $newPath");
} else {
print("❌ Impossibile trovare il file generato in build/app/outputs/...");
}
}
=== FILE: lib/comp_12.dart ===
// Versione: FINAL - SLIVER LAYOUT (Footer Sicuro e Testi Corretti)
import 'package:flutter/material.dart';
import 'global_data.dart';
import 'comp_13.dart'; // IMPORTA IL GRAFICO
import 'comp_15.dart'; // IMPORTA LE FIRME
class Comp12Screen extends StatefulWidget {
const Comp12Screen({super.key});
@override
_Comp12ScreenState createState() => _Comp12ScreenState();
}
class _Comp12ScreenState extends State<Comp12Screen> {
final List<String> _testiCircostanze = [
"1. In fermata / in sosta",
"2. Ripartiva dopo una sosta / apriva una portiera",
"3. Stava parcheggiando",
"4. Usciva da un parcheggio / luogo privato",
"5. Entrava in un parcheggio / luogo privato",
"6. Si immetteva in una piazza a senso rotatorio",
"7. Circolava su una piazza a senso rotatorio",
"8. Tamponava procedendo nello stesso senso",
"9. Procedeva nello stesso senso ma in fila diversa",
"10. Cambiava fila",
"11. Sorpassava",
"12. Girava a destra",
"13. Girava a sinistra",
"14. Retrocedeva",
"15. Invadeva la sede stradale riservata",
"16. Proveniva da destra",
"17. Non osservava il segnale di precedenza/semaforo"
];
bool _isReady = false;
@override
void initState() {
super.initState();
_inizializzaPagina();
}
Future<void> _inizializzaPagina() async {
await Future.delayed(const Duration(milliseconds: 200));
if (mounted) {
setState(() => _isReady = true);
// MOSTRA IL POPUP ANIMATO ALL'AVVIO
WidgetsBinding.instance.addPostFrameCallback((_) => _mostraInfoPopup(context));
}
}
// --- POPUP INFORMATIVO ANIMATO ---
void _mostraInfoPopup(BuildContext context) {
bool isB = GlobalData.latoCorrente == 'B';
Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900;
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierLabel: "Popup",
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 400),
pageBuilder: (context, animation, secondaryAnimation) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
children: [
Icon(Icons.fact_check, color: activeColor, size: 28),
const SizedBox(width: 10),
const Expanded(child: Text("Circostanze Incidente", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("Indica l'esatta dinamica del Veicolo ${GlobalData.latoCorrente} al momento dell'urto.", style: const TextStyle(fontSize: 15)),
const SizedBox(height: 16),
_buildPopupRow(Icons.check_box_outlined, "Selezione Multipla", "Puoi spuntare anche più di una circostanza, ma assicurati che descrivano correttamente l'accaduto."),
const SizedBox(height: 12),
_buildPopupRow(Icons.warning_amber_rounded, "Attenzione", "Le circostanze selezionate in questa pagina sono fondamentali per stabilire la responsabilità del sinistro!"),
],
),
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
foregroundColor: isB ? Colors.black87 : Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
onPressed: () => Navigator.pop(context),
child: const Text("HO CAPITO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
var curvePosizione = CurvedAnimation(
parent: animation,
curve: Curves.easeOutBack,
reverseCurve: Curves.easeInBack,
);
var curveOpacita = CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.4),
end: Offset.zero,
).animate(curvePosizione),
child: FadeTransition(
opacity: curveOpacita,
child: child,
),
);
},
);
}
Widget _buildPopupRow(IconData icon, String title, String desc) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 24, color: Colors.blueGrey),
const SizedBox(width: 12),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
children: [
TextSpan(text: "$title: ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: desc),
],
),
),
),
],
);
}
void _prosegui() {
bool isB = GlobalData.latoCorrente == 'B';
if (isB) {
// IL LATO B SALTA IL GRAFICO E VA DIRETTAMENTE ALLE FIRME (15)
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const Comp15Screen()),
);
} else {
// IL LATO A VA AL GRAFICO (13)
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const Comp13Screen()),
);
}
}
@override
Widget build(BuildContext context) {
bool isB = GlobalData.latoCorrente == 'B';
Color mainCol = isB ? Colors.amber.shade700 : Colors.blue.shade900;
Color bgCol = isB ? const Color(0xFFFFF9C4) : const Color(0xFFF5F7FA);
if (!_isReady) {
return Scaffold(backgroundColor: bgCol, body: Container());
}
return Scaffold(
backgroundColor: bgCol,
appBar: AppBar(
title: Text("12. Circostanze (${GlobalData.latoCorrente})"),
backgroundColor: mainCol,
foregroundColor: isB ? Colors.black : Colors.white,
),
// --- SLIVER LAYOUT ---
body: SafeArea(
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// 1. Header (Testo Istruzioni)
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.all(15),
color: Colors.white,
margin: const EdgeInsets.only(bottom: 1),
child: Text(
"Seleziona le circostanze per il veicolo ${GlobalData.latoCorrente}.",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: mainCol),
),
),
),
// 2. Lista Scrollabile (Le 17 Checkbox)
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
int circIndex = index + 1;
bool isChecked = isB
? (GlobalData.circostanzeB[circIndex] ?? false)
: (GlobalData.circostanzeA[circIndex] ?? false);
return Column(
children: [
Container(
color: Colors.white,
child: CheckboxListTile(
activeColor: mainCol,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
title: Text("${circIndex}. ${_testiCircostanze[index]}", style: const TextStyle(fontSize: 14)),
value: isChecked,
onChanged: (bool? val) {
setState(() {
if (isB) {
GlobalData.circostanzeB[circIndex] = val ?? false;
} else {
GlobalData.circostanzeA[circIndex] = val ?? false;
}
});
},
),
),
const Divider(height: 1, thickness: 1, indent: 16, endIndent: 16),
],
);
},
childCount: _testiCircostanze.length,
),
),
// 3. Piè di pagina elastico (Bottoni)
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.all(20),
child: _navButtons(context, mainCol, isB),
),
),
),
],
),
),
);
}
Widget _navButtons(BuildContext context, Color color, bool isB) {
return Row(
children: [
// Tasto INDIETRO (Expanded flex 1)
Expanded(
flex: 4, // Proporzione 4/10
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, 55),
padding: const EdgeInsets.symmetric(horizontal: 5), // Padding ridotto
),
// FittedBox evita che il testo vada a capo se lo spazio è poco
child: const FittedBox(
child: Text("INDIETRO", style: TextStyle(fontWeight: FontWeight.bold))
)
)
),
const SizedBox(width: 15),
// Tasto SALVA E PROCEDI (Expanded flex 2)
Expanded(
flex: 6, // Proporzione 6/10 (più largo)
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: isB ? Colors.black : Colors.white,
minimumSize: const Size(0, 55)
),
onPressed: _prosegui,
child: const FittedBox(
child: Text("SALVA E PROCEDI", style: TextStyle(fontWeight: FontWeight.bold))
)
)
),
],
);
}
}
=== FILE: lib/utils/cai_constants.dart ===
class CaiMapping {
static const Map<String, String> fields = {
// --- 1. INFORMAZIONI GENERALI ---
'dataIncidente': 'data sinistro',
'oraIncidente': 'ora',
'luogoIncidente': 'luogo sinistro',
// --- 2. VEICOLO A (Colonna Sinistra) ---
// Contraente/Assicurato A
'cognomeContraenteA': 'cognome',
'nomeContraenteA': 'nome',
'codiceFiscaleA': 'codice fiscale',
'comuneA': 'comune',
'capA': 'cap',
'statoA': 'stato',
// Dati Veicolo A
'marcaVeicoloA': 'marca e tipo',
'targaA': 'targa',
'statoImmatricolazioneA': 'stato immatricolazione',
// Compagnia Assicurativa A
'compagniaA': 'COMPAGNIA',
'numeroPolizzaA': 'numero polizza',
'agenziaA': 'AGENZIA',
// Conducente A (ATTENZIONE: Refuso 'cogmome' nel PDF originale)
'cognomeConducenteA': 'cogmome',
'nomeConducenteA': 'Nome',
'codiceFiscaleConducenteA': 'Codice fiscale',
'patenteA': 'numero patente',
// --- 3. VEICOLO B (Colonna Destra) ---
// Contraente/Assicurato B
'cognomeContraenteB': 'Cognome assicurato',
'nomeContraenteB': 'Nome Assicurato',
'codiceFiscaleB': 'codice fiscale assicurato',
'comuneB': 'comune/prov/indirizzo',
// Dati Veicolo B
'marcaVeicoloB': 'marca e modello',
'targaB': 'targa1',
'statoImmatricolazioneB': 'stato immatricolazione1',
// Compagnia Assicurativa B
'compagniaB': 'compagnia1',
'numeroPolizzaB': 'numero polizza1',
'agenziaB': 'agenzia1',
// Conducente B
'cognomeConducenteB': 'cognome1',
'nomeConducenteB': 'nome1',
'codiceFiscaleConducenteB': 'codice fiscale1',
'patenteB': 'num patente',
// --- 4. TESTIMONI E OSSERVAZIONI ---
'testimone1': '1° teste',
'testimone2': '2° teste',
'osservazioniA': 'osservazioni',
'osservazioniB': 'osservazioni1',
};
}
=== FILE: lib/test_scraping.dart ===
import 'package:flutter/material.dart';
import 'verifica_rca_screen.dart'; // Assicurati che l'import sia corretto
void main() {
runApp(const MaterialApp(
home: TestScrapingPage(),
));
}
class TestScrapingPage extends StatefulWidget {
const TestScrapingPage({super.key});
@override
_TestScrapingPageState createState() => _TestScrapingPageState();
}
class _TestScrapingPageState extends State<TestScrapingPage> {
// Metti una targa vera qui per fare prima nei test
final TextEditingController _targaController = TextEditingController(text: "AB123CD");
String _risultato = "Nessun dato ancora";
void _lanciaVerifica() async {
// 1. Lancia la schermata di verifica
final dataScadenza = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VerificaRcaScreen(targa: _targaController.text),
),
);
// 2. Quando torni indietro, se c'è un risultato, mostralo
if (dataScadenza != null) {
setState(() {
_risultato = "Scadenza trovata: $dataScadenza";
});
// Feedback visivo
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Data importata: $dataScadenza"), backgroundColor: Colors.green),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("TEST SCRAPING RCA"), backgroundColor: Colors.orange),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Inserisci una targa reale per testare:", style: TextStyle(fontSize: 16)),
const SizedBox(height: 10),
TextField(
controller: _targaController,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: "Targa",
hintText: "Es. GA000GA",
),
textCapitalization: TextCapitalization.characters,
),
const SizedBox(height: 30),
ElevatedButton.icon(
onPressed: _lanciaVerifica,
icon: const Icon(Icons.search),
label: const Text("VERIFICA COPERTURA"),
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
textStyle: const TextStyle(fontSize: 18),
),
),
const SizedBox(height: 40),
const Divider(),
const Text("RISULTATO:", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey)),
const SizedBox(height: 10),
Text(
_risultato,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.blue),
),
],
),
),
);
}
}
=== FILE: lib/main.dart ===
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
// ⚠️ IMPORTANTE: Assicurati che questo file esista (generato da flutterfire configure)
import 'firebase_options.dart';
import 'global_data.dart';
import 'scelta_lato.dart';
import 'carro_attr.dart';
import 'ps.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. BLOCCO INIZIALE VERTICALE
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// 2. INIZIALIZZAZIONE FIREBASE CON BIVIO IOS/ANDROID
// Il controllo .isEmpty evita il crash "Duplicate App" durante il debug
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
// Questa riga gestisce automaticamente il bivio tra le chiavi Android e iOS
// leggendole dal file firebase_options.dart
options: DefaultFirebaseOptions.currentPlatform,
);
}
_effettuaLoginAnonimo();
runApp(const MyApp());
}
Future<void> _effettuaLoginAnonimo() async {
try {
if (FirebaseAuth.instance.currentUser == null) {
await FirebaseAuth.instance.signInAnonymously();
debugPrint("✅ Login anonimo effettuato.");
}
} catch (e) {
debugPrint("⚠️ Login anonimo fallito (ritento tra 2s): $e");
Future.delayed(const Duration(seconds: 2), _effettuaLoginAnonimo);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'La tua App CAI',
debugShowCheckedModeBanner: false,
// --- IMPOSTIAMO I COLORI GLOBALI DEL CALENDARIO E DELL'APP ---
theme: ThemeData.light().copyWith(
colorScheme: const ColorScheme.light(
primary: Color(0xFF1565C0), // Blu (usato per i calendari)
onPrimary: Colors.white,
onSurface: Colors.black,
),
),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('it', 'IT'),
],
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with RouteAware {
@override
void initState() {
super.initState();
// Al primo avvio puliamo tutto
_resetCompleto();
}
// Metodo centralizzato per reset e orientamento
Future<void> _resetCompleto() async {
GlobalData.reset();
await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
debugPrint("🏠 Home: Dati resettati e Verticale forzato.");
}
@override
Widget build(BuildContext context) {
// Intercettiamo il "ritorno" alla home per forzare il verticale
// (utile se si torna indietro con gesture o tasto back fisico)
return PopScope(
canPop: false, // La home è la root, non si esce
child: Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: const Text(
'CAI Facile',
style: TextStyle(fontWeight: FontWeight.w900, letterSpacing: 1.5, fontSize: 24)
),
centerTitle: true,
backgroundColor: Colors.blue.shade900.withOpacity(0.95),
foregroundColor: Colors.white,
elevation: 10,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20))
),
),
body: Stack(
children: [
// 1. Sfondo Base (Mappa)
const Positioned.fill(child: BackgroundImage()),
// Contenuto
SafeArea(
child: Center(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1. Compila Sinistro
_buildButton3D(
context: context,
label: "COMPILA SINISTRO",
icon: Icons.assignment_outlined,
baseColor: Colors.blue.shade800,
onTap: () async {
// Reset esplicito prima di iniziare
await _resetCompleto();
if (context.mounted) {
Navigator.push(
context,
MaterialPageRoute(builder: (c) => const SceltaLatoScreen())
).then((_) {
// Quando l'utente torna indietro dalla compilazione,
// forziamo di nuovo il reset e il verticale
_resetCompleto();
});
}
},
),
const SizedBox(height: 25),
// 2. Carro Attrezzi
_buildButton3D(
context: context,
label: "SOS CARRO ATTREZZI",
icon: Icons.support_agent,
baseColor: Colors.red.shade800,
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (c) => const CarroAttrezziScreen()));
},
),
const SizedBox(height: 25),
// 3. Pronto Soccorso
_buildButton3D(
context: context,
label: "PRONTO SOCCORSO",
icon: Icons.local_hospital_outlined,
baseColor: Colors.green.shade800,
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (c) => const ProntoSoccorsoScreen()));
},
),
],
),
),
),
),
),
],
),
),
);
}
// Widget helper per i bottoni con effetto 3D
Widget _buildButton3D({
required BuildContext context,
required String label,
required IconData icon,
required Color baseColor,
required VoidCallback onTap,
}) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.4), offset: const Offset(0, 6), blurRadius: 8, spreadRadius: 1)
],
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
baseColor.withOpacity(0.9),
baseColor,
baseColor.withRed((baseColor.red - 20).clamp(0, 255))
],
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(16),
onTap: onTap,
splashColor: Colors.white.withOpacity(0.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
child: Row(
children: [
Icon(icon, size: 32, color: Colors.white),
const SizedBox(width: 20),
Expanded(
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w800,
fontSize: 18,
letterSpacing: 1.0
)
)
),
Icon(Icons.arrow_forward_ios, size: 18, color: Colors.white.withOpacity(0.7)),
],
),
),
),
),
);
}
}
// Widget Sfondo
class BackgroundImage extends StatelessWidget {
const BackgroundImage({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: const Color(0xFFF0F4F8),
child: Image.asset(
'assets/sfondo_mappa.jpg',
fit: BoxFit.cover,
color: const Color(0xFFF0F4F8).withOpacity(0.6),
colorBlendMode: BlendMode.lighten,
errorBuilder: (c, e, s) => Container(color: Colors.grey.shade200),
),
);
}
}
=== FILE: lib/verifica_rca_screen.dart ===
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
class VerificaRcaScreen extends StatefulWidget {
final String targa;
const VerificaRcaScreen({super.key, required this.targa});
@override
_VerificaRcaScreenState createState() => _VerificaRcaScreenState();
}
class _VerificaRcaScreenState extends State<VerificaRcaScreen> {
InAppWebViewController? webViewController;
bool isLoading = true;
String? dataScadenzaTrovata;
bool ricercaFallita = false;
final String urlPortale = "https://www.ilportaledellautomobilista.it/web/portale-automobilista/verifica-copertura-rc";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Verifica Copertura RCA"),
backgroundColor: Colors.blue[900],
foregroundColor: Colors.white,
),
body: Stack(
children: [
// 1. WEBVIEW (Il motore nascosto/mascherato)
InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(urlPortale)),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
supportZoom: false,
),
onWebViewCreated: (controller) {
webViewController = controller;
},
onLoadStop: (controller, url) async {
setState(() { isLoading = false; });
// Appena caricata, nascondiamo la grafica del sito
await _preparaPagina();
},
onProgressChanged: (controller, progress) {
// Ogni volta che la pagina cambia (es. dopo il click su Cerca), controlliamo se c'è il risultato
if (progress == 100) {
_cercaRisultato();
}
},
),
// 2. LOADER / SCHERMATA SUCCESSO (Copre la WebView quando serve)
if (isLoading || dataScadenzaTrovata != null || ricercaFallita)
Container(
color: Colors.white,
width: double.infinity,
height: double.infinity,
child: Center(
child: _buildOverlayContent(),
),
),
],
),
);
}
Widget _buildOverlayContent() {
if (ricercaFallita) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 60),
const SizedBox(height: 20),
const Text("Veicolo non assicurato\no targa errata", textAlign: TextAlign.center, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 30),
ElevatedButton(
onPressed: () {
setState(() {
ricercaFallita = false;
isLoading = false; // Rimostra la webview per riprovare
webViewController?.reload();
});
},
child: const Text("Riprova"),
)
],
);
}
if (dataScadenzaTrovata != null) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 80),
const SizedBox(height: 20),
const Text("Scadenza Trovata!", style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
Text(dataScadenzaTrovata!, style: TextStyle(fontSize: 30, color: Colors.blue[900], fontWeight: FontWeight.bold)),
const SizedBox(height: 40),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[900],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 15),
),
onPressed: () {
Navigator.pop(context, dataScadenzaTrovata);
},
child: const Text("USA QUESTA DATA", style: TextStyle(fontSize: 18)),
)
],
);
}
return const CircularProgressIndicator();
}
// --- FUNZIONI DI SCRAPING ---
Future<void> _preparaPagina() async {
if (webViewController == null) return;
// 1. INIEZIONE CSS: Stile "App Mobile"
await webViewController!.evaluateJavascript(source: """
// Nascondi tutto il contorno inutile
var elementsToHide = document.querySelectorAll('header, footer, .navbar, .breadcrumb, #cookie-bar, .portlet-title, .lfr-meta-actions');
elementsToHide.forEach(el => el.style.display = 'none');
var mainForm = document.querySelector('form');
if(mainForm) {
document.body.innerHTML = '';
document.body.appendChild(mainForm);
document.body.style.backgroundColor = '#ffffff';
document.body.style.padding = '20px';
document.body.style.fontFamily = 'sans-serif';
}
""");
// 2. INIEZIONE JS: Compilazione e UX
await webViewController!.evaluateJavascript(source: """
// A. Compila Targa
var campoTarga = document.getElementById('targa');
if (campoTarga) {
campoTarga.value = '${widget.targa}';
campoTarga.style.fontSize = '24px';
campoTarga.style.fontWeight = 'bold';
campoTarga.style.textAlign = 'center';
campoTarga.style.border = '2px solid #1565C0';
campoTarga.readOnly = true;
}
// B. Seleziona "Autoveicolo"
var selectVeicolo = document.querySelector('select');
if (selectVeicolo) {
for (var i = 0; i < selectVeicolo.options.length; i++) {
if (selectVeicolo.options[i].text.toLowerCase().includes('auto')) {
selectVeicolo.selectedIndex = i;
break;
}
}
// Nascondi la select e la sua label
selectVeicolo.style.display = 'none';
if(selectVeicolo.previousElementSibling) selectVeicolo.previousElementSibling.style.display = 'none';
}
// C. Restyling Bottone Ricerca
var btn = document.querySelector("input[name='ricercaCoperturaVeicolo']");
if (btn) {
btn.style.cssText = '';
btn.style.width = '100%';
btn.style.height = '60px';
btn.style.backgroundColor = '#1565C0';
btn.style.color = 'white';
btn.style.border = 'none';
btn.style.borderRadius = '12px';
btn.style.fontSize = '20px';
btn.style.marginTop = '30px';
btn.value = 'CERCA SCADENZA';
}
// D. Focus sul Captcha
var campoCaptcha = document.querySelector("input[name*='captcha']");
if(campoCaptcha) {
campoCaptcha.placeholder = 'Inserisci i caratteri qui';
campoCaptcha.style.height = '50px';
campoCaptcha.style.fontSize = '20px';
campoCaptcha.style.textAlign = 'center';
campoCaptcha.style.marginTop = '10px';
campoCaptcha.scrollIntoView();
}
""");
}
Future<void> _cercaRisultato() async {
if (webViewController == null) return;
// Leggiamo tutto l'HTML della pagina
String? html = await webViewController!.getHtml();
if (html == null) return;
// CASO 1: SUCCESSO
if (html.contains("Scadenza copertura")) {
// Eseguiamo JS per estrarre la data precisa
var dataEstratta = await webViewController!.evaluateJavascript(source: """
(function() {
// Cerca tutti i 'td' (celle di tabella)
var cells = document.querySelectorAll('td');
for (var i = 0; i < cells.length; i++) {
// Se la cella contiene la label...
if (cells[i].innerText.includes('Scadenza copertura')) {
// ...prendi il testo della cella successiva (dove c'è la data)
var nextCell = cells[i].nextElementSibling;
return nextCell ? nextCell.innerText : null;
}
}
return null;
})();
""");
if (dataEstratta != null && dataEstratta.toString().trim().isNotEmpty) {
setState(() {
dataScadenzaTrovata = dataEstratta.toString().trim();
});
}
}
// CASO 2: FALLIMENTO (Non assicurata o targa errata)
else if (html.contains("dal controllo non risulta coperto") || html.contains("Targa non trovata")) {
setState(() {
ricercaFallita = true;
});
}
// CASO 3: ANCORA NEL FORM (Es. captcha errato)
else {
// Riapplichiamo lo stile grafico perché il reload della pagina potrebbe averlo resettato
_preparaPagina();
}
}
}
=== FILE: lib/comp_10.dart ===
// Versione: FINAL - FIX CONTATORE VISIBILE
import 'package:flutter/material.dart';
import 'global_data.dart';
import 'comp_12.dart';
class Comp10Screen extends StatefulWidget {
const Comp10Screen({super.key});
@override
_Comp10ScreenState createState() => _Comp10ScreenState();
}
class _Comp10ScreenState extends State<Comp10Screen> {
// Set per gestire selezioni multiple uniche
Set<String> _selectedSectors = {};
final double _canvasWidth = 300.0;
final double _canvasHeight = 220.0;
late TextEditingController _controllerDanni;
late TextEditingController _controllerOsservazioni;
final bool isB = GlobalData.latoCorrente == 'B';
late Map<String, Rect> _hitboxes;
@override
void initState() {
super.initState();
_selectedSectors = isB
? Set.from(GlobalData.puntiUrtoB_List)
: Set.from(GlobalData.puntiUrtoA_List);
_controllerDanni = TextEditingController(text: isB ? GlobalData.danni_visibili_B : GlobalData.danni_visibili_A);
_controllerOsservazioni = TextEditingController(text: isB ? GlobalData.osservazioni_B : GlobalData.osservazioni_A);
_hitboxes = isB ? _getHitboxesB() : _getHitboxesA();
// MOSTRA IL POPUP INFORMATIVO ANIMATO AL CARICAMENTO
WidgetsBinding.instance.addPostFrameCallback((_) {
_mostraInfoPopup(context);
});
}
// --- POPUP INFORMATIVO ---
void _mostraInfoPopup(BuildContext context) {
Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900;
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierLabel: "Popup",
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 400),
pageBuilder: (context, animation, secondaryAnimation) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
children: [
Icon(Icons.minor_crash, color: activeColor, size: 28),
const SizedBox(width: 10),
const Expanded(child: Text("Danni e Osservazioni", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("In questa sezione indicherai i danni subiti dal Veicolo ${GlobalData.latoCorrente}.", style: const TextStyle(fontSize: 15)),
const SizedBox(height: 16),
_buildPopupRow(Icons.touch_app, "Punto d'urto", "Tocca direttamente sull'immagine per inserire una 'X' nel punto d'urto. Puoi selezionare più punti. Tocca di nuovo per rimuovere la 'X'."),
const SizedBox(height: 12),
_buildPopupRow(Icons.edit_document, "Danni Visibili", "Descrivi brevemente i danni visibili (es. 'Paraurti rotto')."),
const SizedBox(height: 12),
_buildPopupRow(Icons.comment, "Osservazioni", "Aggiungi eventuali commenti personali sulla dinamica (opzionale)."),
],
),
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
foregroundColor: isB ? Colors.black87 : Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
onPressed: () => Navigator.pop(context),
child: const Text("HO CAPITO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
var curvePosizione = CurvedAnimation(
parent: animation,
curve: Curves.easeOutBack,
reverseCurve: Curves.easeInBack,
);
var curveOpacita = CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.4),
end: Offset.zero,
).animate(curvePosizione),
child: FadeTransition(
opacity: curveOpacita,
child: child,
),
);
},
);
}
Widget _buildPopupRow(IconData icon, String title, String desc) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 24, color: Colors.blueGrey),
const SizedBox(width: 12),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
children: [
TextSpan(text: "$title: ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: desc),
],
),
),
),
],
);
}
// --- MAPPATURA LATO A (BLU) ---
Map<String, Rect> _getHitboxesA() {
return {
'9': const Rect.fromLTWH(35, 85, 40, 40),
'10': const Rect.fromLTWH(25, 110, 25, 50),
'11': const Rect.fromLTWH(60, 110, 25, 50),
'12': const Rect.fromLTWH(35, 175, 40, 40),
'1': const Rect.fromLTWH(90, 50, 35, 40),
'8': const Rect.fromLTWH(127, 50, 35, 40),
'2': const Rect.fromLTWH(87, 90, 30, 60),
'7': const Rect.fromLTWH(138, 90, 30, 60),
'3': const Rect.fromLTWH(80, 150, 35, 40),
'6': const Rect.fromLTWH(130, 150, 30, 40),
'4': const Rect.fromLTWH(100, 190, 25, 30),
'5': const Rect.fromLTWH(135, 190, 25, 30),
'13': const Rect.fromLTWH(180, 35, 30, 40),
'14': const Rect.fromLTWH(210, 35, 30, 40),
'15': const Rect.fromLTWH(240, 35, 30, 40),
'16': const Rect.fromLTWH(172, 75, 30, 70),
'17': const Rect.fromLTWH(240, 75, 30, 70),
'19': const Rect.fromLTWH(165, 145, 40, 60),
'18': const Rect.fromLTWH(238, 145, 40, 60),
'20': const Rect.fromLTWH(200, 195, 50, 25),
};
}
// --- MAPPATURA LATO B (GIALLO) ---
Map<String, Rect> _getHitboxesB() {
return {
'M9': const Rect.fromLTWH(35, 78, 40, 40),
'M10': const Rect.fromLTWH(25, 110, 25, 50),
'M11': const Rect.fromLTWH(60, 110, 25, 50),
'M12': const Rect.fromLTWH(35, 175, 40, 40),
'21': const Rect.fromLTWH(88, 50, 35, 40),
'22': const Rect.fromLTWH(130, 50, 35, 40),
'23': const Rect.fromLTWH(85, 90, 30, 60),
'24': const Rect.fromLTWH(140, 90, 30, 60),
'25': const Rect.fromLTWH(95, 190, 25, 30),
'26': const Rect.fromLTWH(138, 190, 25, 30),
'27': const Rect.fromLTWH(180, 35, 30, 40),
'34': const Rect.fromLTWH(210, 35, 30, 40),
'28': const Rect.fromLTWH(240, 35, 30, 40),
'29': const Rect.fromLTWH(170, 75, 30, 90),
'30': const Rect.fromLTWH(245, 75, 30, 90),
'31': const Rect.fromLTWH(168, 171, 40, 60),
'33': const Rect.fromLTWH(208, 178, 31, 60),
'32': const Rect.fromLTWH(239, 171, 40, 60),
};
}
void _handleTap(TapUpDetails details) {
Offset touchPosition = details.localPosition;
String? foundSector;
_hitboxes.forEach((key, rect) {
if (rect.contains(touchPosition)) {
foundSector = key;
}
});
if (foundSector != null) {
setState(() {
if (_selectedSectors.contains(foundSector)) {
_selectedSectors.remove(foundSector);
} else {
_selectedSectors.add(foundSector!);
}
});
}
}
void _salvaEProsegui() {
setState(() {
if (isB) {
GlobalData.puntiUrtoB_List = _selectedSectors.toList();
GlobalData.danni_visibili_B = _controllerDanni.text.toUpperCase();
GlobalData.osservazioni_B = _controllerOsservazioni.text.toUpperCase();
} else {
GlobalData.puntiUrtoA_List = _selectedSectors.toList();
GlobalData.danni_visibili_A = _controllerDanni.text.toUpperCase();
GlobalData.osservazioni_A = _controllerOsservazioni.text.toUpperCase();
}
});
Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp12Screen()));
}
@override
Widget build(BuildContext context) {
Color mainCol = isB ? Colors.amber.shade700 : Colors.blue.shade900;
Color bgCol = isB ? const Color(0xFFFFF9C4) : const Color(0xFFE3F2FD);
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: bgCol,
appBar: AppBar(
title: Text("10, 11, 14. Dettagli (${GlobalData.latoCorrente})"),
backgroundColor: mainCol,
foregroundColor: isB ? Colors.black : Colors.white,
),
// --- SLIVER LAYOUT ---
body: SafeArea(
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
_buildTitle("10. PUNTO D'URTO INIZIALE (Seleziona anche più di uno)", mainCol),
const SizedBox(height: 10),
Center(
child: Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
child: GestureDetector(
onTapUp: _handleTap,
child: Container(
width: _canvasWidth,
height: _canvasHeight,
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15)),
child: Stack(
children: [
Center(child: Image.asset(
isB ? 'assets/punti_danni_B.png' : 'assets/punti_danni_A.png',
width: _canvasWidth, height: _canvasHeight, fit: BoxFit.contain,
)),
..._selectedSectors.map((sectorId) {
if (_hitboxes.containsKey(sectorId)) {
return CustomPaint(
painter: XPainter(_hitboxes[sectorId]!),
size: Size(_canvasWidth, _canvasHeight),
);
}
return Container();
}),
],
),
),
),
),
),
if (_selectedSectors.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text("Punti: ${_selectedSectors.join(', ')}",
style: TextStyle(color: mainCol, fontWeight: FontWeight.bold)),
),
const Divider(height: 30),
_buildTitle("11. DANNI VISIBILI", mainCol),
TextField(
controller: _controllerDanni,
// Max 45 caratteri per stare nelle 2 righe del PDF (20+25)
maxLength: 45,
maxLines: 2,
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.characters,
decoration: _inputDeco("Es: Paraurti ant, Faro sx..."),
),
const SizedBox(height: 10),
_buildTitle("14. OSSERVAZIONI", mainCol),
TextField(
controller: _controllerOsservazioni,
maxLength: 55,
maxLines: 2,
keyboardType: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
decoration: _inputDeco("Es: Ho ragione io perché..."),
),
]),
),
),
// --- STICKY FOOTER ---
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Container(
padding: const EdgeInsets.all(16),
child: _navButtons(context, mainCol),
),
),
),
],
),
),
),
);
}
Widget _buildTitle(String text, Color color) {
return Align(alignment: Alignment.centerLeft, child: Padding(
padding: const EdgeInsets.only(bottom: 8.0, top: 10.0),
child: Text(text, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: color)),
));
}
InputDecoration _inputDeco(String hint) {
return InputDecoration(
hintText: hint,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
contentPadding: const EdgeInsets.all(15),
// counterText: "", <-- RIMOSSO: Ora il contatore si vede!
);
}
Widget _navButtons(BuildContext context, Color color) {
return Row(
children: [
Expanded(
flex: 4,
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, 55),
padding: const EdgeInsets.symmetric(horizontal: 5),
),
child: const FittedBox(
child: Text("INDIETRO", style: TextStyle(fontWeight: FontWeight.bold))
)
)
),
const SizedBox(width: 15),
Expanded(
flex: 6,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
foregroundColor: isB ? Colors.black : Colors.white,
minimumSize: const Size(0, 55)
),
onPressed: _salvaEProsegui,
child: const FittedBox(
child: Text("SALVA E PROSEGUI", style: TextStyle(fontWeight: FontWeight.bold))
)
)
),
],
);
}
}
class XPainter extends CustomPainter {
final Rect targetRect;
XPainter(this.targetRect);
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()..color = Colors.red..strokeWidth = 3.0..strokeCap = StrokeCap.round..style = PaintingStyle.stroke;
double cx = targetRect.left + targetRect.width / 2;
double cy = targetRect.top + targetRect.height / 2;
double iconSize = 10.0;
canvas.drawLine(Offset(cx - iconSize, cy - iconSize), Offset(cx + iconSize, cy + iconSize), paint);
canvas.drawLine(Offset(cx + iconSize, cy - iconSize), Offset(cx - iconSize, cy + iconSize), paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
=== FILE: lib/comp_8.dart ===
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'comp_9.dart';
import 'global_data.dart';
class DateInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
var text = newValue.text.replaceAll('/', '');
if (text.length > 8) text = text.substring(0, 8);
var result = "";
for (int i = 0; i < text.length; i++) {
if (i == 2 || i == 4) result += "/";
result += text[i];
}
return newValue.copyWith(
text: result,
selection: TextSelection.collapsed(offset: result.length)
);
}
}
class Comp8Screen extends StatefulWidget {
const Comp8Screen({super.key});
@override
_Comp8ScreenState createState() => _Comp8ScreenState();
}
class _Comp8ScreenState extends State<Comp8Screen> {
late TextEditingController _polizza, _cartaVerde, _validoDal, _validoAl;
late TextEditingController _agenzia, _denomAgenzia, _indirizzoAgenzia, _statoAgenzia, _telAgenzia;
late TextEditingController _compagniaManuale;
String? _selectedAssicurazione;
bool _isManuale = false;
late bool _danniMaterialiAssicurati;
bool get isB => GlobalData.latoCorrente == 'B';
String? _erroreValidoDal;
String? _erroreValidoAl;
@override
void initState() {
super.initState();
_initControllers();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_validoDal.text.isNotEmpty) _validaDataDal(_validoDal.text);
if (_validoAl.text.isNotEmpty) _validaDataAl(_validoAl.text);
// MOSTRA IL POPUP ANIMATO ALL'AVVIO
_mostraInfoPopup(context);
});
}
void _initControllers() {
String denominazioneSalvata = isB ? GlobalData.Denominazione_B : GlobalData.Denominazione_A;
_polizza = TextEditingController(text: isB ? GlobalData.Numero_Polizza_B : GlobalData.Numero_Polizza_A);
_cartaVerde = TextEditingController(text: isB ? GlobalData.N_carta_verde_B : GlobalData.N_carta_verde_A);
_validoDal = TextEditingController(text: isB ? GlobalData.Data_Inizio_Dal_B : GlobalData.Data_Inizio_Dal_A);
_validoAl = TextEditingController(text: isB ? GlobalData.Data_Scadenza_Al_B : GlobalData.Data_Scadenza_Al_A);
_agenzia = TextEditingController(text: isB ? GlobalData.Agenzia_B : GlobalData.Agenzia_A);
_denomAgenzia = TextEditingController(text: isB ? GlobalData.Denominazione_agenzia_B : GlobalData.Denominazione_agenzia_A);
_indirizzoAgenzia = TextEditingController(text: isB ? GlobalData.Indirizzo_agenzia_B : GlobalData.Indirizzo_agenzia_A);
_statoAgenzia = TextEditingController(text: (isB ? GlobalData.Stato_agenzia_B : GlobalData.Stato_agenzia_A).isEmpty ? "ITALIA" : (isB ? GlobalData.Stato_agenzia_B : GlobalData.Stato_agenzia_A));
_telAgenzia = TextEditingController(text: isB ? GlobalData.N_tel_mail_agenzia_B : GlobalData.N_tel_mail_agenzia_A);
_danniMaterialiAssicurati = isB ? GlobalData.FLAG_danni_mat_assicurati_B : GlobalData.FLAG_danni_mat_assicurati_A;
_compagniaManuale = TextEditingController();
if (denominazioneSalvata.isNotEmpty) {
if (GlobalData.assicurazioni.containsKey(denominazioneSalvata)) {
_selectedAssicurazione = denominazioneSalvata;
_isManuale = (denominazioneSalvata == "ALTRO (Inserimento manuale)");
} else {
_selectedAssicurazione = "ALTRO (Inserimento manuale)";
_compagniaManuale.text = denominazioneSalvata;
_isManuale = true;
}
} else {
_selectedAssicurazione = null;
_isManuale = false;
}
}
// --- POPUP INFORMATIVO ANIMATO ---
void _mostraInfoPopup(BuildContext context) {
Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900;
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierLabel: "Popup",
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 400),
pageBuilder: (context, animation, secondaryAnimation) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
children: [
Icon(Icons.business, color: activeColor, size: 28),
const SizedBox(width: 10),
const Expanded(child: Text("Compagnia di Assicurazione", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("Recupera i dati assicurativi per il Veicolo ${GlobalData.latoCorrente}.", style: const TextStyle(fontSize: 15)),
const SizedBox(height: 16),
_buildPopupRow(Icons.description, "Polizza", "Il numero di polizza e le date di validità sono obbligatorie."),
const SizedBox(height: 12),
_buildPopupRow(Icons.calendar_today, "Date", "Usa l'icona del calendario per inserire rapidamente la data o scrivila manualmente nel formato GG/MM/AAAA."),
const SizedBox(height: 12),
_buildPopupRow(Icons.store, "Agenzia", "I dati dell'Agenzia sono facoltativi ma aiutano a velocizzare la pratica."),
],
),
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
foregroundColor: isB ? Colors.black87 : Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
onPressed: () => Navigator.pop(context),
child: const Text("HO CAPITO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
var curvePosizione = CurvedAnimation(
parent: animation,
curve: Curves.easeOutBack,
reverseCurve: Curves.easeInBack,
);
var curveOpacita = CurvedAnimation(
parent: animation,
curve: Curves.easeOut,
reverseCurve: Curves.easeIn,
);
return SlideTransition(
position: Tween<Offset>(
begin: const Offset(0.0, 0.4),
end: Offset.zero,
).animate(curvePosizione),
child: FadeTransition(
opacity: curveOpacita,
child: child,
),
);
},
);
}
Widget _buildPopupRow(IconData icon, String title, String desc) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 24, color: Colors.blueGrey),
const SizedBox(width: 12),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
children: [
TextSpan(text: "$title: ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: desc),
],
),
),
),
],
);
}
String? _calcolaErroreData(String data) {
if (data.isEmpty) return null; // Se vuoto non diamo errore immediato (ci pensa il tasto Salva)
if (data.length < 10) return "Formato: GG/MM/AAAA"; // Avvisa subito se mancano cifre
List<String> parti = data.split('/');
if (parti.length != 3) return "Formato errato";
int? giorno = int.tryParse(parti[0]);
int? mese = int.tryParse(parti[1]);
int? anno = int.tryParse(parti[2]);
if (giorno == null || mese == null || anno == null) return "Data non valida";
if (mese < 1 || mese > 12) return "Mese errato";
int giorniMax = _giorniInMese(mese, anno);
if (giorno < 1 || giorno > giorniMax) return "Giorno errato per questo mese";
if (anno < 1900 || anno > 2100) return "Anno non valido";
return null; // La data è perfetta
}
Future<void> _selezionaData(BuildContext context, TextEditingController controller, Function(String) onChanged) async {
DateTime initialDate = DateTime.now();
if (controller.text.length == 10) {
try {
List<String> parti = controller.text.split('/');
initialDate = DateTime(int.parse(parti[2]), int.parse(parti[1]), int.parse(parti[0]));
} catch (_) {}
}
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime(1990),
lastDate: DateTime(2100),
locale: const Locale('it', 'IT'),
);
if (picked != null) {
String dataFormattata = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
setState(() {
controller.text = dataFormattata;
onChanged(dataFormattata);
});
}
}
void _validaDataDal(String val) => setState(() => _erroreValidoDal = _calcolaErroreData(val));
void _validaDataAl(String val) => setState(() => _erroreValidoAl = _calcolaErroreData(val));
int _giorniInMese(int mese, int anno) {
if (mese == 2) {
bool bisestile = (anno % 4 == 0 && anno % 100 != 0) || (anno % 400 == 0);
return bisestile ? 29 : 28;
}
if ([4, 6, 9, 11].contains(mese)) return 30;
return 31;
}
// ---------------------------------
void _salvaTutto() {
bool nomeCompagniaMancante = (_selectedAssicurazione == null ||
(_selectedAssicurazione == "ALTRO (Inserimento manuale)" && _compagniaManuale.text.trim().isEmpty));
// MODIFICA: Rimossi i controlli sui campi Agenzia, Denominazione, Indirizzo, Stato e Telefono
if (nomeCompagniaMancante || _polizza.text.trim().isEmpty || _validoDal.text.trim().isEmpty ||
_validoAl.text.trim().isEmpty) {
// MODIFICA: Testo aggiornato
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Compagnia, Polizza e Date sono obbligatorie"), backgroundColor: Colors.red));
return;
}
if (_erroreValidoDal != null || _erroreValidoAl != null) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Correggi le date prima di proseguire!"), backgroundColor: Colors.red));
return;
}
if ((_validoDal.text.length < 10) || (_validoAl.text.length < 10)) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Le date devono essere complete (GG/MM/AAAA)"), backgroundColor: Colors.orange));
return;
}
String denomFinale = (_selectedAssicurazione == "ALTRO (Inserimento manuale)") ? _compagniaManuale.text.toUpperCase() : (_selectedAssicurazione ?? "");
if (isB) {
GlobalData.Denominazione_B = denomFinale;
GlobalData.Numero_Polizza_B = _polizza.text.toUpperCase();
GlobalData.N_carta_verde_B = _cartaVerde.text.toUpperCase();
GlobalData.Data_Inizio_Dal_B = _validoDal.text;
GlobalData.Data_Scadenza_Al_B = _validoAl.text;
GlobalData.Agenzia_B = _agenzia.text.toUpperCase();
GlobalData.Denominazione_agenzia_B = _denomAgenzia.text.toUpperCase();
GlobalData.Indirizzo_agenzia_B = _indirizzoAgenzia.text.toUpperCase();
GlobalData.Stato_agenzia_B = _statoAgenzia.text.toUpperCase();
GlobalData.N_tel_mail_agenzia_B = _telAgenzia.text.toUpperCase();
GlobalData.FLAG_danni_mat_assicurati_B = _danniMaterialiAssicurati;
} else {
GlobalData.Denominazione_A = denomFinale;
GlobalData.Numero_Polizza_A = _polizza.text.toUpperCase();
GlobalData.N_carta_verde_A = _cartaVerde.text.toUpperCase();
GlobalData.Data_Inizio_Dal_A = _validoDal.text;
GlobalData.Data_Scadenza_Al_A = _validoAl.text;
GlobalData.Agenzia_A = _agenzia.text.toUpperCase();
GlobalData.Denominazione_agenzia_A = _denomAgenzia.text.toUpperCase();
GlobalData.Indirizzo_agenzia_A = _indirizzoAgenzia.text.toUpperCase();
GlobalData.Stato_agenzia_A = _statoAgenzia.text.toUpperCase();
GlobalData.N_tel_mail_agenzia_A = _telAgenzia.text.toUpperCase();
GlobalData.FLAG_danni_mat_assicurati_A = _danniMaterialiAssicurati;
}
Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp9Screen()));
}
@override
Widget build(BuildContext context) {
Color mainCol = isB ? Colors.amber.shade700 : Colors.blue.shade900;
Color bgCol = isB ? const Color(0xFFFFF9C4) : const Color(0xFFE3F2FD);
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: bgCol,
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text('8. Assicurazione (${GlobalData.latoCorrente})'),
backgroundColor: mainCol,
foregroundColor: isB ? Colors.black : Colors.white,
),
// --- LAYOUT SLIVER ---
body: SafeArea(
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
// 1. Contenuto Scrollabile
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: SliverList(
delegate: SliverChildListDelegate([
_buildCard(
titolo: "8. IMPRESA DI ASSICURAZIONE",
accentColor: mainCol,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: DropdownButtonFormField<String>(
value: _selectedAssicurazione,
isExpanded: true,
decoration: InputDecoration(
labelText: "Seleziona Compagnia *",
prefixIcon: Icon(Icons.business, color: isB ? Colors.orange.shade800 : Colors.blue.shade700),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
filled: true, fillColor: Colors.white,
),
items: GlobalData.assicurazioni.keys.map((String key) {
return DropdownMenuItem<String>(value: key, child: Text(key, style: const TextStyle(fontSize: 13)));
}).toList(),
onChanged: (val) {
setState(() {
_selectedAssicurazione = val;
_isManuale = (val == "ALTRO (Inserimento manuale)");
if (!_isManuale) _compagniaManuale.clear();
});
},
),
),
if (_isManuale)
_buildField(_compagniaManuale, "Inserisci Nome Compagnia *", Icons.edit_note, mainCol),
_buildField(_polizza, "N. di polizza *", Icons.description, mainCol),
_buildField(_cartaVerde, "N. di Carta Verde (Opz)", Icons.language, mainCol),
Row(children: [
Expanded(
child: _buildField(_validoDal, "Valida dal *", Icons.date_range, mainCol,
isDate: true,
errorText: _erroreValidoDal,
onChanged: _validaDataDal,
// USIAMO INKWELL PER STRINGERE I MARGINI
customPrefix: InkWell(
onTap: () => _selezionaData(context, _validoDal, _validaDataDal),
borderRadius: BorderRadius.circular(20),
child: Container(
width: 38,
alignment: Alignment.center,
child: Icon(Icons.calendar_today, size: 20, color: isB ? Colors.orange.shade800 : Colors.blue.shade700),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: _buildField(_validoAl, "Fino al *", Icons.event_available, mainCol,
isDate: true,
errorText: _erroreValidoAl,
onChanged: _validaDataAl,
// USIAMO INKWELL PER STRINGERE I MARGINI
customPrefix: InkWell(
onTap: () => _selezionaData(context, _validoAl, _validaDataAl),
borderRadius: BorderRadius.circular(20),
child: Container(
width: 38,
alignment: Alignment.center,
child: Icon(Icons.calendar_today, size: 20, color: isB ? Colors.orange.shade800 : Colors.blue.shade700),
),
),
),
),
]),
],
),
),
_buildCard(
titolo: "AGENZIA (OPZIONALE)", // Modificato titolo per chiarezza
accentColor: mainCol,
child: Column(
children: [
// MODIFICA: Rimossi asterischi dalle label
_buildField(_agenzia, "Ufficio/Agenzia", Icons.store, mainCol),
_buildField(_denomAgenzia, "Denominazione", Icons.info_outline, mainCol),
_buildField(_indirizzoAgenzia, "Indirizzo", Icons.place, mainCol),
Row(children: [
Expanded(child: _buildField(_statoAgenzia, "Stato", Icons.flag, mainCol)),
const SizedBox(width: 10),
Expanded(child: _buildField(_telAgenzia, "Tel. / E-mail", Icons.contact_phone, mainCol)),
]),
],
),
),
_buildCard(
titolo: "DANNI PROPRI",
accentColor: mainCol,
child: _buildSwitch(
"La polizza copre anche i danni materiali al proprio veicolo?",
_danniMaterialiAssicurati,
(val) => setState(() => _danniMaterialiAssicurati = val),
mainCol
),
),
]),
),
),
// 2. Piè di pagina elastico
SliverFillRemaining(
hasScrollBody: false,
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(16),
child: _navButtons(context, mainCol),
),
),
),
],
),
),
),
);
}
// --- NUOVO WIDGET SWITCH PERSONALIZZATO (NO / SÌ) ---
Widget _buildSwitch(String label, bool value, Function(bool) onChanged, Color activeColor) {
return InkWell(
onTap: () => onChanged(!value),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Expanded(
child: Text(
label,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
),
),
const SizedBox(width: 8),
Text(
"NO",
style: TextStyle(
fontWeight: !value ? FontWeight.bold : FontWeight.normal,
color: !value ? Colors.red.shade700 : Colors.grey.shade400,
fontSize: 14,
),
),
Switch(
value: value,
onChanged: onChanged,
activeColor: activeColor,
activeTrackColor: activeColor.withOpacity(0.4),
inactiveThumbColor: Colors.grey,
inactiveTrackColor: Colors.grey.shade300,
),
Text(
"SÌ",
style: TextStyle(
fontWeight: value ? FontWeight.bold : FontWeight.normal,
color: value ? activeColor : Colors.grey.shade400,
fontSize: 14,
),
),
],
),
),
);
}
Widget _buildField(TextEditingController controller, String label, IconData icon, Color iconColor, {bool isDate = false, String? errorText, Function(String)? onChanged, Widget? customPrefix}) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: TextField(
controller: controller,
keyboardType: isDate ? TextInputType.number : TextInputType.text,
inputFormatters: isDate ? [FilteringTextInputFormatter.digitsOnly, DateInputFormatter()] : [],
textCapitalization: TextCapitalization.characters,
onChanged: onChanged,
// Rimpiccioliamo leggermente il testo se è una data per farlo stare più comodo
style: TextStyle(fontSize: isDate ? 14 : 16),
decoration: InputDecoration(
labelText: label,
hintText: isDate ? "GG/MM/AAAA" : null,
errorText: errorText,
errorStyle: const TextStyle(fontSize: 10), // Rende il testo di errore più compatto
prefixIcon: customPrefix ?? Icon(icon, size: 20, color: isB ? Colors.orange.shade800 : Colors.blue.shade700),
// --- IL TRUCCO È QUI: Diciamo all'icona di occupare meno spazio ---
prefixIconConstraints: const BoxConstraints(minWidth: 38, minHeight: 38),
contentPadding: const EdgeInsets.only(left: 0, right: 8, top: 15, bottom: 15),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
filled: true, fillColor: Colors.white,
),
),
);
}
Widget _buildCard({required String titolo, required Widget child, required Color accentColor}) {
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(titolo, style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold, color: accentColor)),
const Divider(height: 20),
child,
]),
),
);
}
Widget _navButtons(BuildContext context, Color color) {
return Row(
children: [
Expanded(child: OutlinedButton(onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom(minimumSize: const Size(0, 55)), child: const Text("INDIETRO"))),
const SizedBox(width: 15),
Expanded(flex: 2, child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: color, foregroundColor: isB ? Colors.black : Colors.white, minimumSize: const Size(0, 55)),
onPressed: _salvaTutto,
child: const Text("SALVA E PROSEGUI"),
)),
],
);
}
@override
void dispose() {
_polizza.dispose(); _cartaVerde.dispose();
_validoDal.dispose(); _validoAl.dispose(); _agenzia.dispose();
_denomAgenzia.dispose(); _indirizzoAgenzia.dispose();
_statoAgenzia.dispose(); _telAgenzia.dispose();
_compagniaManuale.dispose();
super.dispose();
}
}
=== FILE: lib/ps.dart ===
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // Fondamentale per leggere il JSON
import 'package:geolocator/geolocator.dart';
import 'package:url_launcher/url_launcher.dart';
class ProntoSoccorsoScreen extends StatefulWidget {
const ProntoSoccorsoScreen({super.key});
@override
State<ProntoSoccorsoScreen> createState() => _ProntoSoccorsoScreenState();
}
class _ProntoSoccorsoScreenState extends State<ProntoSoccorsoScreen> {
List<dynamic> _ospedali = [];
bool _isLoading = true;
String _statusMessage = "Caricamento archivio ospedali...";
Position? _posizioneUtente;
@override
void initState() {
super.initState();
_inizializzaDati();
}
Future<void> _inizializzaDati() async {
try {
// 1. CARICA IL FILE JSON LOCALE
// Assicurati che il nome del file in assets sia esattamente questo
final String jsonString = await rootBundle.loadString('assets/ospedali_completo.json');
List<dynamic> datiGrezzi = json.decode(jsonString);
setState(() => _statusMessage = "Ricerca posizione GPS...");
// 2. CHIEDI PERMESSI E TROVA POSIZIONE
Position? posizione = await _determinaPosizione();
if (posizione != null) {
_posizioneUtente = posizione;
// 3. CALCOLA DISTANZA PER OGNI OSPEDALE
for (var ospedale in datiGrezzi) {
// Gestione sicura dei numeri (a volte arrivano come stringhe o int)
double lat = (ospedale['lat'] is String) ? double.parse(ospedale['lat']) : ospedale['lat'].toDouble();
double lng = (ospedale['lng'] is String) ? double.parse(ospedale['lng']) : ospedale['lng'].toDouble();
double distanzaInMetri = Geolocator.distanceBetween(
posizione.latitude,
posizione.longitude,
lat,
lng,
);
ospedale['distanza'] = distanzaInMetri;
ospedale['lat_num'] = lat; // Salviamo il double pulito per dopo
ospedale['lng_num'] = lng;
}
// 4. ORDINA DAL PIÙ VICINO
datiGrezzi.sort((a, b) => (a['distanza'] as double).compareTo(b['distanza'] as double));
// (Opzionale) Prendi solo i primi 50 per non appesantire la lista, o tienili tutti
// datiGrezzi = datiGrezzi.take(50).toList();
}
if (mounted) {
setState(() {
_ospedali = datiGrezzi;
_isLoading = false;
});
}
} catch (e) {
debugPrint("Errore: $e");
if (mounted) {
setState(() {
_statusMessage = "Errore: Impossibile caricare gli ospedali.\n$e";
_isLoading = false;
});
}
}
}
Future<Position?> _determinaPosizione() async {
bool serviceEnabled;
LocationPermission permission;
serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return null;
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) return null;
}
if (permission == LocationPermission.deniedForever) return null;
return await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
}
Future<void> _apriMappa(double lat, double lng) async {
// URL universale che funziona su Android e iOS aprendo l'app di mappe predefinita
final Uri url = Uri.parse("http://maps.google.com/maps?q=$lat,$lng");
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
throw Exception('Could not launch $url');
}
}
Future<void> _chiama112() async {
final Uri url = Uri.parse("tel:112");
if (await canLaunchUrl(url)) await launchUrl(url);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Pronto Soccorso Vicini"),
backgroundColor: Colors.green.shade800,
foregroundColor: Colors.white,
),
body: Column(
children: [
// HEADER EMERGENZA
Container(
padding: const EdgeInsets.all(16),
color: Colors.red.shade50,
child: Row(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.red.shade800, size: 30),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("EMERGENZA GRAVE?", style: TextStyle(color: Colors.red.shade900, fontWeight: FontWeight.bold)),
const Text("Non andare da solo, chiama il 112."),
],
),
),
ElevatedButton(
onPressed: _chiama112,
style: ElevatedButton.styleFrom(backgroundColor: Colors.red, foregroundColor: Colors.white),
child: const Text("CHIAMA 112"),
)
],
),
),
Expanded(
child: _isLoading
? Center(child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: Colors.green.shade800),
const SizedBox(height: 20),
Text(_statusMessage, textAlign: TextAlign.center),
],
))
: _ospedali.isEmpty
? const Center(child: Text("Nessun ospedale trovato nel database."))
: ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: _ospedali.length,
itemBuilder: (context, index) {
final ospedale = _ospedali[index];
final double km = (ospedale['distanza'] ?? 0) / 1000;
final String livello = ospedale['livello'] ?? "";
// Colore badge livello
Color badgeColor = Colors.grey;
if (livello.contains("DEA2")) badgeColor = Colors.red.shade700;
else if (livello.contains("DEA1")) badgeColor = Colors.orange.shade700;
else if (livello.contains("PS")) badgeColor = Colors.green.shade700;
return Card(
elevation: 3,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.green.shade100)
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.place, color: Colors.green, size: 20),
Text("${km.toStringAsFixed(1)} km", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 11)),
],
),
),
title: Text(
ospedale['nome'],
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text("${ospedale['indirizzo']}, ${ospedale['citta']}", style: TextStyle(color: Colors.grey.shade700, fontSize: 13)),
const SizedBox(height: 6),
if (livello != "nan" && livello.isNotEmpty)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: badgeColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: badgeColor.withOpacity(0.3))
),
child: Text(
"Livello: $livello",
style: TextStyle(color: badgeColor, fontSize: 11, fontWeight: FontWeight.bold),
),
)
],
),
trailing: const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
onTap: () {
if (ospedale['lat_num'] != null && ospedale['lng_num'] != null) {
_apriMappa(ospedale['lat_num'], ospedale['lng_num']);
}
},
),
);
},
),
),
],
),
);
}
}
=== FILE: lib/scambio_dati_screen.dart ===
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'global_data.dart';
import 'cid_data_manager.dart';
import 'security_service.dart';
class ScambioDatiScreen extends StatefulWidget {
const ScambioDatiScreen({super.key});
@override
State<ScambioDatiScreen> createState() => _ScambioDatiScreenState();
}
class _ScambioDatiScreenState extends State<ScambioDatiScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
final MobileScannerController _cameraController = MobileScannerController();
final TextEditingController _manualCodeController = TextEditingController();
String? _codiceSessione;
String? _chiaveSegreta;
String? _shortCode; // IL PIN DI 6 LETTERE
StreamSubscription? _streamSubscription;
bool _isLoading = false;
bool _scambioConcluso = false;
@override
void initState() {
super.initState();
// LOGICA DI APERTURA INTELLIGENTE:
// Lato A -> Inizia con la Fotocamera (Indice 1)
// Lato B -> Inizia mostrando il QR (Indice 0)
int initialIndex = (GlobalData.latoCorrente == 'A') ? 1 : 0;
_tabController = TabController(length: 2, vsync: this, initialIndex: initialIndex);
// Avvio automatico Host (prepara il QR in background a prescindere dalla Tab aperta)
_avviaHost();
}
@override
void dispose() {
// Cancello la sessione se esco senza concludere, a meno che non sia l'ID della firma
if (!_scambioConcluso && _codiceSessione != null && _codiceSessione != GlobalData.idSessione) {
FirebaseFirestore.instance.collection('scambi_cid').doc(_codiceSessione).delete();
}
_streamSubscription?.cancel();
_tabController.dispose();
_cameraController.dispose();
_manualCodeController.dispose();
super.dispose();
}
// --- GENERATORE DI PIN A 6 LETTERE MAIUSCOLE ---
String _generaShortCode() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
Random rnd = Random();
return String.fromCharCodes(Iterable.generate(6, (_) => chars.codeUnitAt(rnd.nextInt(chars.length))));
}
// --- 1. LOGICA HOST (CHI MOSTRA IL QR) ---
Future<void> _avviaHost() async {
if (_codiceSessione != null) return;
if ((GlobalData.latoCorrente == 'A' && GlobalData.puntiFirmaA.isEmpty) ||
(GlobalData.latoCorrente == 'B' && GlobalData.puntiFirmaB.isEmpty)) {
return;
}
setState(() => _isLoading = true);
try {
String sessionId = GlobalData.idSessione ?? "CID_${DateTime.now().millisecondsSinceEpoch}";
String shortCode = _generaShortCode();
GlobalData.idSessione = sessionId;
GlobalData.idScambioTemporaneo = sessionId;
String chiave = GlobalData.chiaveSegretaCorrente ?? SecurityService.generaChiaveSessione();
GlobalData.chiaveSegretaCorrente = chiave;
Map<String, dynamic> mieiDati = CidDataManager.estraiDatiPerExport();
String payloadCriptato = SecurityService.criptaDati(mieiDati, chiave);
await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).set({
"timestamp_scambio": FieldValue.serverTimestamp(),
"host_lato": GlobalData.latoCorrente,
"secure_payload_host": payloadCriptato,
"secure_payload_guest": null,
"status": "waiting_guest",
"short_code": shortCode,
"chiave_temporanea": chiave
}, SetOptions(merge: true));
if (mounted) {
setState(() {
_codiceSessione = sessionId;
_chiaveSegreta = chiave;
_shortCode = shortCode;
_isLoading = false;
});
}
// Ascolto risposta dell'altro telefono
_streamSubscription = FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).snapshots().listen((snapshot) {
if (!snapshot.exists) {
if (mounted) setState(() => _codiceSessione = null);
return;
}
var data = snapshot.data();
if (data != null && data['secure_payload_guest'] != null) {
_streamSubscription?.cancel();
_completaSync(data['secure_payload_guest'], chiave);
}
});
} catch (e) {
debugPrint("Err Host: $e");
if (mounted) setState(() => _isLoading = false);
}
}
void _completaSync(String encryptedData, String chiave) {
try {
Map<String, dynamic> dati = SecurityService.decriptaDati(encryptedData, chiave);
CidDataManager.importaDati(dati);
_scambioConcluso = true;
_showSuccessAndExit();
} catch (e) {
if (mounted) ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Errore decriptazione")));
}
}
// --- 2. LOGICA GUEST: VIA FOTOCAMERA ---
void _onDetect(BarcodeCapture capture) {
if (_isLoading || _scambioConcluso) return;
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
if (barcode.rawValue != null) {
_partecipaGuestScansione(barcode.rawValue!);
break;
}
}
}
Future<void> _partecipaGuestScansione(String qrRawData) async {
if (qrRawData.isEmpty) return;
setState(() => _isLoading = true);
try {
if (!qrRawData.contains('|')) throw Exception("QR non valido");
var parts = qrRawData.split('|');
String sessionId = parts[0];
String chiave = parts[1];
await _eseguiPartecipazione(sessionId, chiave);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore Scansione: $e")));
await Future.delayed(const Duration(seconds: 2));
setState(() => _isLoading = false);
}
}
}
// --- 3. LOGICA GUEST: INSERIMENTO MANUALE ---
Future<void> _partecipaGuestManuale() async {
String codiceInserito = _manualCodeController.text.trim().toUpperCase();
if (codiceInserito.length < 5) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Inserisci un codice valido!"), backgroundColor: Colors.orange));
return;
}
setState(() => _isLoading = true);
try {
// 1. Cerca la sessione tramite codice corto su Firebase
final querySnapshot = await FirebaseFirestore.instance
.collection('scambi_cid')
.where('short_code', isEqualTo: codiceInserito)
.limit(1)
.get();
if (querySnapshot.docs.isEmpty) {
throw Exception("Codice non trovato o scaduto.");
}
final doc = querySnapshot.docs.first;
String sessionId = doc.id;
String chiave = doc['chiave_temporanea'] ?? "CHIAVE_DI_BACKUP";
await _eseguiPartecipazione(sessionId, chiave, manualHostData: doc.data());
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("Errore: $e"), backgroundColor: Colors.red));
setState(() => _isLoading = false);
}
}
}
// --- FUNZIONE CENTRALE PER GUEST (USATA DA FOTOCAMERA E MANUALE) ---
Future<void> _eseguiPartecipazione(String sessionId, String chiave, {Map<String, dynamic>? manualHostData}) async {
if (GlobalData.idSessione != null && GlobalData.idSessione != sessionId) {
debugPrint("🗑️ Cancello vecchio file firma orfano: ${GlobalData.idSessione}");
await FirebaseFirestore.instance.collection('scambi_cid').doc(GlobalData.idSessione).delete();
GlobalData.idSessione = null;
}
Map<String, dynamic> data;
if (manualHostData != null) {
data = manualHostData;
} else {
DocumentSnapshot doc = await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).get();
if (!doc.exists) throw Exception("Sessione scaduta.");
data = doc.data() as Map<String, dynamic>;
}
String? hostDataEnc = data['secure_payload_host'];
if (hostDataEnc == null) throw Exception("Dati host mancanti.");
Map<String, dynamic> datiHost = SecurityService.decriptaDati(hostDataEnc, chiave);
CidDataManager.importaDati(datiHost);
Map<String, dynamic> mieiDati = CidDataManager.estraiDatiPerExport();
String myDataEnc = SecurityService.criptaDati(mieiDati, chiave);
await FirebaseFirestore.instance.collection('scambi_cid').doc(sessionId).update({
"secure_payload_guest": myDataEnc,
"status": "completed"
});
GlobalData.idSessione = sessionId;
GlobalData.idScambioTemporaneo = sessionId;
GlobalData.chiaveSegretaCorrente = chiave;
_scambioConcluso = true;
_showSuccessAndExit();
}
void _showSuccessAndExit() {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("SCAMBIO EFFETTUATO!"), backgroundColor: Colors.green, duration: Duration(seconds: 1))
);
Navigator.pop(context);
}
@override
Widget build(BuildContext context) {
String qrString = "";
if (_codiceSessione != null && _chiaveSegreta != null) {
qrString = "$_codiceSessione|$_chiaveSegreta";
}
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text("Scambio Dati"),
backgroundColor: Colors.blue.shade900,
foregroundColor: Colors.white,
bottom: TabBar(
controller: _tabController,
indicatorColor: Colors.white,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
tabs: const [
Tab(icon: Icon(Icons.qr_code), text: "IL TUO QR"),
Tab(icon: Icon(Icons.camera_alt), text: "SCANSIONA / CODICE")
]
),
),
body: TabBarView(
controller: _tabController,
// Impedisce di strusciare accidentalmente e chiudere lo scanner
physics: const NeverScrollableScrollPhysics(),
children: [
// ================= TAB 1: HOST (MOSTRA QR E PIN) =================
Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_codiceSessione == null) ...[
const CircularProgressIndicator(),
const SizedBox(height: 20),
const Text("Generazione in corso...")
] else ...[
const Text("Fai scansionare questo QR:", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(color: Colors.white, border: Border.all(color: Colors.black12)),
child: QrImageView(data: qrString, size: 240)
),
const SizedBox(height: 30),
const Text("Oppure fai inserire questo PIN:", style: TextStyle(color: Colors.black54)),
const SizedBox(height: 5),
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.orange.shade800, width: 2)
),
child: Text(
_shortCode ?? "---",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 32, letterSpacing: 6, color: Colors.orange.shade900)
),
),
const SizedBox(height: 30),
const CircularProgressIndicator(),
const SizedBox(height: 10),
const Text("In attesa dell'altro utente...", style: TextStyle(color: Colors.grey))
]
]
)
)
),
// ================= TAB 2: GUEST (SCANNER SPLIT SCREEN) =================
Stack(
children: [
Column(
children: [
// METÀ SUPERIORE: FOTOCAMERA
Expanded(
flex: 5,
child: Stack(
children: [
MobileScanner(
controller: _cameraController,
onDetect: _onDetect,
),
// Overlay mirino per far capire all'utente dove inquadrare
Center(
child: Container(
width: 200, height: 200,
decoration: BoxDecoration(
border: Border.all(color: Colors.greenAccent, width: 3),
borderRadius: BorderRadius.circular(20)
),
),
),
Positioned(
bottom: 10,
left: 0,
right: 0,
child: Container(
color: Colors.black54,
padding: const EdgeInsets.symmetric(vertical: 8),
child: const Text(
"Inquadra il QR Code",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
)
)
],
),
),
// DIVISORIO
Container(height: 5, color: Colors.blue.shade900),
// METÀ INFERIORE: INSERIMENTO MANUALE
Expanded(
flex: 5,
child: Container(
color: Colors.grey.shade100,
width: double.infinity,
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.keyboard, size: 40, color: Colors.blueGrey),
const SizedBox(height: 10),
const Text(
"La fotocamera non funziona?\nInserisci qui il PIN a 6 lettere dell'altro utente:",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.black87),
),
const SizedBox(height: 20),
TextField(
controller: _manualCodeController,
textCapitalization: TextCapitalization.characters,
maxLength: 6,
textAlign: TextAlign.center,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24, letterSpacing: 8),
decoration: InputDecoration(
hintText: "PIN",
counterText: "",
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15)),
filled: true,
fillColor: Colors.white,
prefixIcon: const Icon(Icons.password, color: Colors.blueGrey),
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
),
onPressed: _partecipaGuestManuale,
icon: const Icon(Icons.download),
label: const Text("SCARICA DATI", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
),
)
],
),
),
),
)
],
),
// OVERLAY CARICAMENTO
if (_isLoading) const ColoredBox(color: Colors.black54, child: Center(child: CircularProgressIndicator(color: Colors.white))),
],
)
],
),
);
}
}
=== FILE: lib/pdf_inspector.dart ===
import 'dart:io';
import 'package:syncfusion_flutter_pdf/pdf.dart';
void main() {
// 1. Leggi il file PDF (Assicurati del percorso corretto!)
// Se sei nella root del progetto, il percorso dovrebbe essere:
final File file = File('assets/CAI.pdf');
if (!file.existsSync()) {
print('ERRORE: File non trovato in ${file.path}');
return;
}
final List<int> bytes = file.readAsBytesSync();
final PdfDocument document = PdfDocument(inputBytes: bytes);
final PdfForm form = document.form;
print('\n--- LISTA CAMPI TROVATI NEL PDF ---');
print('Totale campi: ${form.fields.count}\n');
for (int i = 0; i < form.fields.count; i++) {
final field = form.fields[i];
String tipo = 'Sconosciuto';
if (field is PdfTextBoxField) tipo = 'TEXT';
if (field is PdfCheckBoxField) tipo = 'CHECKBOX';
// Stampa in formato facile da copiare
print('Key: "${field.name}" \t Tipo: $tipo');
}
print('-----------------------------------\n');
document.dispose();
}
=== FILE: lib/carro_attr.dart ===
import 'dart:convert';
import 'dart:async'; // Necessario per il Timeout
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:url_launcher/url_launcher.dart';
class CarroAttrezziScreen extends StatefulWidget {
const CarroAttrezziScreen({super.key});
@override
State<CarroAttrezziScreen> createState() => _CarroAttrezziScreenState();
}
class _CarroAttrezziScreenState extends State<CarroAttrezziScreen> {
List<dynamic> _officine = [];
bool _isLoading = true;
String _statusMessage = "Attivazione GPS...";
@override
void initState() {
super.initState();
_avviaRicercaSicura();
}
Future<void> _avviaRicercaSicura() async {
// Piccolo ritardo iniziale per dare tempo alla UI di disegnarsi
await Future.delayed(const Duration(milliseconds: 500));
if (!mounted) return;
_cercaSoccorsiVicini();
}
Future<void> _cercaSoccorsiVicini() async {
try {
// 1. GESTIONE PERMESSI ROBUSTA
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) {
if (!mounted) return;
setState(() {
_statusMessage = "Attiva il GPS per trovare i soccorsi.";
_isLoading = false;
});
return;
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
if (permission == LocationPermission.denied) {
if (!mounted) return;
setState(() {
_statusMessage = "Permesso GPS negato.";
_isLoading = false;
});
return;
}
}
if (permission == LocationPermission.deniedForever) {
if (!mounted) return;
setState(() {
_statusMessage = "Permessi GPS bloccati permanentemente.";
_isLoading = false;
});
return;
}
// 2. CICLO WHILE PER OTTENERE LA POSIZIONE (WAIT FOR GPS)
if (mounted) setState(() => _statusMessage = "Ricerca posizione in corso...");
Position? position;
int tentativi = 0;
const int maxTentativi = 3; // Prova 3 volte prima di arrendersi
// FINCHÉ non ho la posizione E non ho superato i tentativi...
while (position == null && tentativi < maxTentativi) {
try {
if (tentativi > 0) {
if (mounted) setState(() => _statusMessage = "Aggancio satelliti (Tentativo ${tentativi + 1}/$maxTentativi)...");
}
// Prova a prendere la posizione con un timeout di 6 secondi per tentativo
position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
timeLimit: const Duration(seconds: 6),
);
} catch (e) {
// Se fallisce, aspetta 1 secondo e riprova
tentativi++;
await Future.delayed(const Duration(seconds: 1));
}
}
// Se dopo il ciclo while è ancora null, proviamo l'ultima posizione nota
if (position == null) {
if (mounted) setState(() => _statusMessage = "Segnale debole, uso ultima posizione...");
position = await Geolocator.getLastKnownPosition();
}
// Se è ancora null, alziamo bandiera bianca
if (position == null) {
throw "Impossibile ottenere la posizione GPS dopo vari tentativi.";
}
// 3. RICERCA SU OVERPASS API (OSM)
if (mounted) setState(() => _statusMessage = "Ricerca soccorsi nei paraggi...");
String query = """
[out:json][timeout:25];
(
node["craft"="car_repair"](around:15000,${position.latitude},${position.longitude});
node["shop"="car_repair"](around:15000,${position.latitude},${position.longitude});
node["service"="vehicle_recovery"](around:15000,${position.latitude},${position.longitude});
);
out body;
""";
final url = Uri.parse('https://overpass-api.de/api/interpreter?data=${Uri.encodeComponent(query)}');
final response = await http.get(
url,
headers: {'User-Agent': 'CidApp_Flutter/1.0'},
).timeout(const Duration(seconds: 20));
if (response.statusCode == 200) {
final data = json.decode(response.body);
List<dynamic> elements = data['elements'];
List<Map<String, dynamic>> listaElaborata = [];
for (var element in elements) {
if (element['tags'] != null && element['tags']['name'] != null) {
double distanzaMetri = Geolocator.distanceBetween(
position!.latitude, position.longitude, element['lat'], element['lon']
);
listaElaborata.add({
'name': element['tags']['name'],
'phone': element['tags']['phone'] ?? element['tags']['contact:phone'] ?? element['tags']['mobile'],
'street': element['tags']['addr:street'] ?? "",
'city': element['tags']['addr:city'] ?? "",
'distance': distanzaMetri,
'lat': element['lat'],
'lon': element['lon'],
});
}
}
listaElaborata.sort((a, b) => (a['distance'] as double).compareTo(b['distance'] as double));
if (mounted) {
setState(() {
_officine = listaElaborata;
_isLoading = false;
});
}
} else {
throw "Errore server OSM: ${response.statusCode}";
}
} catch (e) {
if (mounted) {
setState(() {
_statusMessage = "Nessun soccorso trovato o errore GPS.\nRiprova tra poco.";
_isLoading = false;
});
}
}
}
Future<void> _chiamaNumero(String? numero) async {
if (numero == null) return;
// Pulisce il numero da spazi o caratteri strani
final pulito = numero.replaceAll(RegExp(r'[^0-9+]'), '');
final Uri url = Uri.parse("tel:$pulito");
if (await canLaunchUrl(url)) await launchUrl(url);
}
Future<void> _apriMappa(double lat, double lon) async {
// Link universale per aprire la navigazione
final Uri url = Uri.parse("https://www.google.com/maps/search/?api=1&query=$lat,$lon");
try {
if (await canLaunchUrl(url)) {
await launchUrl(url, mode: LaunchMode.externalApplication);
} else {
throw 'Impossibile aprire la mappa';
}
} catch (e) {
debugPrint("Errore apertura mappa: $e");
}
}
Future<void> _cercaSuGoogle(String nome) async {
final Uri url = Uri.parse("https://www.google.com/search?q=soccorso stradale $nome telefono");
if (await canLaunchUrl(url)) await launchUrl(url, mode: LaunchMode.externalApplication);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Soccorso Stradale"),
backgroundColor: Colors.red.shade800,
foregroundColor: Colors.white,
),
body: _isLoading
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: Colors.red),
const SizedBox(height: 20),
Text(_statusMessage, style: const TextStyle(color: Colors.grey)),
],
),
)
: _officine.isEmpty
? Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.car_crash, size: 60, color: Colors.grey),
const SizedBox(height: 10),
const Text("Nessun soccorso trovato nei paraggi (15km).", textAlign: TextAlign.center),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => _cercaSuGoogle("vicino a me"),
child: const Text("Cerca su Google"),
)
],
),
),
)
: ListView.builder(
padding: const EdgeInsets.all(10),
itemCount: _officine.length,
itemBuilder: (context, index) {
final officina = _officine[index];
double km = (officina['distance'] as double) / 1000;
String indirizzo = officina['street'];
if (officina['city'] != "") indirizzo += (indirizzo.isNotEmpty ? ", " : "") + officina['city'];
if (indirizzo.isEmpty) indirizzo = "Posizione GPS";
return Card(
elevation: 3,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
// KM a sinistra
leading: Container(
width: 55,
height: 55,
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade100)
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on, color: Colors.red.shade700, size: 24),
Text("${km.toStringAsFixed(1)} km", style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold), maxLines: 1),
],
),
),
title: Text(officina['name'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
subtitle: Text(indirizzo, style: const TextStyle(fontSize: 13), maxLines: 2, overflow: TextOverflow.ellipsis),
// DUE TASTI A DESTRA: MAPPA e CHIAMA
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Tasto MAPPA
IconButton(
icon: const Icon(Icons.directions, color: Colors.blue, size: 32),
onPressed: () => _apriMappa(officina['lat'], officina['lon']),
tooltip: "Naviga",
),
// Tasto CHIAMA
officina['phone'] != null
? IconButton(
icon: const Icon(Icons.phone_in_talk, color: Colors.green, size: 32),
onPressed: () => _chiamaNumero(officina['phone']),
)
: IconButton(
icon: const Icon(Icons.search, color: Colors.orange, size: 32),
onPressed: () => _cercaSuGoogle(officina['name']),
tooltip: "Cerca Web",
),
],
),
),
),
);
},
),
);
}
}
=== FILE: lib/comp_9.dart ===
// Versione: 2.3.5 - Popup Automatico + Calendari Cliccabili
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'global_data.dart';
import 'comp_10.dart';
class DateInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
var text = newValue.text.replaceAll('/', '');
if (text.length > 8) text = text.substring(0, 8);
var result = "";
for (int i = 0; i < text.length; i++) {
if (i == 2 || i == 4) result += "/";
result += text[i];
}
return newValue.copyWith(
text: result,
selection: TextSelection.collapsed(offset: result.length)
);
}
}
class Comp9Screen extends StatefulWidget {
const Comp9Screen({super.key});
@override
_Comp9ScreenState createState() => _Comp9ScreenState();
}
class _Comp9ScreenState extends State<Comp9Screen> {
late TextEditingController _cognome, _nome, _cf, _nascita, _indirizzo, _stato, _tel, _patente, _categoriaAltro, _scadenza;
String _selectedCat = "B";
String? _erroreNascita;
String? _erroreScadenza;
bool get isB => GlobalData.latoCorrente == 'B';
@override
void initState() {
super.initState();
_initControllers();
// Appena la pagina è costruita, lancia il controllo per i popup
WidgetsBinding.instance.addPostFrameCallback((_) {
// PRIMA mostra le info della pagina...
_mostraInfoPopup(context);
// ...POI controlla se ci sono date pre-compilate da validare
if (_nascita.text.isNotEmpty) _validaNascita(_nascita.text);
if (_scadenza.text.isNotEmpty) _validaScadenza(_scadenza.text);
});
}
void _initControllers() {
if (isB) {
_cognome = TextEditingController(text: GlobalData.Cognome_cond_B);
_nome = TextEditingController(text: GlobalData.Nome_cond_B);
_cf = TextEditingController(text: GlobalData.Cod_fiscale_cond_B);
_nascita = TextEditingController(text: GlobalData.Data_nascita_cond_B);
_indirizzo = TextEditingController(text: GlobalData.Indirizzo_cond_B);
_stato = TextEditingController(text: GlobalData.Stato_cond_B.isEmpty ? "ITALIA" : GlobalData.Stato_cond_B);
_tel = TextEditingController(text: GlobalData.N_tel_mail_cond_B);
_patente = TextEditingController(text: GlobalData.N_Patente_cond_B);
_scadenza = TextEditingController(text: GlobalData.Scadenza_cond_B);
_setupCategoria(GlobalData.Categoria_cond_B);
} else {
_cognome = TextEditingController(text: GlobalData.Cognome_cond_A);
_nome = TextEditingController(text: GlobalData.Nome_cond_A);
_cf = TextEditingController(text: GlobalData.Cod_fiscale_cond_A);
_nascita = TextEditingController(text: GlobalData.Data_nascita_cond_A);
_indirizzo = TextEditingController(text: GlobalData.Indirizzo_cond_A);
_stato = TextEditingController(text: GlobalData.Stato_cond_A.isEmpty ? "ITALIA" : GlobalData.Stato_cond_A);
_tel = TextEditingController(text: GlobalData.N_tel_mail_cond_A);
_patente = TextEditingController(text: GlobalData.N_Patente_cond_A);
_scadenza = TextEditingController(text: GlobalData.Scadenza_cond_A);
_setupCategoria(GlobalData.Categoria_cond_A);
}
}
// --- POPUP INFORMATIVO ANIMATO ---
void _mostraInfoPopup(BuildContext context) {
Color activeColor = isB ? Colors.amber.shade700 : Colors.blue.shade900;
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierLabel: "Popup",
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 400),
pageBuilder: (context, animation, secondaryAnimation) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
children: [
Icon(Icons.person_pin, color: activeColor, size: 28),
const SizedBox(width: 10),
const Expanded(child: Text("Dati Conducente", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("In questa sezione inseriremo i dati della persona che era alla guida del Veicolo ${GlobalData.latoCorrente}.", style: const TextStyle(fontSize: 15)),
const SizedBox(height: 16),
_buildPopupRow(Icons.file_download, "Importazione", "Se il conducente è la stessa persona del contraente (l'assicurato), potrai importare i suoi dati in un tocco."),
const SizedBox(height: 12),
_buildPopupRow(Icons.badge, "Patente", "Assicurati di inserire correttamente il numero, la categoria e la data di scadenza della patente di guida."),
],
),
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
foregroundColor: isB ? Colors.black87 : Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
onPressed: () {
Navigator.pop(context); // Chiude l'info popup
// Dopo aver chiuso questo, controlla se deve mostrare l'altro popup per l'importazione
_mostraDialogoImportazione();
},
child: const Text("HO CAPITO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
var curvePosizione = CurvedAnimation(parent: animation, curve: Curves.easeOutBack, reverseCurve: Curves.easeInBack);
var curveOpacita = CurvedAnimation(parent: animation, curve: Curves.easeOut, reverseCurve: Curves.easeIn);
return SlideTransition(position: Tween<Offset>(begin: const Offset(0.0, 0.4), end: Offset.zero).animate(curvePosizione), child: FadeTransition(opacity: curveOpacita, child: child));
},
);
}
Widget _buildPopupRow(IconData icon, String title, String desc) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 24, color: Colors.blueGrey),
const SizedBox(width: 12),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
children: [
TextSpan(text: "$title: ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: desc),
],
),
),
),
],
);
}
// LOGICA POPUP AUTOMATICO
void _mostraDialogoImportazione() {
// 1. Se i campi sono già compilati (es. torno indietro per modificare), NON mostrare nulla
if (_cognome.text.trim().isNotEmpty && _nome.text.trim().isNotEmpty) return;
// 2. Controllo se esistono i dati del contraente da importare
String contraenteNome = isB ? GlobalData.Nome_contraente_B : GlobalData.Nome_contraente_A;
String contraenteCognome = isB ? GlobalData.Cognome_contraente_B : GlobalData.Cognome_contraente_A;
if (contraenteNome.isEmpty && contraenteCognome.isEmpty) return;
// 3. Mostra il Popup
showDialog(
context: context,
barrierDismissible: false, // L'utente deve fare una scelta
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: Row(
children: [
Icon(Icons.person_add_alt_1, color: isB ? Colors.amber[800] : Colors.blue[800]),
const SizedBox(width: 10),
const Text("Conducente"),
],
),
content: Text(
"Il conducente è la stessa persona del contraente ($contraenteNome $contraenteCognome)?\n\nVuoi usare i suoi dati?",
style: const TextStyle(fontSize: 16),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text("NO, scrivo a mano", style: TextStyle(color: Colors.grey[600])),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: isB ? Colors.amber[700] : Colors.blue.shade900,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: () {
_importaDati();
Navigator.pop(ctx);
},
child: const Text("SÌ, importa"),
),
],
),
);
}
void _importaDati() {
setState(() {
if (isB) {
_cognome.text = GlobalData.Cognome_contraente_B;
_nome.text = GlobalData.Nome_contraente_B;
_cf.text = GlobalData.Codice_Fiscale_contraente_B;
String addr = GlobalData.Indirizzo_contraente_B;
if (GlobalData.CAP_contraente_B.isNotEmpty) addr += " ${GlobalData.CAP_contraente_B}";
_indirizzo.text = addr.trim();
_stato.text = GlobalData.Stato_contraente_B.isNotEmpty ? GlobalData.Stato_contraente_B : "ITALIA";
_tel.text = GlobalData.N_telefono_mail_contraente_B;
} else {
_cognome.text = GlobalData.Cognome_contraente_A;
_nome.text = GlobalData.Nome_contraente_A;
_cf.text = GlobalData.Codice_Fiscale_contraente_A;
String addr = GlobalData.Indirizzo_contraente_A;
if (GlobalData.CAP_contraente_A.isNotEmpty) addr += " ${GlobalData.CAP_contraente_A}";
_indirizzo.text = addr.trim();
_stato.text = GlobalData.Stato_contraente_A.isNotEmpty ? GlobalData.Stato_contraente_A : "ITALIA";
_tel.text = GlobalData.N_telefono_mail_contraente_A;
}
});
}
void _setupCategoria(String catEsistente) {
if (["A", "B", "C", "D", "E"].contains(catEsistente)) {
_selectedCat = catEsistente;
_categoriaAltro = TextEditingController();
} else if (catEsistente.isNotEmpty) {
_selectedCat = "Altro";
_categoriaAltro = TextEditingController(text: catEsistente);
} else {
_selectedCat = "B";
_categoriaAltro = TextEditingController();
}
}
// --- LOGICA CALENDARIO ---
Future<void> _selezionaData(BuildContext context, TextEditingController controller, Function(String) onChanged) async {
DateTime initialDate = DateTime.now();
if (controller.text.length == 10) {
try {
List<String> parti = controller.text.split('/');
initialDate = DateTime(int.parse(parti[2]), int.parse(parti[1]), int.parse(parti[0]));
} catch (_) {}
}
final DateTime? picked = await showDatePicker(
context: context,
initialDate: initialDate,
firstDate: DateTime(1900), // Anno minimo sensato per la nascita
lastDate: DateTime(2100),
locale: const Locale('it', 'IT'),
);
if (picked != null) {
String dataFormattata = "${picked.day.toString().padLeft(2, '0')}/${picked.month.toString().padLeft(2, '0')}/${picked.year}";
setState(() {
controller.text = dataFormattata;
onChanged(dataFormattata);
});
}
}
String? _calcolaErroreData(String data) {
if (data.isEmpty) return null;
if (data.length < 10) return "Formato: GG/MM/AAAA";
List<String> parti = data.split('/');
if (parti.length != 3) return "Formato errato";
int? giorno = int.tryParse(parti[0]);
int? mese = int.tryParse(parti[1]);
int? anno = int.tryParse(parti[2]);
if (giorno == null || mese == null || anno == null) return "Data non valida";
if (giorno < 1 || giorno > 31) return "Giorno errato";
if (mese < 1 || mese > 12) return "Mese errato";
int giorniMax = _giorniInMese(mese, anno);
if (giorno > giorniMax) {
String nomeMese = _getNomeMese(mese);
return "$nomeMese $anno ha solo $giorniMax gg";
}
return null;
}
int _giorniInMese(int mese, int anno) {
if (mese == 2) {
bool bisestile = (anno % 4 == 0 && anno % 100 != 0) || (anno % 400 == 0);
return bisestile ? 29 : 28;
}
if ([4, 6, 9, 11].contains(mese)) return 30;
return 31;
}
String _getNomeMese(int mese) {
const mesi = ["Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic"];
if (mese >= 1 && mese <= 12) return mesi[mese - 1];
return "Mese";
}
void _validaNascita(String val) => setState(() => _erroreNascita = _calcolaErroreData(val));
void _validaScadenza(String val) => setState(() => _erroreScadenza = _calcolaErroreData(val));
void _salvaEProsegui() {
String catFinale = (_selectedCat == "Altro") ? _categoriaAltro.text.toUpperCase() : _selectedCat;
bool categoriaMancante = (_selectedCat == "Altro" && _categoriaAltro.text.trim().isEmpty);
if (_cognome.text.trim().isEmpty || _nome.text.trim().isEmpty || _cf.text.trim().isEmpty ||
_nascita.text.trim().isEmpty || _indirizzo.text.trim().isEmpty || _stato.text.trim().isEmpty ||
_patente.text.trim().isEmpty || _scadenza.text.trim().isEmpty || categoriaMancante) {
_mostraErrore("Tutti i dati del conducente sono obbligatori!", Colors.red);
return;
}
if (_erroreNascita != null || _erroreScadenza != null) {
_mostraErrore("Correggi le date in rosso!", Colors.red);
return;
}
if (_nascita.text.length < 10 || _scadenza.text.length < 10) {
_mostraErrore("Formato data incompleto (GG/MM/AAAA)", Colors.orange);
return;
}
if (isB) {
GlobalData.Cognome_cond_B = _cognome.text.toUpperCase();
GlobalData.Nome_cond_B = _nome.text.toUpperCase();
GlobalData.Cod_fiscale_cond_B = _cf.text.toUpperCase();
GlobalData.Data_nascita_cond_B = _nascita.text;
GlobalData.Indirizzo_cond_B = _indirizzo.text.toUpperCase();
GlobalData.Stato_cond_B = _stato.text.toUpperCase();
GlobalData.N_tel_mail_cond_B = _tel.text.toUpperCase();
GlobalData.N_Patente_cond_B = _patente.text.toUpperCase();
GlobalData.Categoria_cond_B = catFinale;
GlobalData.Scadenza_cond_B = _scadenza.text;
} else {
GlobalData.Cognome_cond_A = _cognome.text.toUpperCase();
GlobalData.Nome_cond_A = _nome.text.toUpperCase();
GlobalData.Cod_fiscale_cond_A = _cf.text.toUpperCase();
GlobalData.Data_nascita_cond_A = _nascita.text;
GlobalData.Indirizzo_cond_A = _indirizzo.text.toUpperCase();
GlobalData.Stato_cond_A = _stato.text.toUpperCase();
GlobalData.N_tel_mail_cond_A = _tel.text.toUpperCase();
GlobalData.N_Patente_cond_A = _patente.text.toUpperCase();
GlobalData.Categoria_cond_A = catFinale;
GlobalData.Scadenza_cond_A = _scadenza.text;
}
Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp10Screen()));
}
void _mostraErrore(String msg, Color colore) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg), backgroundColor: colore, duration: const Duration(seconds: 3)),
);
}
@override
Widget build(BuildContext context) {
Color accentColor = isB ? Colors.amber.shade700 : Colors.blue.shade900;
Color bgColor = isB ? const Color(0xFFFFF9C4) : const Color(0xFFE3F2FD);
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
backgroundColor: bgColor,
resizeToAvoidBottomInset: true,
appBar: AppBar(
title: Text("9. Conducente (${GlobalData.latoCorrente})"),
backgroundColor: accentColor,
foregroundColor: isB ? Colors.black : Colors.white,
),
body: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(16),
child: Column(
children: [
_buildSectionCard(
titolo: "DATI PERSONALI",
accentColor: accentColor,
children: [
_buildTextField(_cognome, "Cognome *", Icons.person, accentColor),
_buildTextField(_nome, "Nome *", Icons.person_outline, accentColor),
// --- MODIFICATO: DATA DI NASCITA ---
_buildTextField(_nascita, "Data di Nascita *", Icons.cake, accentColor,
isDate: true, hint: "GG/MM/AAAA", errorText: _erroreNascita, onChanged: _validaNascita,
customPrefix: InkWell(
onTap: () => _selezionaData(context, _nascita, _validaNascita),
borderRadius: BorderRadius.circular(20),
child: Container(
width: 38,
alignment: Alignment.center,
child: Icon(Icons.cake, size: 20, color: accentColor),
),
),
),
_buildTextField(_cf, "Codice Fiscale *", Icons.badge, accentColor, isUpper: true),
_buildTextField(_indirizzo, "Indirizzo (Via, Cap, Città) *", Icons.home, accentColor),
_buildTextField(_stato, "Stato *", Icons.flag, accentColor),
_buildTextField(_tel, "Tel/Email *", Icons.contact_mail, accentColor),
],
),
_buildSectionCard(
titolo: "PATENTE DI GUIDA",
accentColor: accentColor,
children: [
_buildTextField(_patente, "N. Patente *", Icons.credit_card, accentColor, isUpper: true),
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: DropdownButtonFormField<String>(
value: _selectedCat,
decoration: InputDecoration(
labelText: "Categoria *",
prefixIcon: Icon(Icons.category, color: accentColor),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
filled: true, fillColor: Colors.white,
),
items: ["A", "B", "C", "D", "E", "Altro"].map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(),
onChanged: (v) => setState(() => _selectedCat = v!),
),
),
if (_selectedCat == "Altro")
_buildTextField(_categoriaAltro, "Specifica Categoria *", Icons.edit, accentColor, isUpper: true),
// --- MODIFICATO: SCADENZA PATENTE ---
_buildTextField(_scadenza, "Valida fino al *", Icons.event_available, accentColor,
isDate: true, hint: "GG/MM/AAAA", errorText: _erroreScadenza, onChanged: _validaScadenza,
customPrefix: InkWell(
onTap: () => _selezionaData(context, _scadenza, _validaScadenza),
borderRadius: BorderRadius.circular(20),
child: Container(
width: 38,
alignment: Alignment.center,
child: Icon(Icons.event_available, size: 20, color: accentColor),
),
),
),
],
),
_navButtons(accentColor),
const SizedBox(height: 10),
],
),
),
),
),
);
}
// --- MODIFICATO IL METODO PER ACCETTARE customPrefix E RESTRINGERE LA UI ---
Widget _buildTextField(TextEditingController c, String label, IconData icon, Color color,
{bool isDate = false, bool isUpper = false, bool isNumeric = false, String? hint, String? errorText, Function(String)? onChanged, Widget? customPrefix}) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: TextField(
controller: c,
keyboardType: isDate || isNumeric ? TextInputType.number : TextInputType.text,
textCapitalization: isUpper ? TextCapitalization.characters : TextCapitalization.sentences,
onChanged: onChanged,
style: TextStyle(fontSize: isDate ? 14 : 16), // Rimpicciolisce un po' le date
inputFormatters: [
if (isDate) DateInputFormatter(),
if (isDate) LengthLimitingTextInputFormatter(10),
if (isNumeric) FilteringTextInputFormatter.digitsOnly,
if (isNumeric) LengthLimitingTextInputFormatter(5),
],
decoration: InputDecoration(
labelText: label,
hintText: hint,
errorText: errorText,
errorStyle: const TextStyle(fontSize: 10),
prefixIcon: customPrefix ?? Icon(icon, color: color),
prefixIconConstraints: isDate ? const BoxConstraints(minWidth: 38, minHeight: 38) : null,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 15),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
filled: true, fillColor: Colors.white,
),
),
);
}
Widget _buildSectionCard({required String titolo, required List<Widget> children, required Color accentColor}) => Container(
width: double.infinity, margin: const EdgeInsets.only(bottom: 20), padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(15), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10)]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(titolo, style: TextStyle(fontWeight: FontWeight.bold, color: accentColor, fontSize: 17)),
const Divider(height: 25), ...children,
]),
);
Widget _navButtons(Color btnColor) => Row(children: [
Expanded(flex: 4, child: OutlinedButton(onPressed: () => Navigator.pop(context), style: OutlinedButton.styleFrom(minimumSize: const Size(0, 55), side: BorderSide(color: btnColor, width: 1.5), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), child: const FittedBox(child: Text("INDIETRO", style: TextStyle(fontWeight: FontWeight.bold))))),
const SizedBox(width: 15),
Expanded(flex: 7, child: ElevatedButton(style: ElevatedButton.styleFrom(backgroundColor: btnColor, foregroundColor: isB ? Colors.black : Colors.white, minimumSize: const Size(0, 55), elevation: 5, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15))), onPressed: _salvaEProsegui, child: const FittedBox(child: Text("SALVA E PROSEGUI", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))))),
]);
@override
void dispose() {
_cognome.dispose(); _nome.dispose(); _cf.dispose(); _nascita.dispose();
_indirizzo.dispose(); _stato.dispose(); _tel.dispose();
_patente.dispose(); _categoriaAltro.dispose(); _scadenza.dispose();
super.dispose();
}
}
=== FILE: lib/scelta_lato.dart ===
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; // <-- AGGIUNTA LIBRERIA PER IL DEBUG
import 'global_data.dart';
import 'comp_1-5.dart';
import 'comp_6-7.dart';
class SceltaLatoScreen extends StatefulWidget {
const SceltaLatoScreen({super.key});
@override
State<SceltaLatoScreen> createState() => _SceltaLatoScreenState();
}
class _SceltaLatoScreenState extends State<SceltaLatoScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_mostraGuidaSicurezza(context);
});
}
void _mostraGuidaSicurezza(BuildContext context) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(color: Colors.red.shade50, borderRadius: BorderRadius.circular(10)),
child: const Icon(Icons.health_and_safety, color: Colors.red),
),
const SizedBox(width: 10),
const Text("GUIDA RAPIDA", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: const [
_StepGuida(icon: Icons.back_hand, text: "FERMATI in sicurezza e non intralciare il traffico."),
Divider(),
_StepGuida(icon: Icons.engineering, text: "Se sei fuori dal centro abitato indossa il GILET arancione e posiziona il TRIANGOLO."),
Divider(),
_StepGuida(icon: Icons.local_hospital, text: "Ci sono feriti? NON muoverli e chiama il 118."),
Divider(),
_StepGuida(icon: Icons.file_copy, text: "Prepara la patente e i dati della polizza, ti serviranno per compilare il modulo."),
Divider(),
_StepGuida(
icon: Icons.camera_alt,
text: "FAI LE FOTO ORA! 📸\n1. Targhe veicoli.\n2. Danni da vicino e lontano.\n3. Posizione auto sulla strada.\n\nLe allegherai alla fine nella mail!",
isBold: true
),
],
),
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade800,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))
),
onPressed: () => Navigator.pop(ctx),
child: const Text("HO CAPITO, INIZIAMO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Compilazione CAI", style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.blue.shade900,
foregroundColor: Colors.white,
elevation: 0,
actions: [
if (kDebugMode) // <-- AGGIUNTO: NASCONDE IL TASTO NEGLI STORE
IconButton(
icon: const Icon(Icons.flash_on, color: Colors.orangeAccent),
tooltip: "POPOLA DATI TEST",
onPressed: () {
GlobalData.popolaDatiDiTest();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("⚡ Dati di test caricati! Vai a generare il PDF."),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
)
);
},
),
IconButton(
icon: const Icon(Icons.help_outline, color: Colors.yellowAccent, size: 28),
onPressed: () => _mostraGuidaSicurezza(context),
tooltip: "Guida Rapida Sicurezza",
)
],
),
body: Stack(
children: [
// 1. BASE CROMATICA DIVISA (Blu/Giallo)
Row(
children: [
Expanded(child: Container(color: const Color(0xFFE3F2FD))), // Blu chiaro
Expanded(child: Container(color: const Color(0xFFFFFDE7))), // Giallo chiaro
],
),
// 2. FILIGRANA STILIZZATA (Sfondo CID)
Positioned.fill(
child: Opacity(
opacity: 0.12,
child: Image.asset(
'assets/sfondo_cid.jpg',
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
errorBuilder: (c, e, s) => const SizedBox(),
),
),
),
// 3. CONTENUTO ATTIVO (SEMPLIFICATO E OTTIMIZZATO)
SafeArea(
child: SingleChildScrollView(
// Aggiungiamo un po' di padding per non attaccare il tutto ai bordi dello schermo
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Il box informativo ora è diretto figlio della colonna
_buildInfoBox(context),
const SizedBox(height: 20), // Spazio ridotto tra box e bottoni
// PULSANTI AFFIANCATI (Responsive)
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// LATO A (BLU)
Expanded(
child: _buildBtnConRilievo(
context,
"VEICOLO A",
"LATO BLU",
Colors.blue.shade800,
'A',
Icons.directions_car_rounded,
),
),
const SizedBox(width: 20),
// LATO B (GIALLO)
Expanded(
child: _buildBtnConRilievo(
context,
"VEICOLO B",
"LATO GIALLO",
Colors.amber.shade600,
'B',
Icons.directions_car_filled_rounded,
),
),
],
),
),
const SizedBox(height: 20), // Un po' di spazio extra in fondo
],
),
),
),
],
),
);
}
// FUNZIONE AGGIORNATA PER EFFETTO 3D POTENZIATO
Widget _buildBtnConRilievo(BuildContext context, String titolo, String sottotitolo, Color color, String lato, IconData icon) {
bool isB = lato == 'B';
// Definiamo i colori per il gradiente (luce in alto a sx, ombra in basso a dx)
Color colorLight, colorDark;
if (isB) {
// Per il giallo
colorLight = Colors.amber.shade400; // Luce
colorDark = Colors.amber.shade700; // Ombra
} else {
// Per il blu
colorLight = Colors.blue.shade600; // Luce
colorDark = Colors.blue.shade900; // Ombra
}
return Container(
// Decorazione complessa del Container per l'effetto 3D
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24), // Arrotondamento leggermente aumentato
// 1. GRADIENTE DI SUPERFICIE (Simula la luce che colpisce un oggetto curvo)
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [colorLight, colorDark],
stops: const [0.1, 0.9], // Regola i punti di luce e ombra
),
// 2. OMBRE STRATIFICATE (Doppia ombra per profondità realistica)
boxShadow: [
// Ombra 1: "Spessore" (scura, nitida, vicina)
BoxShadow(
color: colorDark.withOpacity(0.6),
blurRadius: 8,
offset: const Offset(0, 8),
spreadRadius: 1, // Espande leggermente l'ombra scura
),
// Ombra 2: "Sollevamento" (morbida, ampia, lontana)
BoxShadow(
color: colorDark.withOpacity(0.3),
blurRadius: 25,
offset: const Offset(0, 18),
spreadRadius: -5, // Contrae l'ombra diffusa per non sporcare troppo
),
],
),
child: Material(
color: Colors.transparent, // Necessario per far vedere il gradiente sottostante
child: InkWell(
// L'InkWell gestisce il tocco e l'effetto "splash"
borderRadius: BorderRadius.circular(24),
onTap: () {
GlobalData.latoCorrente = lato;
if (lato == 'B') {
Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp6_7Screen()));
} else {
Navigator.push(context, MaterialPageRoute(builder: (c) => const Comp1_5Screen()));
}
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Aggiunto un leggero effetto ombra anche all'icona per farla "uscire"
Icon(
icon,
size: 48,
color: isB ? Colors.black87 : Colors.white,
shadows: [
Shadow(
color: Colors.black.withOpacity(0.3),
offset: const Offset(2, 2),
blurRadius: 4,
)
],
),
const SizedBox(height: 15),
Text(
titolo,
style: TextStyle(
fontSize: 20, // Leggermente più grande
fontWeight: FontWeight.w800, // Più grassetto
color: isB ? Colors.black87 : Colors.white,
// Leggera ombra sul testo per contrasto
shadows: [
Shadow(color: Colors.black.withOpacity(0.2), offset: const Offset(1, 1), blurRadius: 2)
]
)
),
const SizedBox(height: 5),
Text(
sottotitolo,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: isB ? Colors.black54 : Colors.white70
)
),
],
),
),
),
),
);
}
Widget _buildInfoBox(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
// Rimosso il margine verticale che creava spazio extra inutile
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.blue.shade200, width: 1.5),
// Aggiunta una leggera ombra per staccarlo dallo sfondo
boxShadow: [
BoxShadow(color: Colors.blue.shade100.withOpacity(0.5), blurRadius: 8, offset: const Offset(0, 4))
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.lightbulb_outline, color: Colors.blue.shade800),
const SizedBox(width: 10),
Expanded(
child: Text(
"Come funziona la scelta del lato?",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue.shade900
),
),
),
],
),
const SizedBox(height: 12),
const Text(
"Nel modulo CAI, i veicoli vengono chiamati A e B. La lettera non indica chi ha ragione o torto, serve solo per distinguere le due auto.",
style: TextStyle(fontSize: 14, height: 1.4),
),
const SizedBox(height: 16),
_buildInfoRow("🤝", "1. Accordati", "Scegli con l'altro conducente chi sarà il Veicolo A e chi il B."),
const SizedBox(height: 12),
_buildInfoRow("✍️", "2. Compila", "Seleziona il tuo lato e inserisci i dati con attenzione per non dover ricominciare."),
const SizedBox(height: 12),
_buildInfoRow("📱", "3. Scambia e Genera", "Firma e scambia i dati con la controparte per ottenere il PDF per l'assicurazione."),
],
),
);
}
Widget _buildInfoRow(String emoji, String title, String desc) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(emoji, style: const TextStyle(fontSize: 18)),
const SizedBox(width: 10),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
children: [
TextSpan(text: "$title\n", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: desc),
],
),
),
),
],
);
}
}
// Widget Helper interno
class _StepGuida extends StatelessWidget {
final IconData icon;
final String text;
final bool isBold;
const _StepGuida({required this.icon, required this.text, this.isBold = false});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: isBold ? Colors.red : Colors.blueGrey, size: 24),
const SizedBox(width: 15),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 15,
height: 1.4,
fontWeight: isBold ? FontWeight.bold : FontWeight.normal,
color: isBold ? Colors.red.shade900 : Colors.black87,
),
),
),
],
),
);
}
}
=== FILE: lib/comp_16.dart ===
import 'dart:io';
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:path_provider/path_provider.dart';
import 'package:printing/printing.dart';
import 'package:share_plus/share_plus.dart';
import 'scambio_dati_screen.dart';
import 'pdf_engine.dart';
import 'global_data.dart';
import 'main.dart';
import 'comp_6-7.dart';
import 'comp_1-5.dart';
class Comp16Screen extends StatefulWidget {
const Comp16Screen({super.key});
@override
State<Comp16Screen> createState() => _Comp16ScreenState();
}
class _Comp16ScreenState extends State<Comp16Screen> with WidgetsBindingObserver {
bool _scambioEffettuato = false;
bool _datiPresenti = false;
bool _ioHoApprovato = false;
bool _tuttiHannoApprovato = false;
bool _staCancellando = false;
bool _cancellazioneAvviataDaMe = false;
String _statusText = "Esegui lo Scambio Dati per iniziare.";
Color _statusColor = Colors.orange.shade800;
IconData _statusIcon = Icons.warning_amber_rounded;
File? _filePdfReale;
Uint8List? _immagineAnteprima;
bool _isLoading = false;
StreamSubscription? _roomSubscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
_puliziaIngresso();
WidgetsBinding.instance.addPostFrameCallback((_) {
_mostraInfoPopup(context);
});
}
void _mostraInfoPopup(BuildContext context) {
Color activeColor = Colors.blue.shade900;
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierLabel: "Popup",
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 400),
pageBuilder: (context, animation, secondaryAnimation) {
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: Row(
children: [
Icon(Icons.sync_alt, color: activeColor, size: 28),
const SizedBox(width: 10),
const Expanded(child: Text("Scambio e Invio", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18))),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text("Questa è la fase finale. Segui questi tre passaggi per concludere il modulo:", style: TextStyle(fontSize: 15)),
const SizedBox(height: 16),
_buildPopupRow(Icons.qr_code_scanner, "1. Scambio Dati", "Inquadra il QR Code dell'altro conducente oppure inserisci a mano il suo codice PIN."),
const SizedBox(height: 12),
_buildPopupRow(Icons.visibility, "2. Anteprima", "Apri l'anteprima per verificare che i dati di entrambi siano impaginati correttamente sul documento."),
const SizedBox(height: 12),
_buildPopupRow(Icons.check_circle, "3. Approvazione", "Se tutto è esatto, clicca su Approva. Quando entrambi avrete approvato, il file sarà pronto per l'invio."),
],
),
),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: activeColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 14),
),
onPressed: () => Navigator.pop(context),
child: const Text("HO CAPITO", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
),
],
);
},
transitionBuilder: (context, animation, secondaryAnimation, child) {
var curvePosizione = CurvedAnimation(parent: animation, curve: Curves.easeOutBack, reverseCurve: Curves.easeInBack);
var curveOpacita = CurvedAnimation(parent: animation, curve: Curves.easeOut, reverseCurve: Curves.easeIn);
return SlideTransition(position: Tween<Offset>(begin: const Offset(0.0, 0.4), end: Offset.zero).animate(curvePosizione), child: FadeTransition(opacity: curveOpacita, child: child));
},
);
}
Widget _buildPopupRow(IconData icon, String title, String desc) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 24, color: Colors.blueGrey),
const SizedBox(width: 12),
Expanded(
child: RichText(
text: TextSpan(
style: const TextStyle(fontSize: 14, color: Colors.black87, height: 1.4),
children: [
TextSpan(text: "$title: ", style: const TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: desc),
],
),
),
),
],
);
}
Future<void> _puliziaIngresso() async {
if (GlobalData.idScambioTemporaneo == null && GlobalData.idSessione != null) {
GlobalData.idScambioTemporaneo = GlobalData.idSessione;
}
if (GlobalData.idScambioTemporaneo == null) {
if (GlobalData.latoCorrente == 'A') GlobalData.resetB(); else GlobalData.resetA();
}
if (mounted) _verificaStatoPostScambio();
}
void _verificaStatoPostScambio() {
bool datiOk = false;
if (GlobalData.latoCorrente == 'A') {
datiOk = GlobalData.Cognome_contraente_B.trim().isNotEmpty && GlobalData.Targa_B.trim().isNotEmpty;
} else {
datiOk = GlobalData.Cognome_contraente_A.trim().isNotEmpty && GlobalData.Targa_A.trim().isNotEmpty;
}
if (mounted) {
setState(() {
if (datiOk) {
_scambioEffettuato = true;
_datiPresenti = true;
_statusText = "Dati ricevuti. Generazione anteprima...";
_statusColor = Colors.blue.shade800;
_statusIcon = Icons.pending_actions;
_generaDocumenti();
_attivaAscoltoStanza();
} else {
_resetStatiUI();
}
});
}
}
void _resetStatiUI() {
_scambioEffettuato = false;
_datiPresenti = false;
_ioHoApprovato = false;
_tuttiHannoApprovato = false;
_statusText = "Esegui lo Scambio Dati per iniziare.";
_statusColor = Colors.orange.shade800;
_statusIcon = Icons.warning_amber_rounded;
_filePdfReale = null;
_immagineAnteprima = null;
}
void _attivaAscoltoStanza() {
String? idDaAscoltare = GlobalData.idScambioTemporaneo ?? GlobalData.idSessione;
if (idDaAscoltare == null || _roomSubscription != null) return;
_roomSubscription = FirebaseFirestore.instance
.collection('scambi_cid')
.doc(idDaAscoltare)
.snapshots()
.listen((snapshot) async {
if (!snapshot.exists) {
if (_ioHoApprovato) {
_roomSubscription?.cancel();
_roomSubscription = null;
return;
}
if (!_staCancellando && !_cancellazioneAvviataDaMe && mounted) {
_gestisciCancellazioneAltrui();
}
return;
}
final data = snapshot.data();
if (data == null) return;
if (data['status'] == 'retry') {
if (!_cancellazioneAvviataDaMe) _gestisciCancellazioneAltrui();
return;
}
bool appA = data['approved_A'] == true;
bool appB = data['approved_B'] == true;
if (appA && appB) {
if (mounted) {
setState(() {
_tuttiHannoApprovato = true;
_ioHoApprovato = true;
_statusText = "DATI APPROVATI!\nPDF creato, procedi con il salvataggio o l'invio";
_statusColor = Colors.green.shade800;
_statusIcon = Icons.check_circle;
});
String? id = GlobalData.idSessione ?? GlobalData.idScambioTemporaneo;
if (id != null) FirebaseFirestore.instance.collection('scambi_cid').doc(id).delete().catchError((_){});
}
}
else if (_ioHoApprovato) {
if (mounted) {
setState(() {
_statusText = "Hai approvato. In attesa dell'altro utente...";
_statusColor = Colors.amber.shade800;
_statusIcon = Icons.hourglass_top;
});
}
}
});
}
Future<void> _eseguiPuliziaFirebase({required bool notificaAltri}) async {
setState(() {
_isLoading = true;
_cancellazioneAvviataDaMe = true;
});
await _roomSubscription?.cancel();
_roomSubscription = null;
Set<String> 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<void> _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<void> _abbandonaScambioEHome() async {
await _eseguiPuliziaFirebase(notificaAltri: true);
GlobalData.reset();
if (mounted) {
setState(() => _isLoading = false);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (c) => const HomeScreen()),
(route) => false
);
}
}
Future<void> _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<void> _concludiEHome() async {
await _eseguiPuliziaFirebase(notificaAltri: false);
GlobalData.reset();
if (mounted) {
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (c) => const HomeScreen()), (r) => false);
}
}
void _resetDatiLocali() {
if (GlobalData.latoCorrente == 'A') GlobalData.resetB(); else GlobalData.resetA();
GlobalData.idScambioTemporaneo = null;
GlobalData.idSessione = null;
}
void _gestisciCancellazioneAltrui() {
_roomSubscription?.cancel();
_roomSubscription = null;
if (mounted) {
Navigator.of(context).popUntil((route) => route.isFirst || route.settings.name == null);
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24.0)),
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
icon: Icon(Icons.warning_amber_rounded, size: 60, color: Colors.amber.shade800),
iconPadding: const EdgeInsets.only(top: 24, bottom: 16),
title: Text("Attenzione", textAlign: TextAlign.center, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 22, color: Colors.amber.shade900)),
content: const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Text(
"L'altro utente ha deciso di modificare i propri dati o non ha accettato i tuoi.\n\nSarai riportato alla schermata iniziale dove potrai eventualmente apporre modifiche.",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, height: 1.4, color: Colors.black87),
),
),
actionsPadding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
actions: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.pop(ctx);
_resetDatiLocali();
if (GlobalData.latoCorrente == 'A') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp1_5Screen()));
} else {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (c) => const Comp6_7Screen()));
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.amber.shade800, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), elevation: 0),
child: const Text("HO CAPITO", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16))
),
)
],
),
);
}
}
Future<void> _generaDocumenti() async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
final List<int> pdfBytes = await PdfEngine.generaDocumentoCai();
if (pdfBytes.isEmpty) throw Exception("PDF vuoto");
final appDocDir = await getApplicationDocumentsDirectory();
final file = File('${appDocDir.path}/CID_${DateTime.now().millisecondsSinceEpoch}.pdf');
await file.writeAsBytes(pdfBytes, flush: true);
Uint8List? anteprima;
await for (final page in Printing.raster(Uint8List.fromList(pdfBytes), pages: [0], dpi: 150)) {
anteprima = await page.toPng(); break;
}
if (mounted) {
setState(() { _filePdfReale = file; _immagineAnteprima = anteprima; _isLoading = false; });
}
} catch (e) {
if (mounted) setState(() => _isLoading = false);
}
}
Future<void> _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<void> _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<String> listaCC = [];
if (contattoChiScrive.contains("@")) listaCC.add(contattoChiScrive);
String oggetto = "DENUNCIA SINISTRO - Polizza n. $polizzaChiScrive - Targa $targaChiScrive";
String corpo = "Spett.le Compagnia,\n\n"
"Con la presente inoltro in allegato il modulo CAI relativo al sinistro avvenuto in data ${GlobalData.data_incidente} alle ore ${GlobalData.ora} nel comune di ${GlobalData.luogo}.\n\n"
"Rimaniamo in attesa dell'apertura del fascicolo.\n\n"
"Cordiali saluti,\n$firmaChiScrive\nContatto: $contattoChiScrive";
final Email email = Email(
subject: oggetto,
body: corpo,
recipients: emailDestinatario.isNotEmpty ? [emailDestinatario] : [],
cc: listaCC,
attachmentPaths: [_filePdfReale!.path],
isHTML: false,
);
await FlutterEmailSender.send(email);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Nessuna app Mail predefinita trovata. Apro la condivisione..."), duration: Duration(seconds: 3), backgroundColor: Colors.orange));
_apriCondivisione(context);
}
}
}
Future<void> _apriCondivisione(BuildContext context) async {
if (_filePdfReale == null) return;
final box = context.findRenderObject() as RenderBox?;
await Share.shareXFiles(
[XFile(_filePdfReale!.path, mimeType: 'application/pdf')],
subject: 'Modulo CAI',
text: 'Ecco il modulo CAI compilato.',
sharePositionOrigin: box != null ? (box.localToGlobal(Offset.zero) & box.size) : null,
);
}
Future<void> _salvaPdfLocale(BuildContext context) async {
await _apriCondivisione(context);
}
@override
void dispose() {
_roomSubscription?.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
bool pdfPronto = !_isLoading && _filePdfReale != null && _immagineAnteprima != null;
bool abilitaAnteprima = _scambioEffettuato && _datiPresenti && pdfPronto;
bool abilitaFinali = _tuttiHannoApprovato && pdfPronto;
String testoAnteprima = !_scambioEffettuato ? "2. ANTEPRIMA (Prima fai Scambio)" :
(_ioHoApprovato ? "ANTEPRIMA (IN ATTESA...)" : "2. APRI ANTEPRIMA E APPROVA");
if (_tuttiHannoApprovato) testoAnteprima = "ANTEPRIMA (COMPLETATA)";
return Container(
width: double.infinity,
height: double.infinity,
decoration: BoxDecoration(
color: const Color(0xFFF0F4F8),
image: DecorationImage(
image: const AssetImage('assets/sfondo_mappa.jpg'),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
const Color(0xFFF0F4F8).withOpacity(0.6),
BlendMode.lighten,
),
),
),
child: PopScope(
canPop: false,
onPopInvoked: (didPop) async {
if (didPop) return;
if (_ioHoApprovato) _concludiEHome(); else _tornaIndietroConPulizia();
},
child: Scaffold(
backgroundColor: Colors.transparent,
extendBodyBehindAppBar: true,
appBar: AppBar(
title: const Text("Invio e Salvataggio", style: TextStyle(fontWeight: FontWeight.w800, fontSize: 20)),
centerTitle: true,
backgroundColor: Colors.blue.shade900.withOpacity(0.95),
foregroundColor: Colors.white,
elevation: 10,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: _ioHoApprovato ? _concludiEHome : _tornaIndietroConPulizia
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(20))),
),
body: Stack(children: [
SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 25, vertical: 20),
child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
_buildStatusCard(),
const SizedBox(height: 20),
// VA DIRETTO ALLA PAGINA DI SCAMBIO
_btn("1. SCAMBIO DATI", Icons.sync_alt, Colors.orange.shade800, onTap: _vaiAScambioDati, disabled: _ioHoApprovato),
const SizedBox(height: 20),
_btn(testoAnteprima, Icons.visibility, _statusColor, onTap: abilitaAnteprima ? _apriAnteprimaSchermoIntero : null, disabled: !abilitaAnteprima),
const SizedBox(height: 8),
Divider(color: Colors.white.withOpacity(0.5), thickness: 1),
const SizedBox(height: 8),
Builder(builder: (ctx) => _btn("SALVA SUL DISPOSITIVO", Icons.save_alt, Colors.green.shade700, onTap: abilitaFinali ? () => _salvaPdfLocale(ctx) : null, disabled: !abilitaFinali)),
const SizedBox(height: 20),
Builder(builder: (ctx) => _btn("INVIA ALL'ASSICURAZIONE", Icons.send_rounded, Colors.green.shade700, onTap: abilitaFinali ? () => _inviaMailConAllegato(ctx) : null, disabled: !abilitaFinali)),
const SizedBox(height: 40),
_btn(
_tuttiHannoApprovato ? "TORNA ALLA HOME" : "CANCELLA TUTTO E ESCI",
_tuttiHannoApprovato ? Icons.home : Icons.delete_sweep,
_tuttiHannoApprovato ? Colors.green.shade800 : Colors.red.shade900,
onTap: _tuttiHannoApprovato ? _concludiEHome : _abbandonaScambioEHome,
disabled: false
),
const SizedBox(height: 30)
]))),
if (_isLoading)
Container(color: Colors.black54, child: const Center(child: Column(mainAxisSize: MainAxisSize.min, children: [CircularProgressIndicator(color: Colors.white), SizedBox(height: 20), Text("Elaborazione in corso...", style: TextStyle(color: Colors.white))]))),
]),
),
),
);
}
Widget _btn(String label, IconData icon, Color color, {VoidCallback? onTap, bool disabled = false}) {
bool on = onTap != null && !disabled;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: on ? [BoxShadow(color: Colors.black.withOpacity(0.3), offset: const Offset(0, 4), blurRadius: 5)] : [],
),
child: ElevatedButton(
onPressed: on ? onTap : null,
style: ElevatedButton.styleFrom(
backgroundColor: on ? color : Colors.grey,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 20),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
child: Row(children: [
Icon(icon, size: 28), const SizedBox(width: 20),
Expanded(child: Text(label, textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16))),
const Icon(Icons.lock, size: 20, color: Colors.transparent)
])
),
);
}
Widget _buildStatusCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white.withOpacity(0.9), borderRadius: BorderRadius.circular(16), border: Border.all(color: _statusColor, width: 2), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 6, offset: const Offset(0, 3))]),
child: Row(children: [
Icon(_statusIcon, color: _statusColor, size: 36), const SizedBox(width: 15),
Expanded(child: Text(_statusText, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: _statusColor)))
])
);
}
}
class ImageViewerScreen extends StatelessWidget {
final Uint8List imageBytes;
final File pdfFile;
final bool isAlreadyApproved;
final Function onConfirmCorrection;
final Function onConfirmApproval;
const ImageViewerScreen({super.key, required this.imageBytes, required this.pdfFile, required this.isAlreadyApproved, required this.onConfirmCorrection, required this.onConfirmApproval});
Future<void> _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<void> _askApproval(BuildContext context) async { Navigator.pop(context); onConfirmApproval(); }
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(title: const Text("Verifica Dati"), backgroundColor: Colors.black, foregroundColor: Colors.white),
body: Column(children: [
Expanded(child: InteractiveViewer(minScale: 0.5, maxScale: 4.0, child: Center(child: Container(color: Colors.white, child: Image.memory(imageBytes, fit: BoxFit.contain))))),
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(color: Colors.white, boxShadow: [BoxShadow(color: Colors.black12, blurRadius: 10, offset: const Offset(0, -2))]),
child: SafeArea(
child: Row(children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _askCorrection(context),
icon: Icon(isAlreadyApproved ? Icons.arrow_back : Icons.edit),
label: Text(isAlreadyApproved ? "INDIETRO" : "CORREGGI"),
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange.shade800, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
),
),
const SizedBox(width: 16),
Expanded(
child: ElevatedButton.icon(
onPressed: isAlreadyApproved ? null : () => _askApproval(context),
icon: isAlreadyApproved ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) : const Icon(Icons.check_circle),
label: Text(isAlreadyApproved ? "IN ATTESA..." : "APPROVA"),
style: ElevatedButton.styleFrom(backgroundColor: isAlreadyApproved ? Colors.grey : Colors.green.shade700, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10))),
),
),
]),
),
)
]),
);
}
}
=== FILE: lib/models.dart ===
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'dart:math' as math; // Serve per i calcoli della freccia
// --- PAINTER PER IL DISEGNO (A SCHERMO E SU PDF) ---
class PainterV40 extends CustomPainter {
final List<TrattoPenna> tr;
final List<ElementoGrafico> el;
PainterV40(this.tr, this.el);
@override
void paint(Canvas canvas, Size size) {
// Stile della penna (Strada/Linee)
Paint pStrada = Paint()
..color = Colors.black
..strokeWidth = 3.0
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
// 1. DISEGNO TRATTI (PENNA E FRECCE)
for (var t in tr) {
if (t.punti.length > 1) {
Path path = Path()..moveTo(t.punti[0].dx, t.punti[0].dy);
for (var pt in t.punti) path.lineTo(pt.dx, pt.dy);
canvas.drawPath(path, pStrada);
// Se è una freccia, disegna la punta alla fine
if (t.tipo == 'freccia') {
// Prendi gli ultimi due punti per calcolare l'angolazione
_disegnaPunta(canvas, t.punti[t.punti.length - 2], t.punti.last, pStrada);
}
}
}
// 2. DISEGNO ELEMENTI (AUTO E TESTO)
for (var e in el) {
canvas.save();
// Sposta e ruota il canvas nella posizione dell'elemento
canvas.translate(e.posizione.dx, e.posizione.dy);
canvas.rotate(e.rotazione);
if (e.tipo == 'testo') {
_disegnaTesto(canvas, e.label ?? "");
} else {
_disegnaAuto(canvas, e.tipo == 'autoA' ? 'A' : 'B', e.tipo == 'autoA' ? Colors.blue : Colors.orange);
}
canvas.restore();
}
}
// Disegna il rettangolo dell'auto con la lettera
void _disegnaAuto(Canvas canvas, String lettera, Color colore) {
Paint p = Paint()..color = colore;
// Auto centrata (70x40 px)
Rect r = Rect.fromCenter(center: Offset.zero, width: 70, height: 40);
canvas.drawRect(r, p);
// Bordo nero auto
canvas.drawRect(r, Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 2);
// Lettera centrata
final tp = TextPainter(
text: TextSpan(text: lettera, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)),
textDirection: TextDirection.ltr
)..layout();
tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2));
}
// Disegna etichette di testo
void _disegnaTesto(Canvas canvas, String txt) {
final tp = TextPainter(
text: TextSpan(text: txt, style: const TextStyle(color: Colors.black, fontSize: 16, fontWeight: FontWeight.bold, backgroundColor: Colors.white70)),
textDirection: TextDirection.ltr
)..layout();
tp.paint(canvas, Offset(-tp.width / 2, -tp.height / 2));
}
// Calcola e disegna la punta della freccia
void _disegnaPunta(Canvas canvas, Offset p1, Offset p2, Paint paint) {
double angle = (p2 - p1).direction;
// Disegna due linee inclinate rispetto alla direzione finale
canvas.drawLine(p2, p2 - Offset.fromDirection(angle - 0.5, 15), paint);
canvas.drawLine(p2, p2 - Offset.fromDirection(angle + 0.5, 15), paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
// --- MODELLO DATI: TRATTO PENNA ---
class TrattoPenna {
List<Offset> punti;
String tipo; // 'penna' o 'freccia'
TrattoPenna(this.punti, {this.tipo = 'penna'});
// Metodo utile per la cancellazione (hit test)
bool contiene(Offset p) {
for (var punto in punti) {
if ((p - punto).distance < 20.0) return true;
}
return false;
}
Map<String, dynamic> toMap() => {
'punti': punti.map((p) => {'dx': p.dx, 'dy': p.dy}).toList(),
'tipo': tipo,
};
factory TrattoPenna.fromMap(Map<String, dynamic> map) => TrattoPenna(
(map['punti'] as List).map((p) => Offset(p['dx'], p['dy'])).toList(),
tipo: map['tipo'] ?? 'penna',
);
}
// --- MODELLO DATI: ELEMENTO GRAFICO (AUTO/TESTO) ---
class ElementoGrafico {
Offset posizione;
String tipo; // 'autoA', 'autoB', 'testo'
double rotazione;
String? label;
ElementoGrafico(this.posizione, this.tipo, {this.rotazione = 0, this.label});
Map<String, dynamic> toMap() => {
'pos': {'dx': posizione.dx, 'dy': posizione.dy},
'tipo': tipo,
'rot': rotazione,
'label': label,
};
factory ElementoGrafico.fromMap(Map<String, dynamic> map) => ElementoGrafico(
Offset(map['pos']['dx'], map['pos']['dy']),
map['tipo'],
rotazione: (map['rot'] as num?)?.toDouble() ?? 0,
label: map['label'],
);
bool contiene(Offset p) => (p - posizione).distance < 35;
// --- METODO CRUCIALE PER IL PDF ---
// Genera un'immagine PNG ritagliata e ottimizzata del grafico
static Future<Uint8List?> fondiGraficoDinamica(List<dynamic> trattiRaw, List<dynamic> elementiRaw) async {
// Conversione sicura dei tipi (nel caso arrivino come dynamic da GlobalData)
List<TrattoPenna> tratti = trattiRaw.cast<TrattoPenna>();
List<ElementoGrafico> elementi = elementiRaw.cast<ElementoGrafico>();
if (tratti.isEmpty && elementi.isEmpty) return null;
// 1. Calcolo Bounding Box (i confini del disegno)
double minX = double.infinity, minY = double.infinity;
double maxX = double.negativeInfinity, maxY = double.negativeInfinity;
void checkPoint(Offset p) {
if (p.dx < minX) minX = p.dx;
if (p.dx > maxX) maxX = p.dx;
if (p.dy < minY) minY = p.dy;
if (p.dy > maxY) maxY = p.dy;
}
for (var t in tratti) { for (var p in t.punti) checkPoint(p); }
for (var e in elementi) checkPoint(e.posizione);
// Se non ci sono dimensioni valide, esci
if (minX == double.infinity) return null;
// Aggiungiamo margine (padding) bianco intorno
const double pad = 40.0;
double width = (maxX - minX) + (pad * 2);
double height = (maxY - minY) + (pad * 2);
// 2. Disegno su Canvas off-screen
final recorder = ui.PictureRecorder();
// Crea canvas delle dimensioni esatte
final canvas = ui.Canvas(recorder, Rect.fromLTWH(0, 0, width, height));
// Sfondo Bianco (Copre la griglia del modulo sottostante)
canvas.drawRect(Rect.fromLTWH(0, 0, width, height), Paint()..color = Colors.white);
// Sposta l'origine del canvas per centrare il disegno ed eliminare lo spazio vuoto in alto/sinistra
canvas.translate(-minX + pad, -minY + pad);
// Usa il painter esistente per ridisegnare tutto
final painter = PainterV40(tratti, elementi);
painter.paint(canvas, Size(width, height));
// 3. Conversione in PNG
final picture = recorder.endRecording();
final img = await picture.toImage(width.toInt(), height.toInt());
final pngBytes = await img.toByteData(format: ui.ImageByteFormat.png);
return pngBytes?.buffer.asUint8List();
}
}