import 'package:animations/animations.dart'; import 'package:fforte/enums/databases.dart'; import 'package:fforte/screens/addCam/exceptions/location_disabled_exception.dart'; import 'package:fforte/screens/addCam/exceptions/location_forbidden_exception.dart'; import 'package:fforte/screens/addCam/services/geolocator_service.dart'; import 'package:fforte/screens/excursion/exceptions/need_always_location_exception.dart'; import 'package:fforte/screens/excursion/widgets/anzahlen.dart'; import 'package:fforte/screens/excursion/widgets/bima_nutzer.dart'; import 'package:fforte/screens/excursion/widgets/hinweise.dart'; import 'package:fforte/screens/excursion/widgets/hund_u_leine.dart'; import 'package:fforte/screens/excursion/widgets/letzter_niederschlag.dart'; import 'package:fforte/screens/excursion/widgets/spur_gefunden.dart'; import 'package:fforte/screens/excursion/widgets/strecke_u_spurbedingungen.dart'; import 'package:fforte/screens/excursion/widgets/tracking.dart'; import 'package:fforte/screens/helper/add_entries_dialog_helper.dart'; import 'package:fforte/screens/helper/snack_bar_helper.dart'; import 'package:fforte/screens/sharedMethods/check_required.dart'; import 'package:fforte/screens/sharedMethods/save_template.dart'; import 'package:fforte/screens/sharedWidgets/datum.dart'; import 'package:fforte/screens/sharedWidgets/var_text_field.dart'; import 'package:fforte/l10n/app_localizations.dart'; import 'package:fforte/services/tracking_service.dart'; import 'package:flutter/material.dart'; import 'package:geolocator/geolocator.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// Main widget for managing excursion data entry /// Provides a multi-step form interface for recording excursion details class ExcursionMain extends StatefulWidget { /// Whether this is a template excursion final bool isTemplate; /// Whether the excursion data has been sent to the server final bool isSent; /// Existing excursion data for editing final Map? existingData; const ExcursionMain({ super.key, this.isTemplate = false, this.isSent = false, this.existingData, }); @override State createState() => _ExcursionMainState(); } /// State class for the main excursion screen class _ExcursionMainState extends State { /// Current step in the form int currentStep = 0; /// Whether this is a template excursion late bool isTemplate; /// Current GPS position Position currentPosition = Position( longitude: 10.0, latitude: 51.0, timestamp: DateTime.now(), accuracy: 0.0, altitude: 0.0, heading: 0.0, speed: 0.0, speedAccuracy: 0.0, altitudeAccuracy: 0.0, headingAccuracy: 0.0, ); /// Whether to show extended BImA information bool bimaExtended = false; /// Map of all form fields and their controllers /// Each field has a controller and required flag Map> rmap = { "ID": {"controller": TextEditingController(), "required": false}, // Step 1 - Basic Information "Datum": {"controller": TextEditingController(), "required": false}, "Rudel": {"controller": TextEditingController(), "required": false}, "Teilnehmer": {"controller": TextEditingController(), "required": false}, "Dauer": {"controller": TextEditingController(), "required": false}, "MHund": {"controller": TextEditingController(), "required": false}, "MLeine": {"controller": TextEditingController(), "required": false}, "BLand": {"controller": TextEditingController(), "required": false}, "Lkr": {"controller": TextEditingController(), "required": false}, "BeiOrt": {"controller": TextEditingController(), "required": false}, "BimaNr": {"controller": TextEditingController(), "required": false}, "BimaName": {"controller": TextEditingController(), "required": false}, "BimaNutzer": {"controller": TextEditingController(), "required": false}, "BimaAGV": {"controller": TextEditingController(), "required": false}, // Step 2 - Environmental Conditions and Observations "Weg": {"controller": TextEditingController(), "required": false}, "Wetter": {"controller": TextEditingController(), "required": false}, "Temperat": {"controller": TextEditingController(), "required": false}, "RegenVor": {"controller": TextEditingController(), "required": false}, "KmAuto": {"controller": TextEditingController(), "required": false}, "KmFuss": {"controller": TextEditingController(), "required": false}, "KmRad": {"controller": TextEditingController(), "required": false}, "KmTotal": {"controller": TextEditingController(), "required": false}, "KmAuProz": {"controller": TextEditingController(), "required": false}, "KmFuProz": {"controller": TextEditingController(), "required": false}, "KmRaProz": {"controller": TextEditingController(), "required": false}, // Track Findings "SpGut": {"controller": TextEditingController(), "required": false}, "SpMittel": {"controller": TextEditingController(), "required": false}, "SpSchlecht": {"controller": TextEditingController(), "required": false}, "SpurFund": {"controller": TextEditingController(), "required": false}, "SpurLang": {"controller": TextEditingController(), "required": false}, "SpurTiere": {"controller": TextEditingController(), "required": false}, "SpSicher": {"controller": TextEditingController(), "required": false}, "WelpenSp": {"controller": TextEditingController(), "required": false}, "WelpenAnz": {"controller": TextEditingController(), "required": false}, "WpSicher": {"controller": TextEditingController(), "required": false}, // Sample Counts "LosungGes": {"controller": TextEditingController(), "required": false}, "LosungAnz": {"controller": TextEditingController(), "required": false}, "LosungGen": {"controller": TextEditingController(), "required": false}, "UrinAnz": {"controller": TextEditingController(), "required": false}, "UrinGen": {"controller": TextEditingController(), "required": false}, "OestrAnz": {"controller": TextEditingController(), "required": false}, "OestrGen": {"controller": TextEditingController(), "required": false}, "HaarAnz": {"controller": TextEditingController(), "required": false}, "HaarGen": {"controller": TextEditingController(), "required": false}, "LosungKm": {"controller": TextEditingController(), "required": false}, "GenetiKm": {"controller": TextEditingController(), "required": false}, "Hinweise": {"controller": TextEditingController(), "required": false}, // Step 3 - Notes and Communication "Bemerk": {"controller": TextEditingController(), "required": false}, "IntKomm": {"controller": TextEditingController(), "required": false}, "FallNum": {"controller": TextEditingController(), "required": false}, "Sent": {"controller": TextEditingController(), "required": false}, }; @override void initState() { // Initialize location services GeolocatorService.deteterminePosition( alwaysOnNeeded: false, ).then((result) => currentPosition = result).catchError((error) async { if (error is LocationDisabledException) { if (mounted) { SnackBarHelper.showSnackBarMessage( context, AppLocalizations.of(context)!.locationDisabled, ); } } else if (error is LocationForbiddenException) { if (mounted) { SnackBarHelper.showSnackBarMessage( context, AppLocalizations.of(context)!.locationForbidden, ); } } else if (error is NeedAlwaysLocation) { if (mounted) { bool reload = await AddEntriesDialogHelper.locationSettingsDialog(context); if (reload) { GeolocatorService.deteterminePosition() .then((res) => currentPosition = res) .catchError((error) { return currentPosition; }); } } } return currentPosition; }); // Load existing data or set defaults if (widget.existingData?.isNotEmpty ?? false) { for (var key in widget.existingData!.keys) { rmap[key]!["controller"]!.text = widget.existingData?[key].toString() ?? ""; } } else { // Set default state and date SharedPreferences.getInstance().then((prefs) { rmap["BLand"]!["controller"]!.text = prefs.getString('bLand') ?? ""; }); rmap["Datum"]!["controller"]!.text = DateTime.now().toString().split(" ").first; rmap["Sent"]!["controller"]!.text = "0"; } isTemplate = widget.isTemplate; super.initState(); } @override void dispose() { // Dispose all controllers for (String key in rmap.keys) { rmap[key]!["controller"].dispose(); } super.dispose(); } /// Get all form field values as a map /// @return Map Map of field names to values Map getFieldsText() { Map puff = {}; for (var itemKey in rmap.keys) { puff[itemKey] = rmap[itemKey]!["controller"]!.text; } return puff; } @override Widget build(BuildContext context) { /// Build the steps for the form /// @return List List of form steps List getSteps() => [ Step( title: Text(AppLocalizations.of(context)!.dateandtime), content: Column( children: [ // Date picker Datum( initDatum: DateTime.now(), onDateChanged: (date) { rmap["Datum"]!["controller"]!.text = date.toString().split(" ").first; }, name: AppLocalizations.of(context)!.date, ), const SizedBox(height: 10), // Pack/Group field VarTextField( textController: rmap["Rudel"]!["controller"]!, localization: AppLocalizations.of(context)!.rudel, dbName: "Rudel", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), // Participants field VarTextField( textController: rmap["Teilnehmer"]!["controller"]!, localization: AppLocalizations.of(context)!.teilnehmer, dbName: "Teilnehmer", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), // Duration field VarTextField( textController: rmap["Dauer"]!["controller"]!, localization: AppLocalizations.of(context)!.dauer, dbName: "Dauer", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 20), // Dog and leash selection HundULeine( mHund: rmap["MHund"]!["controller"]!, mLeine: rmap["MLeine"]!["controller"]!, ), const SizedBox(height: 10), // State field VarTextField( textController: rmap["BLand"]!["controller"]!, localization: AppLocalizations.of(context)!.bland, dbName: "BLand", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), // County field VarTextField( textController: rmap["Lkr"]!["controller"]!, localization: AppLocalizations.of(context)!.lkr, dbName: "Lkr", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), // Nearby location field VarTextField( textController: rmap["BeiOrt"]!["controller"]!, localization: AppLocalizations.of(context)!.beiort, dbName: "BeiOrt", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), // BImA information section const Divider(), const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.all(Radius.circular(10)), child: ExpansionPanelList( expansionCallback: ((int index, bool isExpanded) => setState(() => bimaExtended = isExpanded)), expandedHeaderPadding: EdgeInsets.all(0), children: [ ExpansionPanel( isExpanded: bimaExtended, canTapOnHeader: true, headerBuilder: (context, bool isOpen) => Padding( padding: const EdgeInsets.only(left: 15), child: Align( alignment: Alignment.centerLeft, child: Text( "BImA", style: Theme.of(context).textTheme.bodyLarge, ), ), ), body: Padding( padding: const EdgeInsets.all(15), child: Column( children: [ const SizedBox(height: 10), VarTextField( textController: rmap["BimaNr"]!["controller"]!, localization: AppLocalizations.of(context)!.bimaNr, dbName: "BimaNr", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), VarTextField( textController: rmap["BimaName"]!["controller"]!, localization: AppLocalizations.of(context)!.bimaName, dbName: "BimaName", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), BimaNutzer( onBimaNutzerChanged: (value) { setState(() { rmap["BimaNutzer"]!["controller"]!.text = value; }); }, ), const SizedBox(height: 10), VarTextField( textController: rmap["BimaAGV"]!["controller"]!, localization: AppLocalizations.of(context)!.bimaAGV, dbName: "BimaAGV", required: false, dbDesignation: DatabasesEnum.excursion, ), ], ), ), ), ], ), ), ], ), ), Step( title: Text(AppLocalizations.of(context)!.umstaendeUndAktionen), content: Column( children: [ // GPS tracking button ElevatedButton( onPressed: () async { // Check for always permission before starting tracking LocationPermission permission = await Geolocator.checkPermission(); if (permission != LocationPermission.always) { if (context.mounted) { bool? shouldContinue = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(AppLocalizations.of(context)! .trackingPermissionTitle), content: Text(AppLocalizations.of(context)! .trackingPermissionContent), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: Text(AppLocalizations.of(context)!.cancel), ), TextButton( onPressed: () async { await Geolocator.openAppSettings(); if (context.mounted) Navigator.of(context).pop(true); }, child: Text( AppLocalizations.of(context)!.openSettings), ), ], ), ); if (shouldContinue != true) { return; } // Wait for user to change settings and return // Try checking the permission multiple times for (int i = 0; i < 5; i++) { await Future.delayed(const Duration(seconds: 1)); if (!context.mounted) return; permission = await Geolocator.checkPermission(); if (permission == LocationPermission.always) { break; } // If this is the last attempt and we still don't have permission if (i == 4 && permission != LocationPermission.always) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context)! .permissionNotGranted), duration: const Duration(seconds: 3), ), ); } return; } } } } if (context.mounted) { await Navigator.push(context, MaterialPageRoute( builder: (context) { return Tracking( weg: rmap["Weg"]!["controller"]!, startPosition: currentPosition, ); }, )); } setState(() {}); }, child: Text(AppLocalizations.of(context)!.trackingAnAusschalten), ), const SizedBox(height: 10), // Weather field VarTextField( textController: rmap["Wetter"]!["controller"]!, localization: AppLocalizations.of(context)!.wetter, dbName: "Wetter", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), // Temperature field VarTextField( textController: rmap["Temperat"]!["controller"]!, localization: AppLocalizations.of(context)!.temperatur, dbName: "Temperat", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 10), // Last precipitation selection LetzterNiederschlag( controller: rmap["RegenVor"]!["controller"]!), const SizedBox(height: 20), // Distance and track conditions StreckeUSpurbedingungen( kmAutoController: rmap["KmAuto"]!["controller"]!, kmFussController: rmap["KmFuss"]!["controller"]!, kmRadController: rmap["KmRad"]!["controller"]!, spGutController: rmap["SpGut"]!["controller"]!, spMittelController: rmap["SpMittel"]!["controller"]!, spSchlechtController: rmap["SpSchlecht"]!["controller"]!, ), const SizedBox(height: 20), const Divider(), // Track findings SpurGefunden( spurFund: rmap["SpurFund"]!["controller"]!, spurLang: rmap["SpurLang"]!["controller"]!, spurTiere: rmap["SpurTiere"]!["controller"]!, spSicher: rmap["SpSicher"]!["controller"]!, welpenSp: rmap["WelpenSp"]!["controller"]!, welpenAnz: rmap["WelpenAnz"]!["controller"]!, wpSicher: rmap["WpSicher"]!["controller"]!, ), const Divider(), const SizedBox(height: 20), // Sample counts Anzahlen( losungAnz: rmap["LosungAnz"]!["controller"]!, losungGes: rmap["LosungGes"]!["controller"]!, losungGen: rmap["LosungGen"]!["controller"]!, urinAnz: rmap["UrinAnz"]!["controller"]!, urinGen: rmap["UrinGen"]!["controller"]!, oestrAnz: rmap["OestrAnz"]!["controller"]!, oestrGen: rmap["OestrGen"]!["controller"]!, haarAnz: rmap["HaarAnz"]!["controller"]!, haarGen: rmap["HaarGen"]!["controller"]!, ), const SizedBox(height: 20), const Divider(), const SizedBox(height: 20), // Observations section Align( alignment: Alignment.bottomLeft, child: Text( AppLocalizations.of(context)!.hinweise, style: Theme.of(context).textTheme.titleMedium, ), ), Hinweise(hinweise: rmap["Hinweise"]!["controller"]!), ], ), ), Step( title: Text(AppLocalizations.of(context)!.intkomm), content: Column( children: [ // Remarks field VarTextField( textController: rmap["Bemerk"]!["controller"]!, localization: AppLocalizations.of(context)!.sonstbemerkungen, dbName: "Bemerk", required: false, dbDesignation: DatabasesEnum.excursion, ), const SizedBox(height: 20), // Internal communication field VarTextField( textController: rmap["IntKomm"]!["controller"]!, localization: AppLocalizations.of(context)!.intkomm, dbName: "IntKomm", required: false, dbDesignation: DatabasesEnum.excursion, ), ], ), ), ]; return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? res) async { if (didPop) { return; } // Show confirmation dialog final result = await showDialog( context: context, builder: (context) => AlertDialog( title: Text(AppLocalizations.of(context)!.leavePageTitle), content: Text(AppLocalizations.of(context)!.leavePageContent), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(0), child: Text(AppLocalizations.of(context)!.nein), ), TextButton( onPressed: () { saveTemplate(getFieldsText(), DatabasesEnum.excursion); Navigator.of(context).pop(1); }, child: Text(AppLocalizations.of(context)!.leaveAndSaveTemplate), ), TextButton( onPressed: () => Navigator.of(context).pop(2), child: Text(AppLocalizations.of(context)!.leaveWithoutSaving), ), ], ), ); if (result == null || result == 0) { return; } else if (result == 1) { // Save as template and leave if (context.mounted) { saveTemplate( getFieldsText(), DatabasesEnum.excursion, ); } TrackingService.resetInstance(); if (context.mounted) { Navigator.of(context).pop(); } } else { // Just leave without saving TrackingService.resetInstance(); if (context.mounted) { Navigator.of(context).pop(); } } }, child: Scaffold( appBar: AppBar( title: Text(AppLocalizations.of(context)!.excursion), actions: [ IconButton( onPressed: () async { if (context.mounted) { await Navigator.push(context, MaterialPageRoute( builder: (context) { return Tracking( weg: rmap["Weg"]!["controller"]!, startPosition: currentPosition, ); }, )); } }, icon: Image.asset( TrackingService().isTracking ? "assets/icons/tracking_on.png" : "assets/icons/tracking_off.png", width: 40, )), ], ), body: PageTransitionSwitcher( duration: const Duration(microseconds: 800), transitionBuilder: ( Widget child, Animation animation, Animation secondaryAnimation, ) { return SharedAxisTransition( animation: animation, secondaryAnimation: secondaryAnimation, transitionType: SharedAxisTransitionType.vertical, child: child, ); }, child: Stepper( key: ValueKey(currentStep), steps: getSteps(), currentStep: currentStep, onStepTapped: (value) { setState(() { currentStep = value; }); }, onStepContinue: () async { final isLastStep = currentStep == getSteps().length - 1; if (!isLastStep) { var res = await saveTemplate( getFieldsText(), DatabasesEnum.excursion, ); isTemplate = true; setState(() { rmap["ID"]!["controller"]!.text = res.toString(); currentStep += 1; }); } else { if (widget.isSent) { Navigator.pushNamedAndRemoveUntil( context, '/home', (route) => false, ); return; } bool empty = CheckRequired.checkRequired(rmap); // for debugging always false // empty = false; if (empty) { AddEntriesDialogHelper.showTemplateDialog( context, getFieldsText(), DatabasesEnum.excursion, ); return; } else { bool pop = await AddEntriesDialogHelper.showSaveOptionsDialog( context, getFieldsText(), isTemplate, DatabasesEnum.excursion, ); if (pop && context.mounted) Navigator.of(context).pop(); } } }, onStepCancel: () { if (currentStep == 0) { Navigator.pop(context); } else { setState(() { currentStep -= 1; }); } }, ), ), ), ); } }