import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:fforte/screens/addCam/cam_widgets.dart'; import 'package:fforte/methods/db_helper.dart'; import 'package:fforte/methods/http_request.dart'; import 'package:fforte/screens/sharedWidgets/datum.dart'; import 'package:fforte/screens/sharedWidgets/var_text_field.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:geolocator/geolocator.dart'; import 'package:latlong2/latlong.dart'; import 'package:animations/animations.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AddCamMain extends StatefulWidget { final bool isTemplate; final bool isFinished; final bool isSent; final Map? existingData; const AddCamMain( {super.key, this.isTemplate = false, this.existingData, this.isFinished = false, this.isSent = false}); @override State createState() => _AddCamMainState(); } class _AddCamMainState extends State { // var declaration int currentStep = 0; // bool isTemplate = false; TextEditingController cid = TextEditingController(); TextEditingController rudelC = TextEditingController(); TextEditingController adresse1C = TextEditingController(); TextEditingController adresse2C = TextEditingController(); TextEditingController adresse3C = TextEditingController(); TextEditingController bLandC = TextEditingController(); TextEditingController lkrC = TextEditingController(); TextEditingController beiOrtC = TextEditingController(); TextEditingController ortInfoC = TextEditingController(); TextEditingController ffTypC = TextEditingController(); TextEditingController kSchloNrC = TextEditingController(); TextEditingController auftragC = TextEditingController(); TextEditingController kontAbspC = TextEditingController(); TextEditingController sonstBemC = TextEditingController(); TextEditingController fKontakt1C = TextEditingController(); TextEditingController fKontakt2C = TextEditingController(); TextEditingController fKontakt3C = TextEditingController(); TextEditingController standortC = TextEditingController(); TextEditingController kTage1C = TextEditingController(); TextEditingController kTage2C = TextEditingController(); TextEditingController intKommC = TextEditingController(); TextEditingController betreuungC = TextEditingController(); String selectedStatus = 'aktiv'; String selectedFotoFilm = 'Foto'; String selectedMEZ = 'Sommerzeit'; String selectedPlatzung = ''; 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); DateTime? abbauDat; DateTime datum = DateTime.now(); DateTime? kontDat = DateTime.now(); DateTime? protoAm = DateTime.now(); Map getPlace() { Map place = { 'ID': widget.existingData?['ID'], 'CID': cid.text, 'Rudel': rudelC.text, 'Datum': datum.toString().split(" ").first, 'Adresse1': adresse1C.text, 'Adresse2': adresse2C.text, 'Adresse3': adresse3C.text, 'BLand': bLandC.text, 'Lkr': lkrC.text, 'BeiOrt': beiOrtC.text, 'OrtInfo': ortInfoC.text, 'Status': selectedStatus, 'FFTyp': ffTypC.text, 'FotoFilm': selectedFotoFilm, 'MEZ': selectedMEZ, 'Platzung': selectedPlatzung, 'KSchloNr': kSchloNrC.text, 'KontDat': kontDat.toString().split(" ").first, 'AbbauDat': abbauDat.toString().split(" ").first.replaceAll("null", ""), 'Auftrag': auftragC.text, 'KontAbsp': kontAbspC.text, 'SonstBem': sonstBemC.text, 'FKontakt1': fKontakt1C.text, 'FKontakt2': fKontakt2C.text, 'FKontakt3': fKontakt3C.text, 'Standort': standortC.text, 'KTage1': kTage1C.text, 'KTage2': kTage2C.text, 'ProtoAm': protoAm.toString().split(" ").first, 'IntKomm': intKommC.text, 'Betreuung': betreuungC.text, 'DECLNG': currentPosition.longitude, 'DECLAT': currentPosition.latitude, }; return place; } bool empty = false; // determine live position with checks for denied permission and turned off location service Future _deteterminePosition() async { bool locationEnabled; LocationPermission permissionGiven; locationEnabled = await Geolocator.isLocationServiceEnabled(); if (!locationEnabled && mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.locationDisabled))); return currentPosition; } permissionGiven = await Geolocator.checkPermission(); if (permissionGiven == LocationPermission.denied) { permissionGiven = await Geolocator.requestPermission(); if (permissionGiven == LocationPermission.denied && mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.locationForbidden))); return currentPosition; } } if (permissionGiven == LocationPermission.deniedForever && mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.locationForbidden))); return currentPosition; } return currentPosition = await Geolocator.getCurrentPosition(); } @override void initState() { super.initState(); // updates the currentPosition var after the _determine position has finished. Means user view updates with his live location _deteterminePosition().then((position) { setState(() { currentPosition = position; }); }); // If a template is edited this fills in the existing values if (widget.isTemplate || widget.isFinished && widget.existingData != null) { cid.text = widget.existingData!['CID'] ?? ""; rudelC.text = widget.existingData!['Rudel'] ?? ""; adresse1C.text = widget.existingData!['Adresse1'] ?? ""; adresse2C.text = widget.existingData!['Adresse2'] ?? ""; adresse3C.text = widget.existingData!['Adresse3'] ?? ""; bLandC.text = widget.existingData!['BLand'] ?? ""; lkrC.text = widget.existingData!['Lkr'] ?? ""; beiOrtC.text = widget.existingData!['BeiOrt'] ?? ""; ortInfoC.text = widget.existingData!['OrtInfo'] ?? ""; selectedStatus = widget.existingData!['Status'] ?? ""; ffTypC.text = widget.existingData!['FFTyp'] ?? ""; selectedFotoFilm = widget.existingData!['FotoFilm'] ?? ""; selectedMEZ = widget.existingData!['MEZ'] ?? ""; selectedPlatzung = widget.existingData!['Platzung'] ?? ""; kSchloNrC.text = widget.existingData!['KSchloNr'] ?? ""; datum = DateTime.parse(widget.existingData!['Datum']); kontDat = widget.existingData!['KontDat'] == "" ? null : DateTime.parse(widget.existingData!['KontDat']); abbauDat = widget.existingData!['AbbauDat'] == "" ? null : DateTime.parse(widget.existingData!['AbbauDat']); auftragC.text = widget.existingData!['Auftrag'] ?? ""; kontAbspC.text = widget.existingData!['KontAbsp'] ?? ""; sonstBemC.text = widget.existingData!['SonstBem'] ?? ""; fKontakt1C.text = widget.existingData!['FKontakt1'] ?? ""; fKontakt2C.text = widget.existingData!['FKontakt2'] ?? ""; fKontakt3C.text = widget.existingData!['FKontakt3'] ?? ""; standortC.text = widget.existingData!['Standort'] ?? ""; kTage1C.text = widget.existingData!['KTage1'].toString(); kTage2C.text = widget.existingData!['KTage2'].toString(); protoAm = widget.existingData!['ProtoAm'] == null ? null : DateTime.parse(widget.existingData!['ProtoAm']); intKommC.text = widget.existingData!['IntKomm'] ?? ""; betreuungC.text = widget.existingData!['Betreuung'] ?? ""; } } // Function to show the dialog where the user has to choose if he want to safe his values as a template Future showTemplateDialog(List emptyField) async { return showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: Text(AppLocalizations.of(context)!.fieldEmpty), content: SingleChildScrollView( child: ListBody(children: [Text(emptyField.join("; "))]), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: Text(AppLocalizations.of(context)!.cancel)), TextButton( onPressed: () { saveTemplate(); Navigator.pushNamedAndRemoveUntil( context, '/home', (route) => false); }, child: Text(AppLocalizations.of(context)!.template)) ], ); }); } Future _showServerErrorDialog() { bool isLoading = false; return showDialog( context: context, builder: (context) { return StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return AlertDialog( title: Text(AppLocalizations.of(context)!.servererrortitle), content: isLoading ? const SizedBox( height: 100, child: Center(child: CircularProgressIndicator())) : null, actions: [ if (!isLoading) TextButton( onPressed: () async { setState(() => isLoading = true); int errorCode = await _httpRequest(); setState(() => isLoading = false); if (errorCode != 201 && context.mounted) { _showServerErrorDialog(); } else { if (context.mounted) Navigator.pop(context); saveData(true); _showSuccessDialog(); } }, child: Text(AppLocalizations.of(context)!.sendagain)), if (!isLoading) TextButton( onPressed: () { Navigator.pop(context); }, child: Text(AppLocalizations.of(context)!.cancel)) ], ); }, ); }); } Future showSaveOptionsDialog() async { bool isLoading = false; return showDialog( context: context, barrierDismissible: false, // Verhindert das Schließen des Dialogs durch den Benutzer builder: (BuildContext context) { return StatefulBuilder( builder: (context, setState) { return AlertDialog( title: isLoading ? Text(AppLocalizations.of(context)!.loading) : Text(AppLocalizations.of(context)!.savemethod), content: isLoading ? const SizedBox( height: 100, child: Center(child: CircularProgressIndicator())) : null, actions: [ if (!isLoading) TextButton( onPressed: () async { setState(() => isLoading = true); saveTemplate(); Navigator.pushNamedAndRemoveUntil( context, '/home', (route) => false); }, child: Text(AppLocalizations.of(context)!.template)), if (!isLoading) TextButton( onPressed: () async { setState(() => isLoading = true); int errorCode = await _httpRequest(); setState(() => isLoading = false); if (errorCode != 201 || !context.mounted) { saveData(); _showServerErrorDialog(); } else { saveData(true); _showSuccessDialog(); } }, child: Text(AppLocalizations.of(context)!.sendtoserver)), if (!isLoading) TextButton( onPressed: () async { setState(() => isLoading = true); saveData(); saveFile(); setState(() => isLoading = false); }, child: Text(AppLocalizations.of(context)!.saveasfile)), if (!isLoading) TextButton( onPressed: () { saveData(); Navigator.pushNamedAndRemoveUntil( context, '/home', (route) => false); }, child: Text(AppLocalizations.of(context)!.justsave)), if (!isLoading) TextButton( onPressed: () { Navigator.pop(context); }, child: Text(AppLocalizations.of(context)!.cancel)), ], ); }, ); }); } Future _showSuccessDialog() async { return showDialog( context: context, builder: (context) { return AlertDialog( title: Text(AppLocalizations.of(context)!.successful), actions: [ /* TextButton( onPressed: () { Navigator.pop(context); }, child: Text(AppLocalizations.of(context)!.back)), */ TextButton( onPressed: () { Navigator.pushNamedAndRemoveUntil( context, '/home', (route) => false); }, child: Text(AppLocalizations.of(context)!.continueB)) ], ); }); } Future _httpRequest() async { Map place = getPlace(); HttpRequest method = HttpRequest(); await method.httpRequest(jsonEncode(place)); return method.errorCode; } Future saveFile() async { String? selectedDirectory = await FilePicker.platform.getDirectoryPath(); SharedPreferences prefs = await SharedPreferences.getInstance(); Map place = getPlace(); String jsonPlace = jsonEncode(place); if (selectedDirectory == null) { if (mounted) Navigator.pop(context); return; } await prefs.setString('saveDir', selectedDirectory); File file = File( '$selectedDirectory/${mounted ? AppLocalizations.of(context)!.justplace : const Text('')}-${standortC.text}.txt'); try { await file.writeAsString(jsonPlace); } catch (e) { if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(AppLocalizations.of(context)!.savefilefailed))); } return; } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Datei gespeichert in $selectedDirectory'))); } if (mounted) { Navigator.pushNamedAndRemoveUntil(context, '/home', (route) => false); } } // checks if required fields are not empty. If one is the name will be returned List validateData() { List emptyFields = []; Map fields = { AppLocalizations.of(context)!.camLink: cid, AppLocalizations.of(context)!.rudel: rudelC, AppLocalizations.of(context)!.adresse1: adresse1C, AppLocalizations.of(context)!.bland: bLandC, AppLocalizations.of(context)!.lkr: lkrC, AppLocalizations.of(context)!.beiort: beiOrtC, AppLocalizations.of(context)!.status: TextEditingController(text: selectedStatus), AppLocalizations.of(context)!.fftyp: ffTypC, "${AppLocalizations.of(context)!.foto} / ${AppLocalizations.of(context)!.filelocation}": TextEditingController(text: selectedFotoFilm), AppLocalizations.of(context)!.zeiteinstellung: TextEditingController(text: selectedMEZ), AppLocalizations.of(context)!.platzung: TextEditingController(text: selectedPlatzung), AppLocalizations.of(context)!.ktage1: kTage1C, AppLocalizations.of(context)!.ktage1: kTage2C, AppLocalizations.of(context)!.location: standortC, }; for (var entry in fields.entries) { if (entry.value.text.isEmpty) { emptyFields.add(entry.key); } } empty = false; if (emptyFields.isNotEmpty) empty = true; return emptyFields; } // If the user decides to safe his values as a template this function is called to save the values in the database // If the user already edits a template this template will be upadted otherwise a new one will be created void saveTemplate() async { var placeDB = DBHelper(); Map templates = getPlace(); if (widget.isTemplate) { await placeDB.updateTemplate(templates); } else { await placeDB.addTemplate(templates); } } // If the user has filled all needed values this function will be called to safe them in the database // * also creates a json string to send it to the server later void saveData([bool sent = false]) async { var placeDB = DBHelper(); Map place = getPlace(); // Get the ID of the newly added or updated place int newPlaceId = await placeDB.addPlace(place); if (sent == true) { placeDB.updateSent(newPlaceId); // Update 'Sent' using the correct ID } if (widget.isTemplate) { await placeDB.deleteTemplate(cid.text); } } // The widget tree which gets the shown widget from the ./cam_widgets.dart file // The names of the widgets should be self-explaining @override Widget build(BuildContext context) { // List with the steps. The steps itself will be "shown" later List getSteps() => [ // First step Step( title: Text(AppLocalizations.of(context)!.firststep), content: Column( children: [ Align( alignment: Alignment.bottomLeft, child: VarTextField( required: true, dbName: "Standort", textController: standortC, localization: AppLocalizations.of(context)!.altstort, ), ), const SizedBox( height: 5, ), Align( alignment: Alignment.bottomLeft, child: Row( children: [ Text(AppLocalizations.of(context)!.status), const Text( '*', style: TextStyle(color: Colors.red), ), ], )), Status( initialStatus: selectedStatus, onStatusChanged: (status) { setState(() { selectedStatus = status; }); }, ), VarTextField( textController: betreuungC, localization: AppLocalizations.of(context)!.betreuung, dbName: "Betreuung", required: false), const SizedBox( height: 20, ), VarTextField( textController: cid, localization: AppLocalizations.of(context)!.camLink, dbName: "CID", required: true), VarTextField( textController: ffTypC, localization: AppLocalizations.of(context)!.fftyp, dbName: "FFTyp", required: true), const SizedBox( height: 15, ), Align( alignment: Alignment.bottomLeft, child: Row( children: [ Text(AppLocalizations.of(context)!.zeiteinstellung), const Text( '*', style: TextStyle(color: Colors.red), ) ], )), MEZ( initialMEZ: selectedMEZ, onMEZChanged: (mez) { setState(() { selectedMEZ = mez; }); }, ), const SizedBox( height: 15, ), VarTextField( textController: kSchloNrC, localization: AppLocalizations.of(context)!.kschlonr, dbName: "KSchloNr", required: false), const SizedBox( height: 5, ), VarTextField( textController: rudelC, localization: AppLocalizations.of(context)!.rudel, dbName: "Rudel", required: true), const SizedBox( height: 15, ), ], )), // Second step Step( title: Text(AppLocalizations.of(context)!.secondstep), content: Column( children: [ Row( children: [ Column( children: [ Text(currentPosition.latitude.toString()), Text(currentPosition.longitude.toString()), ], ), const SizedBox( width: 15, ), ElevatedButton( onPressed: () async { final result = await Navigator.of(context) .push( MaterialPageRoute(builder: (context) { return Karte( ortInfoC: ortInfoC, beiOrtC: beiOrtC, currentPosition: currentPosition, onPositionChange: (updatedPosition) { setState(() { currentPosition = updatedPosition; }); }, ); })); if (result != null) { setState(() { currentPosition = Position( latitude: result.latitude, longitude: result.longitude, timestamp: DateTime.now(), accuracy: 0.0, altitude: 0.0, altitudeAccuracy: 0.0, heading: 0.0, headingAccuracy: 0.0, speed: 0.0, speedAccuracy: 0.0, ); }); } }, child: Text(AppLocalizations.of(context)!.openMap)), ], ), VarTextField( textController: bLandC, localization: AppLocalizations.of(context)!.bland, dbName: "BLand", required: true, defaultValue: "bLand", ), VarTextField( textController: lkrC, localization: AppLocalizations.of(context)!.lkr, dbName: "Lkr", required: true), VarTextField( textController: beiOrtC, localization: AppLocalizations.of(context)!.beiort, dbName: "BeiOrt", required: true), VarTextField( textController: ortInfoC, localization: AppLocalizations.of(context)!.ortinfo, dbName: "OrtInfo", required: false), const SizedBox( height: 15, ), Align( alignment: Alignment.bottomLeft, child: Row( children: [ Text(AppLocalizations.of(context)!.platzung), const Text( '*', style: TextStyle(color: Colors.red), ) ], )), Platzung( initialPlatzung: selectedPlatzung, onPlatzungChanged: (platzung) { setState(() { selectedPlatzung = platzung; }); }, ), ], )), // Third step Step( title: Text(AppLocalizations.of(context)!.thirdstep), content: Column( children: [ Datum( initDatum: datum, onDateChanged: (value) { datum = value; }, ), KontDat( initKontDat: kontDat, onDateChanged: (value) { setState(() { kontDat = value; }); }, ), const SizedBox( height: 20, ), Align( alignment: Alignment.bottomLeft, child: Row( children: [ Text(AppLocalizations.of(context)!.ktage), const Text( '*', style: TextStyle(color: Colors.red), ), ], ), ), Row( children: [ Expanded( child: Text( AppLocalizations.of(context)!.ktage1)), const SizedBox(width: 15,), Expanded( flex: 4, child: VarTextField(otherDefault: "24", textController: kTage1C, localization: AppLocalizations.of(context)!.ktage1, dbName: "KTage1", required: true)) ], ), Row( children: [ Expanded( child: Text( AppLocalizations.of(context)!.ktage2)), const SizedBox(width: 15,), Expanded( flex: 4, child: VarTextField(otherDefault: "48", textController: kTage2C, localization: AppLocalizations.of(context)!.ktage2, dbName: "KTage2", required: true)) ], ), const SizedBox( height: 20, ), Row( children: [ AbbauDat( initAbbauDat: abbauDat, onDateChanged: (value) { abbauDat = value; }, ), ], ), const SizedBox( height: 20, ), VarTextField( textController: auftragC, localization: AppLocalizations.of(context)!.auftrag, dbName: "Auftrag", required: false), VarTextField( textController: kontAbspC, localization: AppLocalizations.of(context)!.kontabsp, dbName: "KontAbsp", required: false), VarTextField( textController: sonstBemC, localization: AppLocalizations.of(context)!.sonstbemerkungen, dbName: "SonstBem", required: false), ], )), // Fourth step Step( title: Text(AppLocalizations.of(context)!.fourthstep), content: Column( children: [ VarTextField( textController: adresse1C, localization: AppLocalizations.of(context)!.adresse1, dbName: "Adresse1", required: true, defaultValue: "addresse1", ), VarTextField( textController: adresse2C, localization: AppLocalizations.of(context)!.adresse2, dbName: "Adresse2", required: false), VarTextField( textController: adresse3C, localization: AppLocalizations.of(context)!.adresse3, dbName: "Adresse2", required: false), const SizedBox( height: 15, ), VarTextField( textController: fKontakt1C, localization: AppLocalizations.of(context)!.fkontakt1, dbName: "FKontakt1", required: false), VarTextField( textController: fKontakt2C, localization: AppLocalizations.of(context)!.fkontakt2, dbName: "FKontakt2", required: false), VarTextField( textController: fKontakt3C, localization: AppLocalizations.of(context)!.fkontakt3, dbName: "FKontakt3", required: false), VarTextField( textController: intKommC, localization: AppLocalizations.of(context)!.intkomm, dbName: "IntKomm", required: false), ], )) ]; // Here the site is built with the steps from above return Scaffold( appBar: AppBar(title: Text(AppLocalizations.of(context)!.addplace)), body: PageTransitionSwitcher( duration: const Duration(milliseconds: 800), transitionBuilder: (Widget child, Animation animation, Animation secondaryAnimation) { return SharedAxisTransition( animation: animation, secondaryAnimation: secondaryAnimation, transitionType: SharedAxisTransitionType.vertical, child: child, ); }, child: Stepper( key: ValueKey(currentStep), type: StepperType.vertical, steps: getSteps(), // Functions that handle the navigation through the steps currentStep: currentStep, onStepTapped: (value) { setState(() { currentStep = value; }); }, onStepContinue: () async { final isLastStep = currentStep == getSteps().length - 1; if (!isLastStep) { saveTemplate(); setState(() { currentStep += 1; }); } else { List emptyFields = validateData(); // ! always filled out // empty = false; if (widget.isSent) { Navigator.pushNamedAndRemoveUntil(context, '/home', (route) => false); } else if (empty == true) { showTemplateDialog(emptyFields); return; } else if (empty == false) { await showSaveOptionsDialog(); } } }, onStepCancel: () { if (currentStep == 0) { Navigator.pop(context); } else { setState(() { currentStep -= 1; }); } }))); } }