fertig bis auf Tankstellen und Graph
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
class Environment {
|
||||
static const String appwritePublicEndpoint = 'https://appwrite.joshihomeserver.ipv64.net/v1';
|
||||
static const String appwritePublicEndpoint =
|
||||
'https://appwrite.joshihomeserver.ipv64.net/v1';
|
||||
static const String appwriteProjectId = '6894f2b0001f127bab72';
|
||||
static const String appwriteProjectName = 'Flutter Projects';
|
||||
static const String appwriteRealtimeCollectionId = '68a22f520035a95d6666';
|
||||
static const String appwriteDatabaseId = '68a22ef90021b90f0f43';
|
||||
static const String ptvApiKey =
|
||||
'NTYxMDQ3NTY2OWI3NDI5ZGIzZWIxOWNiNTNhMDEwODY6YTQ4MTJhYzYtYmYzOC00ZmE4LTk4YzYtZDBjNzYyZTAyNjBk';
|
||||
|
||||
// Lokaler Reverse Proxy für Entwicklung (CORS-Workaround)
|
||||
static const String localProxyUrl = 'http://localhost:3000';
|
||||
static const bool useLocalProxy =
|
||||
true; // true = lokaler Proxy, false = Appwrite Function
|
||||
}
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import 'package:flutter_tank_web_app/services/appwrite_service.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../models/tank_model.dart';
|
||||
import '../pages/edit_view.dart';
|
||||
|
||||
class DetailController extends GetxController {
|
||||
late TankModel tank;
|
||||
final appwriteService = AppwriteService();
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
tank = Get.arguments as TankModel;
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void deleteEntry() {
|
||||
appwriteService
|
||||
.deleteDocumentFromCollection(tank.szDocumentId)
|
||||
.then((_) {
|
||||
Get.back(
|
||||
result: 'deleted',
|
||||
); // Zurück zur vorherigen Seite nach dem Löschen
|
||||
})
|
||||
.catchError((error) {
|
||||
Get.snackbar(
|
||||
'Fehler',
|
||||
'Eintrag konnte nicht gelöscht werden: $error',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> editEntry() async {
|
||||
await Get.offAllNamed(EditPage.namedRoute, arguments: tank);
|
||||
}
|
||||
}
|
||||
|
||||
247
lib/controller/edit_controller.dart
Normal file
247
lib/controller/edit_controller.dart
Normal file
@@ -0,0 +1,247 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_tank_web_app/pages/home_view.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
import '../models/tank_model.dart';
|
||||
import '../services/appwrite_service.dart';
|
||||
|
||||
class EditController extends GetxController {
|
||||
final AppwriteService appwriteService = AppwriteService();
|
||||
|
||||
// Form controllers
|
||||
final dateController = TextEditingController();
|
||||
final odometerController = TextEditingController();
|
||||
final litersController = TextEditingController();
|
||||
final pricePerLiterController = TextEditingController();
|
||||
final locationController = TextEditingController();
|
||||
|
||||
// Observable states
|
||||
final isLoading = false.obs;
|
||||
final isNewEntry = true.obs;
|
||||
final calculatedTotal = '0.00'.obs;
|
||||
final isLoadingLocation = false.obs;
|
||||
|
||||
TankModel? editingTankModel;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
// Check if we're editing or creating new
|
||||
if (Get.arguments != null) {
|
||||
editingTankModel = Get.arguments as TankModel;
|
||||
isNewEntry.value = false;
|
||||
_loadExistingData();
|
||||
} else {
|
||||
isNewEntry.value = true;
|
||||
_setDefaultDate();
|
||||
_requestLocation();
|
||||
}
|
||||
|
||||
// Add listeners for automatic calculation
|
||||
litersController.addListener(_calculateTotal);
|
||||
pricePerLiterController.addListener(_calculateTotal);
|
||||
}
|
||||
|
||||
void _loadExistingData() {
|
||||
dateController.text = editingTankModel!.szDate;
|
||||
odometerController.text = editingTankModel!.szOdometer;
|
||||
litersController.text = editingTankModel!.szLiters;
|
||||
pricePerLiterController.text = editingTankModel!.szPricePerLiter;
|
||||
locationController.text = editingTankModel!.szLocation;
|
||||
calculatedTotal.value = editingTankModel!.szPriceTotal;
|
||||
}
|
||||
|
||||
void _setDefaultDate() {
|
||||
final now = DateTime.now();
|
||||
dateController.text =
|
||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
Future<void> _requestLocation() async {
|
||||
bool serviceEnabled;
|
||||
LocationPermission permission;
|
||||
|
||||
try {
|
||||
isLoadingLocation.value = true;
|
||||
|
||||
// 1. Prüfen, ob Standortdienste aktiviert sind
|
||||
serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
return Future.error('Standortdienste sind deaktiviert.');
|
||||
}
|
||||
|
||||
// 2. Berechtigungen prüfen
|
||||
permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
return Future.error('Berechtigung verweigert.');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Position abrufen
|
||||
Position position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
),
|
||||
);
|
||||
|
||||
// 4. Standort über Backend-Proxy abrufen
|
||||
var lat = position.latitude;
|
||||
var lon = position.longitude;
|
||||
|
||||
print('📍 Verwende Backend-Proxy für Geocoding...');
|
||||
String location = await appwriteService.geocodeLocation(lat, lon);
|
||||
locationController.text = location;
|
||||
|
||||
// Info anzeigen basierend auf Ergebnis
|
||||
if (location.startsWith('Lat:')) {
|
||||
Get.snackbar(
|
||||
'Hinweis',
|
||||
'Adresse konnte nicht abgerufen werden. Koordinaten gespeichert.',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange[100],
|
||||
colorText: Colors.orange[900],
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Erfolg',
|
||||
'Standort: $location',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green[100],
|
||||
colorText: Colors.green[900],
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
"Fehler",
|
||||
"Standort konnte nicht abgerufen werden: $e",
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[900],
|
||||
);
|
||||
print("Fehler beim Abrufen des Standorts: $e");
|
||||
locationController.text = '';
|
||||
} finally {
|
||||
isLoadingLocation.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _calculateTotal() {
|
||||
final liters = double.tryParse(litersController.text) ?? 0.0;
|
||||
final pricePerLiter = double.tryParse(pricePerLiterController.text) ?? 0.0;
|
||||
calculatedTotal.value = (liters * pricePerLiter).toStringAsFixed(2);
|
||||
}
|
||||
|
||||
Future<void> selectDate(BuildContext context) async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: DateTime.now(),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime.now(),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: ThemeData.light().copyWith(
|
||||
colorScheme: ColorScheme.light(
|
||||
primary: Colors.blueGrey[700]!,
|
||||
onPrimary: Colors.white,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
dateController.text =
|
||||
'${picked.year}-${picked.month.toString().padLeft(2, '0')}-${picked.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveTankEntry() async {
|
||||
if (!_validateForm()) {
|
||||
Get.snackbar(
|
||||
'Validierungsfehler',
|
||||
'Bitte füllen Sie alle Pflichtfelder aus',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[900],
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
final userId = await appwriteService.getCurrentUserId();
|
||||
if (userId == null) {
|
||||
throw Exception('Benutzer nicht eingeloggt');
|
||||
}
|
||||
|
||||
final data = {
|
||||
'userId': userId,
|
||||
'date': dateController.text,
|
||||
'odometer': odometerController.text,
|
||||
'liters': litersController.text,
|
||||
'pricePerLiter': pricePerLiterController.text,
|
||||
'location': locationController.text,
|
||||
};
|
||||
|
||||
bool success;
|
||||
if (isNewEntry.value) {
|
||||
success = await appwriteService.createDocumentInCollection(data);
|
||||
} else {
|
||||
success = await appwriteService.updateDocumentInCollection(
|
||||
editingTankModel!.szDocumentId,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
Get.snackbar(
|
||||
'Erfolgreich',
|
||||
isNewEntry.value
|
||||
? 'Tankeintrag erstellt'
|
||||
: 'Tankeintrag aktualisiert',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green[100],
|
||||
colorText: Colors.green[900],
|
||||
);
|
||||
Get.offAllNamed(HomePage.namedRoute);
|
||||
} else {
|
||||
throw Exception('Speichern fehlgeschlagen');
|
||||
}
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Fehler',
|
||||
'Beim Speichern ist ein Fehler aufgetreten: $e',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red[100],
|
||||
colorText: Colors.red[900],
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _validateForm() {
|
||||
return dateController.text.isNotEmpty &&
|
||||
odometerController.text.isNotEmpty &&
|
||||
litersController.text.isNotEmpty &&
|
||||
pricePerLiterController.text.isNotEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
dateController.dispose();
|
||||
odometerController.dispose();
|
||||
litersController.dispose();
|
||||
pricePerLiterController.dispose();
|
||||
locationController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../models/tank_model.dart';
|
||||
import '../pages/detail_view.dart';
|
||||
import '../pages/edit_view.dart';
|
||||
import '../services/appwrite_service.dart';
|
||||
|
||||
class HomeController extends GetxController {
|
||||
@@ -24,7 +24,7 @@ class HomeController extends GetxController {
|
||||
|
||||
Future<void> _loadListDocument() async {
|
||||
isLoading.value = true;
|
||||
if(listTankModel.isNotEmpty){
|
||||
if (listTankModel.isNotEmpty) {
|
||||
listTankModel.clear();
|
||||
}
|
||||
var dateYear = DateTime.now().year;
|
||||
@@ -84,7 +84,14 @@ class HomeController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
void viewTankDetails(TankModel tank) {
|
||||
Get.toNamed(DetailPage.namedRoute, arguments: tank);
|
||||
Future<void> viewTankDetails(TankModel tank) async {
|
||||
var result = await Get.toNamed(DetailPage.namedRoute, arguments: tank);
|
||||
if (result == 'deleted') {
|
||||
_loadListDocument();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> navigateToAddTankEntry() async {
|
||||
await Get.offAllNamed(EditPage.namedRoute);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controller/detail_controller.dart';
|
||||
import '../controller/edit_controller.dart';
|
||||
import '../controller/home_controller.dart';
|
||||
import '../controller/login_controller.dart';
|
||||
import '../controller/signin_controller.dart';
|
||||
@@ -15,6 +16,7 @@ class SampleBindings extends Bindings {
|
||||
Get.lazyPut<SigninController>(() => SigninController());
|
||||
Get.lazyPut<HomeController>(() => HomeController());
|
||||
Get.lazyPut<DetailController>(() => DetailController());
|
||||
Get.lazyPut<EditController>(() => EditController());
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:get/get.dart';
|
||||
import '../pages/detail_view.dart';
|
||||
import '../pages/edit_view.dart';
|
||||
import 'sample_bindings.dart';
|
||||
import '../pages/home_view.dart';
|
||||
import '../pages/signin_view.dart';
|
||||
@@ -10,7 +11,7 @@ class SampleRouts {
|
||||
static List<GetPage<dynamic>> samplePages = [
|
||||
GetPage(
|
||||
name: LoginPage.namedRoute,
|
||||
page: () => const LoginPage(),
|
||||
page: () => const LoginPage(),
|
||||
binding: sampleBindings,
|
||||
),
|
||||
GetPage(
|
||||
@@ -28,6 +29,10 @@ class SampleRouts {
|
||||
page: () => const DetailPage(),
|
||||
binding: sampleBindings,
|
||||
),
|
||||
|
||||
GetPage(
|
||||
name: EditPage.namedRoute,
|
||||
page: () => const EditPage(),
|
||||
binding: sampleBindings,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var detailCtrl = controller;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.blueGrey,
|
||||
@@ -42,7 +43,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header Card mit Datum und Gesamtpreis
|
||||
DetailHeaderWidget(tank: controller.tank),
|
||||
DetailHeaderWidget(tank: detailCtrl.tank),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -53,8 +54,8 @@ class DetailPage extends GetView<DetailController> {
|
||||
DetailStatWidget(
|
||||
icon: Icons.location_on,
|
||||
label: 'Standort',
|
||||
value: controller.tank.szLocation.isNotEmpty
|
||||
? controller.tank.szLocation
|
||||
value: detailCtrl.tank.szLocation.isNotEmpty
|
||||
? detailCtrl.tank.szLocation
|
||||
: 'Nicht angegeben',
|
||||
iconColor: Colors.red,
|
||||
),
|
||||
@@ -62,7 +63,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
DetailStatWidget(
|
||||
icon: Icons.calendar_today,
|
||||
label: 'Datum',
|
||||
value: controller.tank.szDate,
|
||||
value: detailCtrl.tank.szDate,
|
||||
iconColor: Colors.blueGrey,
|
||||
),
|
||||
],
|
||||
@@ -77,7 +78,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
DetailStatWidget(
|
||||
icon: Icons.local_gas_station,
|
||||
label: 'Liter getankt',
|
||||
value: '${controller.tank.szLiters} L',
|
||||
value: '${detailCtrl.tank.szLiters} L',
|
||||
iconColor: Colors.orange,
|
||||
valueSize: 24,
|
||||
valueWeight: FontWeight.bold,
|
||||
@@ -89,7 +90,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
child: DetailStatWidget(
|
||||
icon: Icons.euro,
|
||||
label: 'Preis pro Liter',
|
||||
value: '${controller.tank.szPricePerLiter}€',
|
||||
value: '${detailCtrl.tank.szPricePerLiter}€',
|
||||
iconColor: Colors.green,
|
||||
),
|
||||
),
|
||||
@@ -98,7 +99,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
child: DetailStatWidget(
|
||||
icon: Icons.receipt,
|
||||
label: 'Gesamtpreis',
|
||||
value: '${controller.tank.szPriceTotal}€',
|
||||
value: '${detailCtrl.tank.szPriceTotal}€',
|
||||
iconColor: Colors.green[700]!,
|
||||
valueWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -117,7 +118,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
DetailStatWidget(
|
||||
icon: Icons.speed,
|
||||
label: 'Kilometerstand',
|
||||
value: '${controller.tank.szOdometer} km',
|
||||
value: '${detailCtrl.tank.szOdometer} km',
|
||||
iconColor: Colors.blue,
|
||||
valueSize: 24,
|
||||
valueWeight: FontWeight.bold,
|
||||
@@ -134,6 +135,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Bearbeiten Funktion
|
||||
detailCtrl.editEntry();
|
||||
},
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Bearbeiten'),
|
||||
@@ -152,6 +154,7 @@ class DetailPage extends GetView<DetailController> {
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// Löschen Funktion
|
||||
detailCtrl.deleteEntry();
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
label: const Text('Löschen'),
|
||||
|
||||
294
lib/pages/edit_view.dart
Normal file
294
lib/pages/edit_view.dart
Normal file
@@ -0,0 +1,294 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../controller/edit_controller.dart';
|
||||
import '../widgets/edit_form_field_widget.dart';
|
||||
|
||||
class EditPage extends GetView<EditController> {
|
||||
static const String namedRoute = '/tank-edit-page';
|
||||
const EditPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var editCtrl = controller;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.blueGrey,
|
||||
foregroundColor: Colors.white,
|
||||
title: Obx(
|
||||
() => Text(
|
||||
editCtrl.isNewEntry.value
|
||||
? 'Neuer Tankeintrag'
|
||||
: 'Tankeintrag bearbeiten',
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.blueGrey[800]!,
|
||||
Colors.blueGrey[600]!,
|
||||
Colors.blueGrey[300]!,
|
||||
Colors.blue[100]!,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Info Card
|
||||
Obx(
|
||||
() => Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
editCtrl.isNewEntry.value
|
||||
? Icons.add_circle_outline
|
||||
: Icons.edit,
|
||||
color: Colors.blueGrey[700],
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
editCtrl.isNewEntry.value
|
||||
? 'Erfassen Sie einen neuen Tankeintrag'
|
||||
: 'Bearbeiten Sie Ihren Tankeintrag',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blueGrey[900],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Form Card
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Datum
|
||||
EditFormFieldWidget(
|
||||
label: 'Datum',
|
||||
icon: Icons.calendar_today,
|
||||
controller: editCtrl.dateController,
|
||||
isReadOnly: true,
|
||||
onTap: () => editCtrl.selectDate(context),
|
||||
required: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Kilometerstand
|
||||
EditFormFieldWidget(
|
||||
label: 'Kilometerstand',
|
||||
icon: Icons.speed,
|
||||
controller: editCtrl.odometerController,
|
||||
keyboardType: TextInputType.number,
|
||||
suffix: 'km',
|
||||
required: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Liter
|
||||
EditFormFieldWidget(
|
||||
label: 'Liter',
|
||||
icon: Icons.local_gas_station,
|
||||
controller: editCtrl.litersController,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
suffix: 'L',
|
||||
required: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Preis pro Liter
|
||||
EditFormFieldWidget(
|
||||
label: 'Preis pro Liter',
|
||||
icon: Icons.euro,
|
||||
controller: editCtrl.pricePerLiterController,
|
||||
keyboardType: TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
suffix: '€/L',
|
||||
required: true,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Standort
|
||||
Obx(
|
||||
() => editCtrl.isLoadingLocation.value == true
|
||||
? CircularProgressIndicator()
|
||||
: EditFormFieldWidget(
|
||||
label: 'Standort',
|
||||
icon: Icons.location_on,
|
||||
controller: editCtrl.locationController,
|
||||
hint: 'Optional - Tankstellenstandort',
|
||||
maxLines: 2,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Berechneter Gesamtpreis
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.green[200]!,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.receipt_long,
|
||||
color: Colors.green[700],
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Gesamtpreis',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.green[900],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Obx(
|
||||
() => Text(
|
||||
'${editCtrl.calculatedTotal.value}€',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Speichern Button
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: editCtrl.isLoading.value
|
||||
? null
|
||||
: () => editCtrl.saveTankEntry(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueGrey[700],
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
disabledBackgroundColor: Colors.grey[400],
|
||||
),
|
||||
child: editCtrl.isLoading.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.save),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
editCtrl.isNewEntry.value
|
||||
? 'Speichern'
|
||||
: 'Aktualisieren',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Abbrechen Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => Get.back(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.white,
|
||||
side: const BorderSide(color: Colors.white, width: 2),
|
||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'Abbrechen',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,13 @@ class HomePage extends GetView<HomeController> {
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
backgroundColor: Colors.blueGrey,
|
||||
onPressed: () {
|
||||
homCtrl.navigateToAddTankEntry();
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'package:appwrite/models.dart';
|
||||
import 'package:appwrite/appwrite.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../config/environment.dart';
|
||||
|
||||
class AppwriteService {
|
||||
@@ -165,4 +167,47 @@ class AppwriteService {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Geocode coordinates using local proxy or Appwrite Function
|
||||
Future<String> geocodeLocation(double lat, double lon) async {
|
||||
// Wenn lokaler Proxy aktiviert ist, diesen verwenden
|
||||
if (Environment.useLocalProxy) {
|
||||
return _geocodeViaLocalProxy(lat, lon);
|
||||
}
|
||||
|
||||
// Fallback: Koordinaten zurückgeben
|
||||
return 'Lat: ${lat.toStringAsFixed(6)}, Lon: ${lon.toStringAsFixed(6)}';
|
||||
}
|
||||
|
||||
// Geocoding über lokalen Reverse Proxy
|
||||
Future<String> _geocodeViaLocalProxy(double lat, double lon) async {
|
||||
try {
|
||||
final proxyUrl = '${Environment.localProxyUrl}/?lat=$lat&lon=$lon&apiKey=${Environment.ptvApiKey}';
|
||||
|
||||
print('🔄 Verwende lokalen Proxy: ${Environment.localProxyUrl}');
|
||||
|
||||
final response = await http.get(Uri.parse(proxyUrl));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
|
||||
if (data['success'] == true) {
|
||||
final location = data['location'] as String;
|
||||
print('✅ Geocoding erfolgreich (Proxy): $location');
|
||||
return location;
|
||||
} else {
|
||||
print('❌ Geocoding fehlgeschlagen (Proxy): ${data['error']}');
|
||||
return data['fallbackLocation'] ??
|
||||
'Lat: ${lat.toStringAsFixed(6)}, Lon: ${lon.toStringAsFixed(6)}';
|
||||
}
|
||||
} else {
|
||||
print('⚠️ Proxy Response Status: ${response.statusCode}');
|
||||
return 'Lat: ${lat.toStringAsFixed(6)}, Lon: ${lon.toStringAsFixed(6)}';
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ Lokaler Proxy nicht erreichbar: $e');
|
||||
print('💡 Tipp: Starten Sie den Proxy mit: cd proxy-server && node server.js');
|
||||
return 'Lat: ${lat.toStringAsFixed(6)}, Lon: ${lon.toStringAsFixed(6)}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
lib/widgets/edit_form_field_widget.dart
Normal file
100
lib/widgets/edit_form_field_widget.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EditFormFieldWidget extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final TextEditingController controller;
|
||||
final TextInputType? keyboardType;
|
||||
final String? suffix;
|
||||
final String? hint;
|
||||
final bool isReadOnly;
|
||||
final VoidCallback? onTap;
|
||||
final int maxLines;
|
||||
final bool required;
|
||||
|
||||
const EditFormFieldWidget({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.controller,
|
||||
this.keyboardType,
|
||||
this.suffix,
|
||||
this.hint,
|
||||
this.isReadOnly = false,
|
||||
this.onTap,
|
||||
this.maxLines = 1,
|
||||
this.required = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: Colors.blueGrey[700]),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.blueGrey[900],
|
||||
),
|
||||
),
|
||||
if (required)
|
||||
Text(
|
||||
' *',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.red[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
readOnly: isReadOnly,
|
||||
onTap: onTap,
|
||||
maxLines: maxLines,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
hintText: hint,
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey[400],
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
suffixText: suffix,
|
||||
suffixStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.blueGrey[600],
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: isReadOnly ? Colors.grey[100] : Colors.white,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey[300]!),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.blueGrey[700]!, width: 2),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user