9572 lines
No EOL
372 KiB
Text
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();
|
|
}
|
|
} |