Files
fforte/lib/screens/addCam/add_cam_main.dart
2025-06-18 15:58:01 +02:00

763 lines
29 KiB
Dart

import 'package:fforte/enums/databases.dart';
import 'package:fforte/screens/helper/add_entries_dialog_helper.dart';
import 'package:fforte/screens/helper/snack_bar_helper.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/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:flutter/material.dart';
import 'package:fforte/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';
import 'widgets/abbau_dat.dart';
import 'widgets/karte.dart';
import 'widgets/kont_dat.dart';
import 'widgets/mez.dart';
import 'widgets/platzung.dart';
import 'widgets/status.dart';
/// Widget for adding or editing camera trap locations
/// Supports template creation, editing existing entries, and new entries
class AddCamMain extends StatefulWidget {
/// Whether this form is being used to create a template
final bool isTemplate;
/// Whether the entry has been sent to the server
final bool isSent;
/// Existing data to populate the form with (for editing)
final Map<String, dynamic>? existingData;
const AddCamMain({
super.key,
this.isTemplate = false,
this.existingData,
this.isSent = false,
});
@override
State<AddCamMain> createState() => _AddCamMainState();
}
/// State class for the camera trap location form
class _AddCamMainState extends State<AddCamMain> {
/// Current step in the multi-step form
int currentStep = 0;
/// Whether this form is being used as a template
late bool isTemplate;
/// Current GPS position, initialized with default values for Germany
Position currentPosition = Position(
longitude: 10.0, // Default longitude (roughly center of Germany)
latitude: 51.0, // Default latitude (roughly center of Germany)
timestamp: DateTime.now(),
accuracy: 0.0,
altitude: 0.0,
heading: 0.0,
speed: 0.0,
speedAccuracy: 0.0,
altitudeAccuracy: 0.0,
headingAccuracy: 0.0,
);
/// Map containing all form fields with their controllers and required status
/// Organized by steps:
/// - Step 1: Basic information (ID, Status, etc.)
/// - Step 2: Location information
/// - Step 3: Dates and control periods
/// - Step 4: Contact information
Map<String, Map<String, dynamic>> rmap = {
"ID": {"controller": TextEditingController(), "required": false},
// Step 1
"Standort": {"controller": TextEditingController(), "required": true},
"Status": {"controller": TextEditingController(), "required": true},
"Betreuung": {"controller": TextEditingController(), "required": false},
"CID": {"controller": TextEditingController(), "required": true},
"FFTyp": {"controller": TextEditingController(), "required": true},
"MEZ": {"controller": TextEditingController(), "required": true},
"KSchloNr": {"controller": TextEditingController(), "required": false},
"Rudel": {"controller": TextEditingController(), "required": true},
// Step 2
"DECLNG": {"controller": TextEditingController(), "required": false},
"DECLAT": {"controller": TextEditingController(), "required": false},
"BLand": {"controller": TextEditingController(), "required": true},
"Lkr": {"controller": TextEditingController(), "required": true},
"BeiOrt": {"controller": TextEditingController(), "required": true},
"OrtInfo": {"controller": TextEditingController(), "required": false},
"Platzung": {"controller": TextEditingController(), "required": true},
// Step 3
"Datum": {"controller": TextEditingController(), "required": false},
"KontDat": {"controller": TextEditingController(), "required": false},
"KTage1": {"controller": TextEditingController(), "required": true},
"KTage2": {"controller": TextEditingController(), "required": true},
"AbbauDat": {"controller": TextEditingController(), "required": false},
"Auftrag": {"controller": TextEditingController(), "required": false},
"KontAbsp": {"controller": TextEditingController(), "required": false},
"SonstBem": {"controller": TextEditingController(), "required": false},
// Step 4
"Adresse1": {"controller": TextEditingController(), "required": true},
"Adresse2": {"controller": TextEditingController(), "required": false},
"Adresse3": {"controller": TextEditingController(), "required": false},
"FKontakt1": {"controller": TextEditingController(), "required": false},
"FKontakt2": {"controller": TextEditingController(), "required": false},
"FKontakt3": {"controller": TextEditingController(), "required": false},
"IntKomm": {"controller": TextEditingController(), "required": false},
// Gone?
"ProtoAm": {"controller": TextEditingController(), "required": false},
"FotoFilm": {"controller": TextEditingController(), "required": false},
"Sent": {"controller": TextEditingController(), "required": false},
};
/// Retrieves all field values as a map
/// @return Map of field names to their current values
Map<String, String> getFieldsText() {
Map<String, String> puff = {};
for (var itemKey in rmap.keys) {
puff[itemKey] = rmap[itemKey]!["controller"]!.text;
}
return puff;
}
/// Flag indicating whether position is currently being loaded
bool isLoadingPosition = false;
/// Initializes the GPS position
/// Handles location permissions and device settings
/// @return Future[Position] The determined position
Future<Position> _initializePosition() async {
try {
final position = await GeolocatorService.deteterminePosition();
if (mounted) {
setState(() {
currentPosition = position;
isLoadingPosition = false;
// Update the text controllers with new position
rmap["DECLAT"]!["controller"]!.text = position.latitude.toString();
rmap["DECLNG"]!["controller"]!.text = position.longitude.toString();
});
}
return position;
} catch (error) {
if (!mounted) {
return currentPosition;
}
if (error is LocationDisabledException) {
SnackBarHelper.showSnackBarMessage(
context,
AppLocalizations.of(context)!.locationDisabled,
);
} else if (error is LocationForbiddenException) {
SnackBarHelper.showSnackBarMessage(
context,
AppLocalizations.of(context)!.locationForbidden,
);
}
setState(() {
isLoadingPosition = false;
});
return currentPosition;
}
}
@override
void initState() {
super.initState();
isTemplate = widget.isTemplate;
// If a template is edited this fills in the existing values
if (widget.existingData?.isNotEmpty ?? false) {
for (var key in widget.existingData!.keys) {
rmap[key]!["controller"]!.text =
widget.existingData?[key].toString() ?? "";
}
} else {
// If it is not a template set default values
rmap["Datum"]!["controller"]!.text = DateTime.now().toString().split(" ").first;
rmap["Status"]!["controller"]!.text = "aktiv";
rmap["FotoFilm"]!["controller"]!.text = "Foto";
rmap["MEZ"]!["controller"]!.text = "Sommerzeit";
rmap["Platzung"]!["controller"]!.text = "";
SharedPreferences.getInstance().then((SharedPreferences prefs) {
rmap["KTage1"]!["controller"]!.text = prefs.getString("kTage1");
rmap["KTage2"]!["controller"]!.text = prefs.getString("kTage2");
});
}
// Set initial default position
rmap["DECLAT"]!["controller"]!.text = currentPosition.latitude.toString();
rmap["DECLNG"]!["controller"]!.text = currentPosition.longitude.toString();
// Try to get current position
GeolocatorService.deteterminePosition().then((result) {
if (mounted) {
setState(() {
currentPosition = result;
// Update coordinates after getting the position
rmap["DECLAT"]!["controller"]!.text = result.latitude.toString();
rmap["DECLNG"]!["controller"]!.text = result.longitude.toString();
});
}
}).catchError((error) {
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,
);
}
}
});
}
@override
void dispose() {
for (String key in rmap.keys) {
rmap[key]!["controller"].dispose();
}
super.dispose();
}
// 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<Step> getSteps() => [
// First step
Step(
title: Text(AppLocalizations.of(context)!.firststep),
content: Column(
children: [
Align(
alignment: Alignment.bottomLeft,
child: VarTextField(
required: true,
dbName: "Standort",
textController: rmap["Standort"]!["controller"]!,
// textController: betreuungC,
localization: AppLocalizations.of(context)!.altstort,
dbDesignation: DatabasesEnum.place,
),
),
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: rmap["Status"]!["controller"]!.text,
onStatusChanged: (status) {
setState(() {
rmap["Status"]!["controller"]!.text = status;
});
},
),
// --------------------
VarTextField(
textController: rmap["Betreuung"]!["controller"]!,
localization: AppLocalizations.of(context)!.betreuung,
dbName: "Betreuung",
required: false,
dbDesignation: DatabasesEnum.place,
),
const SizedBox(height: 20),
// --------------------
VarTextField(
textController: rmap["CID"]!["controller"],
localization: AppLocalizations.of(context)!.camLink,
dbName: "CID",
required: true,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["FFTyp"]!["controller"],
localization: AppLocalizations.of(context)!.fftyp,
dbName: "FFTyp",
required: true,
dbDesignation: DatabasesEnum.place,
),
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: rmap["MEZ"]!["controller"]!.text,
onMEZChanged: (mez) {
setState(() {
rmap["MEZ"]!["controller"]!.text = mez;
});
},
),
const SizedBox(height: 15),
// --------------------
VarTextField(
textController: rmap["KSchloNr"]!["controller"],
localization: AppLocalizations.of(context)!.kschlonr,
dbName: "KSchloNr",
required: false,
dbDesignation: DatabasesEnum.place,
),
const SizedBox(height: 5),
// --------------------
VarTextField(
textController: rmap["Rudel"]!["controller"],
localization: AppLocalizations.of(context)!.rudel,
dbName: "Rudel",
required: true,
dbDesignation: DatabasesEnum.place,
),
const SizedBox(height: 15),
],
),
),
// Second step
Step(
title: Text(AppLocalizations.of(context)!.secondstep),
content: Column(
children: [
Row(
children: [
Column(
children: [
Text(rmap["DECLAT"]!["controller"]!.text),
Text(rmap["DECLNG"]!["controller"]!.text),
],
),
const SizedBox(width: 15),
ElevatedButton(
onPressed: () async {
setState(() {
isLoadingPosition = true;
});
try {
await _initializePosition();
} catch (e) {
// Error already handled in _initializePosition
if (mounted) {
setState(() {
isLoadingPosition = false;
});
return;
}
}
if (!context.mounted) return;
final result = await Navigator.of(context).push<LatLng>(
MaterialPageRoute(
builder: (context) {
return Karte(
ortInfoC: rmap["OrtInfo"]!["controller"],
beiOrtC: rmap["BeiOrt"]!["controller"],
currentPosition: currentPosition,
decLatC: rmap["DECLAT"]!["controller"],
decLngC: rmap["DECLNG"]!["controller"],
);
},
),
);
if (result != null && mounted) {
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,
);
rmap["DECLAT"]!["controller"]!.text =
result.latitude.toString();
rmap["DECLNG"]!["controller"]!.text =
result.longitude.toString();
isLoadingPosition = false;
});
} else if (mounted) {
setState(() {
isLoadingPosition = false;
});
}
},
child: isLoadingPosition
? Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
color:
Theme.of(context).colorScheme.onPrimary,
),
),
const SizedBox(width: 8),
Text(AppLocalizations.of(context)!.openMap),
],
)
: Text(AppLocalizations.of(context)!.openMap),
),
],
),
// --------------------
VarTextField(
textController: rmap["BLand"]!["controller"]!,
localization: AppLocalizations.of(context)!.bland,
dbName: "BLand",
required: true,
dbDesignation: DatabasesEnum.place,
defaultValue: "bLand",
),
// --------------------
VarTextField(
textController: rmap["Lkr"]!["controller"]!,
localization: AppLocalizations.of(context)!.lkr,
dbName: "Lkr",
required: true,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["BeiOrt"]!["controller"]!,
localization: AppLocalizations.of(context)!.beiort,
dbName: "BeiOrt",
required: true,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["OrtInfo"]!["controller"]!,
localization: AppLocalizations.of(context)!.ortinfo,
dbName: "OrtInfo",
required: false,
dbDesignation: DatabasesEnum.place,
),
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: rmap["Platzung"]!["controller"]!.text,
onPlatzungChanged: (platzung) {
setState(() {
rmap["Platzung"]!["controller"]!.text = platzung;
});
},
),
],
),
),
// Third step
Step(
title: Text(AppLocalizations.of(context)!.thirdstep),
content: Column(
children: [
Datum(
initDatum: DateTime.parse(rmap["Datum"]!["controller"]!.text),
onDateChanged: (value) {
setState(() {
rmap["Datum"]!["controller"]!.text = value.toString().split(" ").first;
});
},
name: AppLocalizations.of(context)!.pickDate,
),
// --------------------
KontDat(
initKontDat: rmap["KontDat"]!["controller"]!.text == ""
? DateTime.now()
: DateTime.parse(rmap["KontDat"]!["controller"]!.text),
onDateChanged: (value) {
setState(() {
rmap["KontDat"]!["controller"]!.text =
value.toString().split(" ").first;
});
},
),
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(
textController: rmap["KTage1"]!["controller"]!,
localization: AppLocalizations.of(context)!.ktage1,
dbName: "KTage1",
required: true,
dbDesignation: DatabasesEnum.place,
),
),
],
),
Row(
children: [
Expanded(child: Text(AppLocalizations.of(context)!.ktage2)),
const SizedBox(width: 15),
Expanded(
flex: 4,
child: VarTextField(
textController: rmap["KTage2"]!["controller"]!,
localization: AppLocalizations.of(context)!.ktage2,
dbName: "KTage2",
required: true,
dbDesignation: DatabasesEnum.place,
),
),
],
),
const SizedBox(height: 20),
// --------------------
Row(
children: [
AbbauDat(
initAbbauDat: rmap["AbbauDat"]!["controller"]!.text == ""
? null
: DateTime.parse(
rmap["AbbauDat"]!["controller"]!.text),
onDateChanged: (value) {
rmap["AbbauDat"]!["controller"]!.text =
value.toString().split(" ").first;
},
),
],
),
const SizedBox(height: 20),
// --------------------
VarTextField(
textController: rmap["Auftrag"]!["controller"]!,
localization: AppLocalizations.of(context)!.auftrag,
dbName: "Auftrag",
required: false,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["KontAbsp"]!["controller"]!,
localization: AppLocalizations.of(context)!.kontabsp,
dbName: "KontAbsp",
required: false,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["SonstBem"]!["controller"]!,
localization: AppLocalizations.of(context)!.sonstbemerkungen,
dbName: "SonstBem",
dbDesignation: DatabasesEnum.place,
required: false,
),
],
),
),
// Fourth step
Step(
title: Text(AppLocalizations.of(context)!.fourthstep),
content: Column(
children: [
VarTextField(
textController: rmap["Adresse1"]!["controller"]!,
localization: AppLocalizations.of(context)!.adresse1,
dbName: "Adresse1",
dbDesignation: DatabasesEnum.place,
required: true,
defaultValue: "addresse1",
),
// --------------------
VarTextField(
textController: rmap["Adresse2"]!["controller"]!,
localization: AppLocalizations.of(context)!.adresse2,
dbName: "Adresse2",
required: false,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["Adresse3"]!["controller"]!,
localization: AppLocalizations.of(context)!.adresse3,
dbName: "Adresse3",
required: false,
dbDesignation: DatabasesEnum.place,
),
const SizedBox(height: 15),
// --------------------
VarTextField(
textController: rmap["FKontakt1"]!["controller"]!,
localization: AppLocalizations.of(context)!.fkontakt1,
dbName: "FKontakt1",
required: false,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["FKontakt2"]!["controller"]!,
localization: AppLocalizations.of(context)!.fkontakt2,
dbName: "FKontakt2",
required: false,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["FKontakt3"]!["controller"]!,
localization: AppLocalizations.of(context)!.fkontakt3,
dbName: "FKontakt3",
required: false,
dbDesignation: DatabasesEnum.place,
),
// --------------------
VarTextField(
textController: rmap["IntKomm"]!["controller"]!,
localization: AppLocalizations.of(context)!.intkomm,
dbName: "IntKomm",
required: false,
dbDesignation: DatabasesEnum.place,
),
],
),
),
];
// 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<double> animation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.vertical,
child: child,
);
},
child: Stepper(
key: ValueKey<int>(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) {
var res = await saveTemplate(
getFieldsText(),
DatabasesEnum.place,
);
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 == true) {
AddEntriesDialogHelper.showTemplateDialog(
context, getFieldsText(), DatabasesEnum.place);
return;
} else if (empty == false) {
await AddEntriesDialogHelper.showSaveOptionsDialog(
context,
getFieldsText(),
isTemplate,
DatabasesEnum.place,
);
}
}
},
onStepCancel: () {
if (currentStep == 0) {
Navigator.pop(context);
} else {
setState(() {
currentStep -= 1;
});
}
},
),
),
);
}
}