diff --git a/.envExample b/.envExample new file mode 100644 index 0000000..eb92d67 --- /dev/null +++ b/.envExample @@ -0,0 +1,4 @@ +APPWRITE_PROJECT_ID= +APPWRITE_PROJECT_NAME= +APPWRITE_PUBLIC_ENDPOINT= +PTV_GEOLINK_API_KEY= \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 0f9a8d6..974f832 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,6 +10,7 @@ analyzer: errors: unused_field: ignore + avoid_print: ignore include: package:flutter_lints/flutter.yaml linter: diff --git a/android/app/build.gradle b/android/app/build.gradle index e2e27e1..1081662 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,8 +5,8 @@ plugins { } android { - ndkVersion = "25.1.8937393" - namespace = "io.appwrite.flutter" + ndkVersion = "27.0.12077973" + namespace = "com.example.flutter_new_tank_app310725" compileSdk = flutter.compileSdkVersion compileOptions { @@ -19,7 +19,7 @@ android { } defaultConfig { - applicationId = "io.appwrite.flutter" + applicationId = "com.example.flutter_new_tank_app310725" minSdk = flutter.minSdkVersion targetSdk = flutter.targetSdkVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6267548..e920667 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,8 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:label="Tank Guru"> + logout() async => + await _account.deleteSession(sessionId: 'current'); + + Future login(Map map) async => + await _account.createEmailPasswordSession( + email: map['email'], + password: map['password'], + ); + + Future signUpAnonymus() async => + await _account.createAnonymousSession(); + + Future signup(Map map) async => _account.create( + userId: ID.unique(), + email: map['email'], + password: map['password'], + name: map['name'], + ); + + Future get getCurrentUser => _account.get(); + + createTankStop(Map map) {} } diff --git a/lib/data/repository/location_repository.dart b/lib/data/repository/location_repository.dart new file mode 100644 index 0000000..1aea6fa --- /dev/null +++ b/lib/data/repository/location_repository.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:http/http.dart' as http; + + +class LocationRepository { + + static final LocationRepository _instance = LocationRepository._internal(); + + /// Singleton instance getter + factory LocationRepository() => _instance; + + //Constructor??? + LocationRepository._internal() { + //init for something + } + + + /// Überprüft, ob der Standortdienst aktiviert ist. + Future isLocationServiceEnabled() async { + return await Geolocator.isLocationServiceEnabled(); + } + + /// Fragt die Berechtigung für den Standort ab. + Future checkPermission() async { + return await Geolocator.checkPermission(); + } + + /// Fordert die Berechtigung für den Standort an. + Future requestPermission() async { + return await Geolocator.requestPermission(); + } + + /// Liefert die aktuelle Position des Geräts. + /// Wirft eine Exception, wenn der Dienst nicht aktiviert ist oder keine Berechtigung vorliegt. + Future getCurrentPosition() async { + bool serviceEnabled = await isLocationServiceEnabled(); + if (!serviceEnabled) { + // Standortdienste sind nicht aktiviert. + return Future.error('Location services are disabled.'); + } + + LocationPermission permission = await checkPermission(); + if (permission == LocationPermission.denied) { + permission = await requestPermission(); + if (permission == LocationPermission.denied) { + // Berechtigungen sind verweigert. + return Future.error('Location permissions are denied'); + } + } + + if (permission == LocationPermission.deniedForever) { + // Berechtigungen sind dauerhaft verweigert. + return Future.error( + 'Location permissions are permanently denied, we cannot request permissions.', + ); + } + + // Wenn alles in Ordnung ist, die Position zurückgeben. + return await Geolocator.getCurrentPosition(); + } + + Future getNearbyLocation(Map map) async { + String locationOrt = '?'; + var lat = map['lat']; + var lng = map['lng']; + // Hier kannst du die Logik hinzufügen, um den Standort zu verwenden, z.B. + String ptvGeoLink = + 'https://api.myptv.com/geocoding/v1/locations/by-position/$lat/$lng?language=de&apiKey=${dotenv.get('PTV_GEOLINK_API_KEY')}'; + final client = http.Client(); + var response = await client.get( + Uri.parse(ptvGeoLink), + headers: {'Content-Type': 'application/json', 'charset': 'utf-8'}, + ); + //Response Data status + if (response.statusCode == 200) { + //Response is succsessful + Map data = json.decode( + utf8.decode(response.bodyBytes), + ); //get response data + Map mapOfAddressfromPosition = + data['locations'][0]['address']; //get response address of position + if (mapOfAddressfromPosition.isNotEmpty) { + locationOrt = + '${mapOfAddressfromPosition['street'].toString()} ${mapOfAddressfromPosition['houseNumber'].toString()}, ${mapOfAddressfromPosition['postalCode'].toString()} ${mapOfAddressfromPosition['city'].toString()}'; + } + } else { + debugPrint(response.statusCode.toString()); + } + client.close(); + + return locationOrt; + } +} diff --git a/lib/main.dart b/lib/main.dart index ff42366..8ed11dd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import './app.dart'; import './utils/app_initializer.dart'; void main() async { - await dotenv.load(fileName: '.env'); await AppInitializer.initialize(); runApp(AppwriteApp()); } diff --git a/lib/pages/login/login_controller.dart b/lib/pages/login/login_controller.dart new file mode 100644 index 0000000..c593061 --- /dev/null +++ b/lib/pages/login/login_controller.dart @@ -0,0 +1,211 @@ +import 'package:appwrite/appwrite.dart'; +import 'package:appwrite/models.dart' as models; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../data/repository/appwrite_repository.dart'; +import '../../utils/extensions/static_helper.dart'; +import '../tank/tank_view.dart'; + +class LoginController extends GetxController { + final AppwriteRepository _authRepository = AppwriteRepository(); + + final isVisible = false.obs; + //Form Key + final formKey = GlobalKey(); + bool isFormValid = false; + + final emailController = TextEditingController(); + final passwordController = TextEditingController(); + final nameController = TextEditingController(); + + @override + void onInit() { + super.onInit(); + // Initialize any necessary data or state here + print('LoginController initialized'); + } + + @override + void onReady() { + super.onReady(); + // This method is called when the controller is ready + print('LoginController is ready'); + if (StaticHelper.hasData()) { + // If session ID exists, navigate to TankPage + print('Session ID found, navigating to TankPage'); + goToTankPage(); + } else { + // If no session ID, initialize the login controller + print('No session ID found, initializing LoginController'); + } + } + + @override + void onClose() { + // Clean up any resources or listeners here + emailController.dispose(); + passwordController.dispose(); + nameController.dispose(); + // emailFocusNode.dispose(); + // passwordFocusNode.dispose(); + // nameFocusNode.dispose(); + super.onClose(); + } + + void clearTextEditingController() { + emailController.clear(); + passwordController.clear(); + nameController.clear(); + } + + String? validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'Bitte geben Sie eine E-Mail-Adresse ein'; + } + if (!GetUtils.isEmail(value)) { + return 'Bitte geben Sie eine gültige E-Mail-Adresse ein'; + } + return null; + } + + String? validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Bitte geben Sie ein Passwort ein'; + } + if (value.length < 8) { + return 'Das Passwort muss mindestens 8 Zeichen lang sein'; + } + return null; + } + + String? validateName(String? value) { + if (value == null || value.isEmpty) { + return 'Bitte geben Sie Ihren Namen ein'; + } + if (value.length < 3) { + return 'Der Name muss mindestens 3 Zeichen lang sein'; + } + return null; + } + + var message = 'NoMessage!'; + var isError = false; + + // Registrierung + Future register() async { + isError = false; + message = 'NoMessage!'; + isFormValid = formKey.currentState!.validate(); + if (!isFormValid) { + print('Formular ist ungültig'); + return; + } else { + print('Formular ist gültig'); + formKey.currentState!.save(); + try { + await _authRepository.signup({ + 'email': emailController.text, + 'password': passwordController.text, + 'name': nameController.text, + }).then((models.User userValue) { + // GetStorage data storage + isVisible(false); + print( + 'User was stored and Loggedin: ${userValue.name}, ${userValue.$id}, ${userValue.email}', + ); + // Store session ID in GetStorage + StaticHelper.dataBox.write('userId', userValue.$id); + StaticHelper.dataBox.write('userName', userValue.name); + StaticHelper.dataBox.write('userEmail', userValue.email); + // Go to TankPage + goToTankPage(); + message = 'Sie wurden registriert und Angemeldet!!'; + StaticHelper.getMySnackeBar('Erfolg', message, Colors.green); + }).catchError((error) { + if (error is AppwriteException) { + // Handle specific Appwrite exceptions + print('Appwrite Fehler: ${error.message}'); + message = 'Appwrite Fehler: ${error.message}'; + isError = true; + } else { + // Handle other types of errors + message = 'Allgemeiner Fehler: $error'; + print('Allgemeiner Fehler: $error'); + isError = true; + } + message = 'Fehler bei der Registrierung: $error'; + print('Fehler bei der Registrierung: $error'); + //Clear GetStorage session ID + StaticHelper.removeFromStorage(); + isError = true; + }); + } catch (e) { + message = 'Fehler bei Registrierung: $e'; + print('Fehler bei Registrierung: $e'); + //Clear GetStorage session ID + StaticHelper.removeFromStorage(); + isError = true; + } + if (isError) StaticHelper.getMySnackeBar('Fehler', message, Colors.red); + } + } + + // Login + Future login() async { + message = 'NoMessage!'; + isError = false; + + isFormValid = formKey.currentState!.validate(); + if (!isFormValid) { + print('Formular ist ungültig'); + return; + } else { + print('Formular ist gültig'); + formKey.currentState!.save(); + try { + await _authRepository.login({ + 'email': emailController.text, + 'password': passwordController.text, + }).then((models.Session session) { + // Store session ID in GetStorage + StaticHelper.dataBox.write('userId', session.$id); + print('Session ID stored: ${StaticHelper.dataBox.read('sessinId')}'); + print( + 'Erfolgreich eingeloggt: ${session.$id}\n${session.providerUid}', + ); + // Go to TankPage + goToTankPage(); + StaticHelper.getMySnackeBar( + 'Erfolg', 'Sie wurden Angemeldet!!', Colors.green); + }).catchError((error) { + if (error is AppwriteException) { + // Handle specific Appwrite exceptions + isError = true; + message = 'Appwrite Fehler: ${error.message}'; + print('Appwrite Fehler: ${error.message}'); + } else { + // Handle other types of errors + print('Allgemeiner Fehler: $error'); + isError = true; + message = 'Allgemeiner Fehler: $error'; + } + print('Fehler beim Login: $error'); + isError = true; + message = 'Fehler beim Login: $error'; + _authRepository.logout(); + }); + } catch (e) { + print('Fehler beim Login: $e'); + isError = true; + message = 'Fehler beim Login: $e'; + _authRepository.logout(); + } + if (isError) StaticHelper.getMySnackeBar('Fehler', message, Colors.red); + } + } + + void goToTankPage() { + Get.offAndToNamed(TankPage.namedRoute); + } +} diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart new file mode 100644 index 0000000..2f4527a --- /dev/null +++ b/lib/pages/login/login_view.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'login_controller.dart'; + +class LoginPage extends GetView { + static const namedRoute = '/login-page'; + const LoginPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Container( + alignment: Alignment.center, + width: double.infinity, + height: double.infinity, + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + headerTextTankGuru(), + const SizedBox(height: 5), + headerTextDescription(), + const SizedBox(height: 10), + loadingImage(), + inputFields(), + ], + ), + ), + ), + ), + ); + } + + //the structure of the login page + SizedBox loadingImage() { + return SizedBox( + width: 300, + height: 300, + child: ClipRRect( + borderRadius: BorderRadius.circular(40), + child: Image.asset('lib/images/gasolineGuru.jpg', fit: BoxFit.cover), + ), + ); + } + + Text headerTextTankGuru() { + return Text( + 'Tank GURU', + style: TextStyle( + fontSize: 35, + fontWeight: FontWeight.bold, + color: Colors.teal[600], + letterSpacing: 2, + ), + ); + } + + Text headerTextDescription() { + return Text( + 'Das ultimative Tanken', + style: TextStyle(fontSize: 20, color: Colors.grey[300]), + ); + } + + Widget inputFields() { + return Obx( + () => SizedBox( + width: 300, + child: Form( + key: controller.formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + SizedBox(height: 20), + TextFormField( + validator: (value) => controller.validateEmail(value), + keyboardType: TextInputType.emailAddress, + controller: controller.emailController, + decoration: InputDecoration( + labelText: 'Email', + border: OutlineInputBorder(), + ), + ), + SizedBox(height: 20), + TextFormField( + validator: (value) => controller.validatePassword(value), + keyboardType: TextInputType.visiblePassword, + controller: controller.passwordController, + decoration: InputDecoration( + labelText: 'Password', + border: OutlineInputBorder(), + ), + obscureText: true, + ), + if (controller.isVisible.value) ...[ + SizedBox(height: 20), + TextFormField( + validator: (value) => controller.validateName(value), + keyboardType: TextInputType.name, + controller: controller.nameController, + decoration: InputDecoration( + labelText: 'Name', + border: OutlineInputBorder(), + ), + ), + ], + SizedBox(height: 20), + SizedBox( + width: 300, + child: ElevatedButton( + onPressed: () { + controller.isVisible.value == false + ? controller.login() + : controller.register(); + }, + child: controller.isVisible.value == false + ? Text('Login') + : Text('Registrieren'), + ), + ), + SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Noch kein Konto? '), + GestureDetector( + onTap: () { + if (!controller.isVisible.value) { + controller.isVisible.value = true; + } else { + controller.isVisible.value = false; + } + }, + child: controller.isVisible.value == false + ? Text( + 'Sign Up', + style: TextStyle( + color: Colors.teal, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ) + : Text( + 'Sign In', + style: TextStyle( + color: Colors.teal, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/tank/tank_controller.dart b/lib/pages/tank/tank_controller.dart new file mode 100644 index 0000000..0e0e316 --- /dev/null +++ b/lib/pages/tank/tank_controller.dart @@ -0,0 +1,266 @@ +import 'package:appwrite/appwrite.dart'; +import 'package:appwrite/models.dart' as models; +import 'package:flutter/material.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; +import 'package:intl/intl.dart'; +import 'package:tankguru_flutter_app_appwrite/utils/extensions/static_helper.dart'; + +import '../../data/repository/location_repository.dart'; +import '../../data/repository/appwrite_repository.dart'; +import '../login/login_view.dart'; + +class TankController extends GetxController { + final _dataBox = GetStorage('MyUserStorage'); + //AppWrite API-REST get Data + final AppwriteRepository _authRepository = AppwriteRepository(); + // GEOLOCATING Services + final LocationRepository _locationRepository = LocationRepository(); + + // Rx-Variablen für die UI, um auf Änderungen zu reagieren + final circleAvatarUserChar = 'A'.obs; + final userNameToDisplay = 'Test User'.obs; + final Rx currentPosition = Rx(null); + final Rx isLoading = false.obs; + final Rx errorMessage = Rx(null); + final rxOrtString = '?'.obs; + final rxSessionIdString = '?'.obs; + final rxSummePreisString = '0.00'.obs; + // TextEditingController für die Formulareingaben + final formKeyTank = GlobalKey(); + bool isFormValid = false; + DateTime? _selectedDateTime; + final dateController = TextEditingController(); + final f = DateFormat('yyyy-MM-dd'); + final kilometerStandEdittingController = TextEditingController(); + final mengeController = TextEditingController(); + final pricePerLiterController = TextEditingController(); + + final FocusNode firstFocusNode = FocusNode(); // Deklariere den FocusNode + + // Methode für das ausgewählte Datum + Future selectDateTime(BuildContext context) async { + // 1. Datum auswählen + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: _selectedDateTime ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime(2101), + ); + _selectedDateTime = pickedDate; + if (_selectedDateTime != null) { + dateController.text = _selectedDateTime!.toIso8601String().substring( + 0, + 10, + ); + } + } + + /// Methode zum Abrufen des Standorts. + Future fetchCurrentLocation() async { + isLoading.value = true; + errorMessage.value = null; + try { + final Position position = await _locationRepository.getCurrentPosition(); + currentPosition.value = position; + final double latitude = position.latitude; + final double longitude = position.longitude; + // Hier kannst du die Logik hinzufügen, um den Standort zu verwenden, z.B. + // den Standort in der UI anzuzeigen oder an einen Server zu senden. + var map = {'lat': latitude, 'lng': longitude}; + rxOrtString.value = await _locationRepository.getNearbyLocation(map); + // Print Standortinformationen in der Konsole + print('Nearby Location: ${rxOrtString.value}'); + print('Current Position: Latitude: $latitude, Longitude: $longitude'); + } catch (e) { + // Hier fängst du die Fehler aus dem Repository auf + errorMessage.value = e.toString(); + } finally { + isLoading.value = false; + } + update(); + } + + void clearTextEditingController() { + formKeyTank.currentState!.reset(); + // Den Fokus wieder auf das erste Feld legen + FocusScope.of(Get.context!).requestFocus(firstFocusNode); + // TextEditingController zurücksetzen + _selectedDateTime = null; // Datum zurücksetzen + dateController.clear(); + kilometerStandEdittingController.clear(); + mengeController.clear(); + pricePerLiterController.clear(); + } + + String? validateKilometerStand(String? value) { + if (value == null || value.isEmpty) { + return 'Bitte Kilometerstand eingeben'; + } + if (!GetUtils.isNum(value)) { + return 'Bitte geben Sie eine gültige Zahl ein'; + } + return null; + } + + String? validateMenge(String? value) { + if (value == null || value.isEmpty) { + return 'Bitte Menge eingeben'; + } + if (!GetUtils.isNum(value)) { + return 'Bitte geben Sie eine gültige Zahl ein'; + } + return null; + } + + String? validatePricePerLiter(String? value) { + if (value == null || value.isEmpty) { + return 'Bitte Preis pro Liter eingeben'; + } + if (!GetUtils.isNum(value)) { + return 'Bitte geben Sie eine gültige Zahl ein'; + } + return null; + } + + //Go to Login Page + void logoutSessionAndGoToLoginPage() async { + // Handle logout logic here + print('Logout session and go to login page'); + // Clear GetStorage session ID + StaticHelper.removeFromStorage(); + print('Session ID removed from GetStorage'); + await _authRepository.logout(); + Get.offAndToNamed(LoginPage.namedRoute); + StaticHelper.getMySnackeBar( + 'Logout', 'Sie wurden abgemeldet!', Colors.blue); + } + + @override + void onInit() { + super.onInit(); + if (_dataBox.hasData('sessinId')) { + rxSessionIdString(_dataBox.read('sessinId').toString()); + } + // Rufe den Standort direkt beim Initialisieren des Controllers ab, falls gewünscht. + fetchCurrentLocation(); + } + + @override + void onReady() async { + super.onReady(); + await getCurrentLoggedinUser(); + FocusScope.of(Get.context!).requestFocus(firstFocusNode); + print('TankController is ready'); + } + + Future getCurrentLoggedinUser() async { + await _authRepository.getCurrentUser.then((models.User user) { + // Hier kannst du den Benutzernamen und das Avatar-Zeichen setzen + userNameToDisplay.value = user.name; + circleAvatarUserChar.value = user.name.substring(0, 1).toUpperCase(); + }).catchError((error) { + print('Fehler beim Abrufen des Benutzers: $error'); + }); + } + + @override + void onClose() { + dateController.dispose(); + kilometerStandEdittingController.dispose(); + mengeController.dispose(); + pricePerLiterController.dispose(); + firstFocusNode.dispose(); // Dispose den FocusNode + } + + void updateSumPrice() { + final double menge = double.tryParse(mengeController.text) ?? 0.0; + final double pricePerLiter = + double.tryParse(pricePerLiterController.text) ?? 0.0; + final double sumPrice = menge * pricePerLiter; + rxSummePreisString.value = sumPrice.toStringAsFixed( + 2, + ); // Formatieren auf 2 Dezimalstellen + } + + void saveTankstopp() async { + bool isErrorBySaving = false; + String messageToUser = ''; + isFormValid = formKeyTank.currentState!.validate(); + if (!isFormValid) { + print('Formular ist ungültig'); + messageToUser = 'Bitte überprüfen Sie Ihre Eingaben.'; + Get.snackbar( + 'Fehler', + messageToUser, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 2), + ); + return; + } else { + print('Formular ist gültig'); + formKeyTank.currentState!.save(); + try { + //creatiung a tank stop + await _authRepository.createTankStop({ + 'userId': _dataBox.read('userId'), + 'date': f.format(DateTime.parse(dateController.text)), + 'odometer': double.parse( + double.parse( + kilometerStandEdittingController.text, + ).toStringAsFixed(0), + ), + 'liters': double.parse( + double.parse(mengeController.text).toStringAsFixed(2), + ), + 'pricePerLiter': double.parse( + double.parse(pricePerLiterController.text).toStringAsFixed(2), + ), + 'location': rxOrtString.value, + }).then((models.Document document) { + print('Tankstopp erfolgreich gespeichert: ${document.data}'); + messageToUser = 'Tankstopp erfolgreich gespeichert!'; + isErrorBySaving = false; + }) + // Handle specific Appwrite exceptions + .catchError((error) { + isErrorBySaving = true; + if (error is AppwriteException) { + // Handle specific Appwrite exceptions + messageToUser = 'Appwrite Fehler: ${error.message}'; + print('Appwrite Fehler: ${error.message}'); + } else { + // Handle other types of errors + messageToUser = 'Allgemeiner Fehler: $error'; + print('Allgemeiner Fehler: $error'); + } + print('Fehler bei der speicherung: $error'); + }); + } + // Handle any other exceptions that might occur + catch (e) { + isErrorBySaving = true; + messageToUser = 'Fehler beim Speichern des Tankstopps: $e'; + print('Fehler beim speichern: $e'); + } + if (!isErrorBySaving) { + clearTextEditingController(); + } + // Handle button press logic here + String title = isErrorBySaving ? 'Fehler' : 'Erfolg'; + Get.snackbar( + title, + messageToUser, + backgroundColor: isErrorBySaving ? Colors.red : Colors.green, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 4), + ); + } + } + + goToTankStopsView() async { + clearTextEditingController(); + //await Get.offAndToNamed(TanklistPage.namedRoute); + } +} diff --git a/lib/pages/tank/tank_view.dart b/lib/pages/tank/tank_view.dart new file mode 100644 index 0000000..1b0bef2 --- /dev/null +++ b/lib/pages/tank/tank_view.dart @@ -0,0 +1,277 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import './tank_controller.dart'; + +class TankPage extends GetView { + static const namedRoute = '/tank-stop-page'; + const TankPage({super.key}); + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + appBar: AppBar( + title: Text( + 'Tankstopp erfassen', + style: TextStyle(color: Colors.grey.shade300), + ), + backgroundColor: Colors.transparent, // Mach die AppBar transparent + elevation: 0, // Entferne den Schatten unter der AppBar + centerTitle: true, + actions: [ + IconButton( + icon: Icon(Icons.list, color: Colors.grey.shade300), + onPressed: () async { + // Handle logout logic here + controller.goToTankStopsView(); + }, + ), + IconButton( + icon: Icon(Icons.logout, color: Colors.grey.shade300), + onPressed: () async { + // Handle logout logic here + controller.logoutSessionAndGoToLoginPage(); + }, + ), + ], + ), + extendBodyBehindAppBar: true, // Erweitere den Body hinter die AppBar + body: Stack( + children: [ + Container( + height: double.infinity, + width: double.infinity, + decoration: const BoxDecoration( + image: DecorationImage( + image: AssetImage('lib/images/backgroundPitstopBlack.png'), + fit: BoxFit + .cover, // Skaliert das Bild, um den Container zu füllen + alignment: Alignment + .bottomCenter, // Richte das Bild am unteren Rand aus + ), + ), + ), + Obx( + () => SingleChildScrollView( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + kToolbarHeight + 20, + left: 20, + right: 20, + bottom: 20, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircleAvatar( + backgroundColor: Colors.blue, + radius: 40, + child: Text( + controller.circleAvatarUserChar.value, + style: const TextStyle( + color: Colors.black, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 5), + Text( + controller.userNameToDisplay.value, + style: const TextStyle( + fontSize: 20, + backgroundColor: Colors.black87, + color: Colors.white, + fontWeight: FontWeight.bold, + letterSpacing: 3, + ), + ), + controller.isLoading.value + ? Text('Location is loading...') + : Text( + controller.rxOrtString.value, + style: const TextStyle( + fontSize: 15, + backgroundColor: Colors.black87, + color: Colors.blue, + ), + ), + const SizedBox(height: 20), + inputFields(), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black.withValues(alpha: 0.9), + ), + child: Text.rich( + TextSpan( + text: 'Summe: ', + style: TextStyle( + fontSize: 18, + color: Colors.grey.shade300, + ), + children: [ + TextSpan( + // Hier kommt der Wert als separater TextSpan + text: controller.rxSummePreisString.value, + // Doppelt so groß wie der Standardtext', + style: TextStyle( + fontSize: 30, // Doppelt so groß wie 18 + color: Colors.blue, // Blaue Farbe + fontWeight: FontWeight + .bold, // Optional: Fetter Text, um ihn hervorzuheben + ), + ), + ], + ), + ), + ), + const SizedBox(height: 20), + SizedBox( + width: 350, + height: 50, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue.withValues( + alpha: 0.9, + ), // Button-Farbe + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 10, + ), // Abgerundete Ecken + ), + ), + onPressed: () { + controller.saveTankstopp(); + }, + child: Text( + 'Tankstop erfassen', + style: TextStyle(color: Colors.white, fontSize: 20), + ), + ), + ), + Container( + margin: EdgeInsets.only(top: 20, bottom: 50), + child: Text( + 'Hinweis: Alle Felder sind Pflichtfelder.', + style: TextStyle( + fontSize: 16, + color: Colors.red.shade300, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget inputFields() { + return SizedBox( + width: 320, + child: Form( + key: controller.formKeyTank, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + TextFormField( + focusNode: controller.firstFocusNode, // Setze den FocusNode + readOnly: true, + onTap: () => controller.selectDateTime(Get.context!), + controller: controller.dateController, + decoration: InputDecoration( + labelText: 'Datum', + labelStyle: const TextStyle(color: Colors.white, fontSize: 20), + errorStyle: const TextStyle(color: Colors.red, fontSize: 16), + filled: true, + fillColor: Colors.black.withValues(alpha: 0.9), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.white, width: 2), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, // Entferne den Rand + ), + ), + style: const TextStyle(color: Colors.white), + ), + SizedBox(height: 20), + TextFormField( + validator: (value) => controller.validateKilometerStand(value), + keyboardType: TextInputType.number, + controller: controller.kilometerStandEdittingController, + decoration: InputDecoration( + labelText: 'Kilometerstand', + labelStyle: const TextStyle(color: Colors.white, fontSize: 20), + errorStyle: const TextStyle(color: Colors.red, fontSize: 16), + filled: true, + fillColor: Colors.black.withValues(alpha: 0.9), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.white, width: 2), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, // Entferne den Rand + ), + ), + ), + SizedBox(height: 20), + TextFormField( + validator: (value) => controller.validateMenge(value), + keyboardType: TextInputType.number, + controller: controller.mengeController, + decoration: InputDecoration( + labelText: 'Menge (in Litern)', + labelStyle: const TextStyle(color: Colors.white, fontSize: 20), + errorStyle: const TextStyle(color: Colors.red, fontSize: 16), + filled: true, + fillColor: Colors.black.withValues(alpha: 0.9), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.white, width: 2), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, // Entferne den Rand + ), + ), + ), + SizedBox(height: 20), + TextFormField( + onChanged: (value) { + // Update the sum price when the price per liter changes + controller.updateSumPrice(); + }, + validator: (value) => controller.validatePricePerLiter(value), + keyboardType: TextInputType.number, + controller: controller.pricePerLiterController, + decoration: InputDecoration( + labelText: 'Preis pro Liter', + labelStyle: const TextStyle(color: Colors.white, fontSize: 20), + errorStyle: const TextStyle(color: Colors.red, fontSize: 16), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.white, width: 2), + ), + filled: true, + fillColor: Colors.black.withValues(alpha: 0.9), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + //borderSide: BorderSide.none, // Entferne den Rand + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/utils/app_initializer.dart b/lib/utils/app_initializer.dart index 6b1d365..f18b249 100644 --- a/lib/utils/app_initializer.dart +++ b/lib/utils/app_initializer.dart @@ -3,7 +3,9 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:window_manager/window_manager.dart'; +import './extensions/http_overrides.dart'; /// A utility class for initializing the Flutter application. /// @@ -19,10 +21,15 @@ class AppInitializer { _ensureInitialized(); await _setupWindowDimensions(); await _setupDeviceOrientation(); + //dotENV initial + await dotenv.load(fileName: '.env'); + //Overrides initial + HttpOverrides.global = MyHttpOverrides(); } /// Ensures that Flutter bindings are initialized. static _ensureInitialized() { + WidgetsFlutterBinding.ensureInitialized(); } diff --git a/lib/utils/extensions/constants.dart b/lib/utils/extensions/constants.dart new file mode 100644 index 0000000..73bc312 --- /dev/null +++ b/lib/utils/extensions/constants.dart @@ -0,0 +1,29 @@ + + + +import 'package:flutter/material.dart'; + +var kTextStyle = TextStyle(fontSize: 20, color: Colors.grey.shade100); +var kTextStyleSub = TextStyle(fontSize: 16, color: Colors.grey.shade400); +var kColorEuroChart = Colors.red.shade500; +var kColorBenzinChart = Colors.yellow.shade500; +var kColorPerLiterChart = Colors.blueGrey.shade300; +var kChartDescriptionFontStyle = TextStyle( + color: Colors.grey.shade900, + fontWeight: FontWeight.bold, + fontSize: 16, +); + +var kBarTitleStyle = TextStyle( + color: Colors.blue.shade900, + fontWeight: FontWeight.bold, + fontSize: 14, +); + +var kInputDecorationDropDownMenueYear = const InputDecoration( + prefixIcon: Icon(Icons.date_range), + hintText: 'Jahresauswahl', + filled: true, + fillColor: Colors.black, + errorStyle: TextStyle(color: Colors.yellow), +); diff --git a/lib/utils/extensions/http_overrides.dart b/lib/utils/extensions/http_overrides.dart new file mode 100644 index 0000000..bd7897a --- /dev/null +++ b/lib/utils/extensions/http_overrides.dart @@ -0,0 +1,9 @@ +import 'dart:io'; + +class MyHttpOverrides extends HttpOverrides{ + @override + HttpClient createHttpClient(SecurityContext? context){ + return super.createHttpClient(context) + ..badCertificateCallback = (X509Certificate cert, String host, int port)=> true; + } +} \ No newline at end of file diff --git a/lib/utils/extensions/sample_bindings.dart b/lib/utils/extensions/sample_bindings.dart new file mode 100644 index 0000000..bfd1318 --- /dev/null +++ b/lib/utils/extensions/sample_bindings.dart @@ -0,0 +1,20 @@ +import 'package:get/get.dart'; + +import '../../pages/login/login_controller.dart'; +import '../../pages/tank/tank_controller.dart'; + + + +class SampleBindings extends Bindings { + @override + void dependencies() { + // Define your dependencies here + Get.lazyPut(() => LoginController()); + Get.lazyPut(() => TankController()); + // Get.lazyPut(() => TanklistController(AuthRepository(AppWriteProvider()))); + // Get.lazyPut(() => GraphController(AuthRepository(AppWriteProvider()))); + // Get.lazyPut(() => TrackingController(AuthRepository(AppWriteProvider()))); + // Get.lazyPut(() => MapController(AuthRepository(AppWriteProvider()), LocationRepository(LocationProvider()))); + } + +} \ No newline at end of file diff --git a/lib/utils/extensions/sample_routes.dart b/lib/utils/extensions/sample_routes.dart new file mode 100644 index 0000000..380aa4c --- /dev/null +++ b/lib/utils/extensions/sample_routes.dart @@ -0,0 +1,44 @@ +import 'package:get/get.dart'; + +import '../../pages/login/login_view.dart'; +import '../../pages/tank/tank_view.dart'; +import './sample_bindings.dart'; + + + +class SampleRouts { + static final sampleBindings = SampleBindings(); + static List> samplePages = [ + GetPage( + name: LoginPage.namedRoute, + page: () => const LoginPage(), + binding: sampleBindings, + ), + GetPage( + name: TankPage.namedRoute, + page: () => const TankPage(), + binding: sampleBindings, + ), + // GetPage( + // name: TanklistPage.namedRoute, + // page: () => const TanklistPage(), + // binding: sampleBindings, + // ), + // GetPage( + // name: GraphPage.namedRoute, + // page: () => const GraphPage(), + // binding: sampleBindings, + // ), + // GetPage( + // name: TrackingPage.namedRoute, + // page: () => const TrackingPage(), + // binding: sampleBindings, + // ), + // GetPage( + // name: MapPage.namedRoute, + // page: () => const MapPage(), + // binding: SampleBindings(), + // ), + ]; + +} \ No newline at end of file diff --git a/lib/utils/extensions/static_helper.dart b/lib/utils/extensions/static_helper.dart new file mode 100644 index 0000000..8c3b152 --- /dev/null +++ b/lib/utils/extensions/static_helper.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +import '../../data/models/tank_model.dart'; + +class StaticHelper { + static double sumMonatEuro = 0.0; + static double sumMonatLiter = 0.0; + static List staticSearchList = []; + + static GetStorage dataBox = GetStorage('MyUserStorage'); + + static List listMonth = [ + {'month': 'Jänner', 'value': '01'}, + {'month': 'Februar', 'value': '02'}, + {'month': 'März', 'value': '03'}, + {'month': 'April', 'value': '04'}, + {'month': 'Mai', 'value': '05'}, + {'month': 'Juni', 'value': '06'}, + {'month': 'Juli', 'value': '07'}, + {'month': 'August', 'value': '08'}, + {'month': 'September', 'value': '09'}, + {'month': 'Oktober', 'value': '10'}, + {'month': 'November', 'value': '11'}, + {'month': 'Dezember', 'value': '12'}, + ]; + + static List staticListGetDaysInBetween( + List list, + String monthValue, + int year, + ) { + List searchList = []; + List days = []; + var dateYear = year; + var auswahlMonat = int.parse(monthValue); + var startDate = DateTime(dateYear, auswahlMonat, 1); + var endDate = DateTime(dateYear, auswahlMonat + 1, 0); + //List of Days + for (int i = 0; i <= endDate.difference(startDate).inDays; i++) { + days.add(DateTime(startDate.year, startDate.month, startDate.day + i)); + } + sumMonatEuro = 0.0; + sumMonatLiter = 0.0; + int entryCounter = 0; + for (var day in days) { + for (AppWriteTankModel model in list) { + int milliSecDate = (DateTime.parse(model.date)).microsecondsSinceEpoch; + if (milliSecDate == day.microsecondsSinceEpoch) { + entryCounter = entryCounter + 1; + model.mnIndexCount = entryCounter; + searchList.add(model); + var mnEuroGesamt = + double.parse(model.liters) * double.parse(model.pricePerLiter); + sumMonatEuro += mnEuroGesamt; + sumMonatLiter += double.parse(model.liters); + } + } + } + searchList.sort((a, b) { + final DateTime dateA = DateTime.parse(a.date); + final DateTime dateB = DateTime.parse(b.date); + return dateB.compareTo(dateA); + }); + return staticSearchList = searchList; + } + + static getMySnackeBar(String title, String message, Color backgroundColor) { + Get.snackbar( + title, + message, + backgroundColor: backgroundColor, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 4), + ); + } + + static removeFromStorage() { + dataBox.remove('sessionId'); + dataBox.remove('userId'); + dataBox.remove('userName'); + dataBox.remove('userEmail'); + } + + static bool hasData() { + return dataBox.hasData('sessionId'); + } +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 22524a6..0432e46 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -67,7 +67,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* AppwriteStarterKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = appwrite_flutter_starter_kit.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* AppwriteStarterKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tankguru_flutter_app_appwrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 4789daa..e6669ed 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -2,6 +2,14 @@ + NSLocationWhenInUseUsageDescription + This app needs access to your location to find nearby gas stations and services. + NSLocationAlwaysUsageDescription + This app needs access to your location to track your position even when the app is in the background. + UIBackgroundModes + + location + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable diff --git a/pubspec.yaml b/pubspec.yaml index 12d1ab0..9ed47c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: appwrite_flutter_starter_kit +name: tankguru_flutter_app_appwrite description: "Appwrite StarterKit in Flutter" publish_to: "none"