fertig bis auf Tankstellen und Graph

This commit is contained in:
2026-01-23 15:03:18 +01:00
parent 5f4f2c4379
commit d5b8df9506
27 changed files with 2198 additions and 17 deletions

View File

@@ -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
}

View File

@@ -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);
}
}

View 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}

View File

@@ -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,
),
];
}

View File

@@ -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
View 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,
),
),
),
),
],
),
),
),
),
);
}
}

View File

@@ -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(

View File

@@ -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)}';
}
}
}

View 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,
),
),
),
],
);
}
}