diff --git a/lib/controller/home_controller.dart b/lib/controller/home_controller.dart index 1da1f48..75c8ce8 100644 --- a/lib/controller/home_controller.dart +++ b/lib/controller/home_controller.dart @@ -1,6 +1,11 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:http/http.dart' as http; +import '../models/response_output.dart'; import '../helper/constants.dart'; @@ -14,74 +19,193 @@ class HomeController extends GetxController { final currentView = MainView.dashboard.obs; //--------------Settings View--------------------------- - final askiUserController = TextEditingController().obs; - final askiPasswordController = TextEditingController().obs; - final loginUrlController = TextEditingController().obs; + final userWebMethodesController = TextEditingController().obs; + final passwordWebMethodesController = TextEditingController().obs; + final powerUrlController = TextEditingController().obs; - final pvTraunDeviceController = TextEditingController().obs; - final pvTraunDeviceMeterIdPVController = TextEditingController().obs; - final pvTraunDeviceMeterIdImportController = TextEditingController().obs; - - final pvSarleinsbachDeviceController = TextEditingController().obs; - final pvSarleinsbachDeviceMeterIdPV01Controller = TextEditingController().obs; - final pvSarleinsbachDeviceMeterIdPV02Controller = TextEditingController().obs; - final pvSarleinsbachDeviceMeterIdPV03Controller = TextEditingController().obs; - final pvSarleinsbachDeviceMeterIdImportController = TextEditingController().obs; - - final pvLannachDeviceController = TextEditingController().obs; - final pvLannachDeviceMeterIdPVController = TextEditingController().obs; - final pvLannachDeviceMeterIdImportController = TextEditingController().obs; - void saveSettings() { - String askiUser = askiUserController.value.text; - String askiPassword = askiPasswordController.value.text; - String loginUrl = loginUrlController.value.text; - String powerUrl = powerUrlController.value.text; - //traun - String pvTraunDevice = pvTraunDeviceController.value.text; - int pvTraunDeviceMeterIdPV = int.parse(pvTraunDeviceMeterIdPVController.value.text); - int pvTraunDeviceMeterIdImport = int.parse(pvTraunDeviceMeterIdImportController.value.text); - //sarleinsbach - String pvSarleinsbachDevice = pvSarleinsbachDeviceController.value.text; - int pvSarleinsbachDeviceMeterIdPV01 = int.parse(pvSarleinsbachDeviceMeterIdPV01Controller.value.text); - int pvSarleinsbachDeviceMeterIdPV02 = int.parse(pvSarleinsbachDeviceMeterIdPV02Controller.value.text); - int pvSarleinsbachDeviceMeterIdPV03 = int.parse(pvSarleinsbachDeviceMeterIdPV03Controller.value.text); - int pvSarleinsbachDeviceMeterIdImport = int.parse(pvSarleinsbachDeviceMeterIdImportController.value.text); - //lannach - String pvLannachDevice = pvLannachDeviceController.value.text; - int pvLannachDeviceMeterIdPV = int.parse(pvLannachDeviceMeterIdPVController.value.text); - int pvLannachDeviceMeterIdImport = int.parse(pvLannachDeviceMeterIdImportController.value.text); + String wbUser = userWebMethodesController.value.text; + String wbPassword = passwordWebMethodesController.value.text; + String wbPowerUrl = powerUrlController.value.text; - - boxSettings.write('askiUser', askiUser); - boxSettings.write('askiPassword', askiPassword); - boxSettings.write('loginUrl', loginUrl); - boxSettings.write('powerUrl', powerUrl); - boxSettings.write('pvTraunDevice', pvTraunDevice); - boxSettings.write('pvTraunDeviceMeterIdPV', pvTraunDeviceMeterIdPV); - boxSettings.write('pvTraunDeviceMeterIdImport', pvTraunDeviceMeterIdImport); - boxSettings.write('pvSarleinsbachDevice', pvSarleinsbachDevice); - boxSettings.write('pvSarleinsbachDeviceMeterIdPV01', pvSarleinsbachDeviceMeterIdPV01); - boxSettings.write('pvSarleinsbachDeviceMeterIdPV02', pvSarleinsbachDeviceMeterIdPV02); - boxSettings.write('pvSarleinsbachDeviceMeterIdPV03', pvSarleinsbachDeviceMeterIdPV03); - boxSettings.write('pvSarleinsbachDeviceMeterIdImport', pvSarleinsbachDeviceMeterIdImport); - boxSettings.write('pvLannachDevice', pvLannachDevice); - boxSettings.write('pvLannachDeviceMeterIdPV', pvLannachDeviceMeterIdPV); - boxSettings.write('pvLannachDeviceMeterIdImport', pvLannachDeviceMeterIdImport); + boxSettings.write('wbUser', wbUser); + boxSettings.write('wbPassword', wbPassword); + boxSettings.write('wbPowerUrl', wbPowerUrl); kDisplaySnackBarGreen("Settings saved successfully!"); print('Einstellungen gespeichert:'); - print('ASKI User: $askiUser'); - print('ASKI Password: $askiPassword'); - print('Login URL: $loginUrl'); - print('Power URL: $powerUrl'); + print('Admin User: $wbUser'); + print('Admin Password: $wbPassword'); + print('Web Methodes Power URL: $wbPowerUrl'); } //------------------------------------------------------ + // http get request to fetch data from the provided URL + //------------------------------------------------------ + + final dateFromToIsoController = TextEditingController().obs; + final dateToIsoController = TextEditingController().obs; + final inputSwitchBranches = RxnString('TraunPV'); + final responseOutputTraunPV = ResponseOutput().obs; + final responseOutputTraunImport = ResponseOutput().obs; + final responseOutputSarleinsbachPV = ResponseOutput().obs; + final responseOutputSarleinsbachImport = ResponseOutput().obs; + final responseOutputLannachPV = ResponseOutput().obs; + final responseOutputLannachImport = ResponseOutput().obs; + + /// Map: Branch → (PV Observable, Import Observable) + Map> get _branchConfig => { + 'TraunPV': { + 'pv': responseOutputTraunPV, + 'import': responseOutputTraunImport, + 'pvSuffix': 'TraunPV', + 'importSuffix': 'TraunImport', + }, + 'TraunImport': { + 'pv': responseOutputTraunPV, + 'import': responseOutputTraunImport, + 'pvSuffix': 'TraunPV', + 'importSuffix': 'TraunImport', + }, + 'SarleinsbachPV': { + 'pv': responseOutputSarleinsbachPV, + 'import': responseOutputSarleinsbachImport, + 'pvSuffix': 'SarleinsbachPV', + 'importSuffix': 'SarleinsbachImport', + }, + 'SarleinsbachImport': { + 'pv': responseOutputSarleinsbachPV, + 'import': responseOutputSarleinsbachImport, + 'pvSuffix': 'SarleinsbachPV', + 'importSuffix': 'SarleinsbachImport', + }, + 'LannachPV': { + 'pv': responseOutputLannachPV, + 'import': responseOutputLannachImport, + 'pvSuffix': 'LannachPV', + 'importSuffix': 'LannachImport', + }, + 'LannachImport': { + 'pv': responseOutputLannachPV, + 'import': responseOutputLannachImport, + 'pvSuffix': 'LannachPV', + 'importSuffix': 'LannachImport', + }, + }; + + void fetchData() async { + final inputFromIso = dateFromToIsoController.value.text; + final inputToIso = dateToIsoController.value.text; + final branch = inputSwitchBranches.value; + + if (branch == null || branch.isEmpty) { + kDisplaySnackBarRed('Ungültiger Standort ausgewählt.'); + return; + } + + final baseUrl = _getBaseUrl(); + if (baseUrl.isEmpty) { + kDisplaySnackBarRed('Bitte zuerst die Server-URL eintragen.'); + return; + } + + final config = _branchConfig[branch]; + if (config == null) { + kDisplaySnackBarRed('Unbekannte Branche: $branch'); + return; + } + + final credentials = + '${userWebMethodesController.value.text}:${passwordWebMethodesController.value.text}'; + final basicAuth = 'Basic ${base64Encode(utf8.encode(credentials))}'; + final baseUri = Uri.parse(baseUrl); + + try { + // Fetch PV Daten + await _fetchAndStore(baseUri, inputFromIso, inputToIso, + config['pvSuffix'], config['pv'], basicAuth); + + // Fetch Import Daten + await _fetchAndStore(baseUri, inputFromIso, inputToIso, + config['importSuffix'], config['import'], basicAuth); + } catch (e) { + print('Error: $e'); + kDisplaySnackBarRed('Netzwerkfehler: $e'); + } + } + + /// Hilfsfunktion - reduziert Codeverdoppelung + Future _fetchAndStore( + Uri baseUri, + String inputFromIso, + String inputToIso, + String switchBranch, + dynamic responseObservable, + String basicAuth, + ) async { + final uri = baseUri.replace( + queryParameters: { + ...baseUri.queryParameters, + 'InputFromIso': inputFromIso, + 'InputToIso': inputToIso, + 'InputSwitchBranches': switchBranch, + }, + ); + + try { + final response = await http.get( + uri, + headers: {'Authorization': basicAuth, 'Accept': 'application/json'}, + ); + + final statusGroup = response.statusCode ~/ 100; + + if (statusGroup == 2) { + print('✓ $switchBranch erfolgreich geladen'); + responseObservable.value = ResponseOutput.fromJson(jsonDecode(response.body)); + } else if (statusGroup == 4) { + kDisplaySnackBarRed('Fehler 4xx: ${response.statusCode}'); + } else if (statusGroup == 5) { + kDisplaySnackBarRed('Fehler 5xx: ${response.statusCode}'); + } else { + kDisplaySnackBarRed('Fehler: ${response.statusCode}'); + } + } catch (e) { + print('Error fetching $switchBranch: $e'); + rethrow; + } + } + + /// Wählt die richtige Base-URL: + /// Web: localhost:3000 (Reverse Proxy) + /// Desktop: Direkt von Settings + String _getBaseUrl() { + final settingsUrl = powerUrlController.value.text.trim(); + + // Auf Web: verwende Reverse Proxy auf localhost:3000 + if (kIsWeb) { + return 'http://localhost:3000/invoke/IFN_ASKI_Service.flows/getAskiData?'; + } + + // Auf Desktop (Windows, Linux, macOS): direkt von Settings + return settingsUrl; + } void changeView(MainView view) { currentView.value = view; + if (currentView.value == MainView.dashboard) { + inputSwitchBranches.value = 'TraunPV'; + } else if (currentView.value == MainView.traun) { + inputSwitchBranches.value = 'TraunPV'; + } else if (currentView.value == MainView.sarleinsbach) { + inputSwitchBranches.value = 'SarleinsbachPV'; + } else if (currentView.value == MainView.lannach) { + inputSwitchBranches.value = 'LannachPV'; + } + if (currentView.value != MainView.dashboard) { + fetchData(); // Fetch data whenever the view changes + } } void toggleDrawer() { @@ -100,49 +224,41 @@ class HomeController extends GetxController { } } - @override + @override void onInit() { super.onInit(); _readStorageData(); } void _readStorageData() { - bool hasAskiUser = boxSettings.hasData('askiUser'); - if (hasAskiUser) { - askiUserController.value.text = boxSettings.read('askiUser'); + bool hasWbUser = boxSettings.hasData('wbUser'); + if (hasWbUser) { + userWebMethodesController.value.text = boxSettings.read('wbUser'); } - bool hasAskiPassword = boxSettings.hasData('askiPassword'); - if (hasAskiPassword) { - askiPasswordController.value.text = boxSettings.read('askiPassword'); + bool hasWbPassword = boxSettings.hasData('wbPassword'); + if (hasWbPassword) { + passwordWebMethodesController.value.text = boxSettings.read('wbPassword'); } - bool hasLoginUrl = boxSettings.hasData('loginUrl'); - if (hasLoginUrl) { - loginUrlController.value.text = boxSettings.read('loginUrl'); - } - bool hasPowerUrl = boxSettings.hasData('powerUrl'); - if (hasPowerUrl) { - powerUrlController.value.text = boxSettings.read('powerUrl'); + bool hasWbPowerUrl = boxSettings.hasData('wbPowerUrl'); + if (hasWbPowerUrl) { + powerUrlController.value.text = boxSettings.read('wbPowerUrl'); } + //fromDateTimeIso and toDateTimeIso + dateFromToIsoController.value.text = DateTime( + DateTime.now().year, + DateTime.now().month, + DateTime.now().day, + ).toIso8601String(); + dateToIsoController.value.text = DateTime.now().toIso8601String(); } @override void onClose() { - askiUserController.close(); - askiPasswordController.close(); - loginUrlController.close(); + userWebMethodesController.close(); powerUrlController.close(); - pvTraunDeviceController.close(); - pvTraunDeviceMeterIdPVController.close(); - pvTraunDeviceMeterIdImportController.close(); - pvSarleinsbachDeviceController.close(); - pvSarleinsbachDeviceMeterIdPV01Controller.close(); - pvSarleinsbachDeviceMeterIdPV02Controller.close(); - pvSarleinsbachDeviceMeterIdPV03Controller.close(); - pvSarleinsbachDeviceMeterIdImportController.close(); - pvLannachDeviceController.close(); - pvLannachDeviceMeterIdPVController.close(); - pvLannachDeviceMeterIdImportController.close(); + passwordWebMethodesController.close(); + dateFromToIsoController.close(); + dateToIsoController.close(); super.onClose(); } - } diff --git a/lib/helper/constants.dart b/lib/helper/constants.dart index 12ea761..5c06dde 100644 --- a/lib/helper/constants.dart +++ b/lib/helper/constants.dart @@ -3,11 +3,15 @@ import 'package:get/get.dart'; var kFontSizeDouble = 15.0; -var kTextSyleLittleGrey = - TextStyle(fontSize: 12.0, color: Colors.grey.shade800); +var kTextSyleLittleGrey = TextStyle( + fontSize: 12.0, + color: Colors.grey.shade800, +); -var kTextSyleHeaderGrey = - TextStyle(fontSize: 25.0, color: Colors.grey.shade800); +var kTextSyleHeaderGrey = TextStyle( + fontSize: 25.0, + color: Colors.grey.shade800, +); var kElevatetButtonStyleDrawer = ElevatedButton.styleFrom( backgroundColor: Colors.blueGrey, @@ -28,24 +32,30 @@ var kInputDecorationDropDownMenue = InputDecoration( ); SnackbarController kDisplaySnackBarRed(String szInfoText) { - return Get.snackbar('Info', szInfoText, - backgroundColor: Colors.red[600], - snackPosition: SnackPosition.TOP, - colorText: Colors.white, - snackStyle: SnackStyle.GROUNDED); + return Get.snackbar( + 'Info', + szInfoText, + backgroundColor: Colors.red[600], + snackPosition: SnackPosition.TOP, + colorText: Colors.white, + snackStyle: SnackStyle.GROUNDED, + ); } SnackbarController kDisplaySnackBarGreen(String szInfoText) { - return Get.snackbar('Info', szInfoText, - backgroundColor: Colors.green[600], - snackPosition: SnackPosition.TOP, - colorText: Colors.black, - snackStyle: SnackStyle.GROUNDED); + return Get.snackbar( + 'Info', + szInfoText, + backgroundColor: Colors.green[600], + snackPosition: SnackPosition.TOP, + colorText: Colors.black, + snackStyle: SnackStyle.GROUNDED, + ); } //Setting Wait for Delete List> kListMapSetting = [ - {'ContainerID': '', 'Info': ''} + {'ContainerID': '', 'Info': ''}, ]; //Switching PROD/DEV diff --git a/lib/models/response_output.dart b/lib/models/response_output.dart new file mode 100644 index 0000000..8dc7ed2 --- /dev/null +++ b/lib/models/response_output.dart @@ -0,0 +1,81 @@ +class ResponseOutput { + DataResult? dataResult; + + ResponseOutput({this.dataResult}); + + ResponseOutput.fromJson(Map json) { + dataResult = json['DataResult'] != null + ? DataResult.fromJson(json['DataResult']) + : null; + } + + Map toJson() { + final Map data = {}; + if (dataResult != null) { + data['DataResult'] = dataResult!.toJson(); + } + return data; + } +} + +class DataResult { + String? httpStatus; + String? orderType; + String? location; + int? meterId; + List? values; + + DataResult( + {this.httpStatus, + this.orderType, + this.location, + this.meterId, + this.values}); + + DataResult.fromJson(Map json) { + httpStatus = json['HttpStatus']; + orderType = json['OrderType']; + location = json['Location']; + meterId = json['meterId']; + if (json['values'] != null) { + values = []; + json['values'].forEach((v) { + values!.add(Values.fromJson(v)); + }); + } + } + + Map toJson() { + final Map data = {}; + data['HttpStatus'] = httpStatus; + data['OrderType'] = orderType; + data['Location'] = location; + data['meterId'] = meterId; + if (values != null) { + data['values'] = values!.map((v) => v.toJson()).toList(); + } + return data; + } +} + +class Values { + String? datetime; + double? value; + Null tariff; + + Values({this.datetime, this.value, this.tariff}); + + Values.fromJson(Map json) { + datetime = json['datetime']; + value = json['value']; + tariff = json['tariff']; + } + + Map toJson() { + final Map data = {}; + data['datetime'] = datetime; + data['value'] = value; + data['tariff'] = tariff; + return data; + } +} diff --git a/lib/widgets/comparison_bar_chart.dart b/lib/widgets/comparison_bar_chart.dart new file mode 100644 index 0000000..ecdcefc --- /dev/null +++ b/lib/widgets/comparison_bar_chart.dart @@ -0,0 +1,333 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import '../models/response_output.dart'; + +/// Wiederverwendbares Balkendiagramm Widget für PV/Import Vergleich +/// +/// Zeigt zwei Datensätze (z.B. TraunPV + TraunImport) als nebeneinander angeordnete Balken +/// - PV Daten in Orange +/// - Import Daten in Rot +/// - Y-Achse: kW Werte +/// - X-Achse: Datetime (als Uhrzeit angezeigt) +class ComparisonBarChart extends StatelessWidget { + final ResponseOutput pvData; + final ResponseOutput importData; + final String title; + + const ComparisonBarChart({ + required this.pvData, + required this.importData, + required this.title, + super.key, + }); + + @override + Widget build(BuildContext context) { + final pvValues = pvData.dataResult?.values ?? []; + final importValues = importData.dataResult?.values ?? []; + + // Erstelle eine Map mit datetime als Schlüssel + final Map pvMap = { + for (var v in pvValues) (v.datetime ?? ''): (v.value ?? 0.0), + }; + final Map importMap = { + for (var v in importValues) (v.datetime ?? ''): (v.value ?? 0.0), + }; + + // Alle eindeutigen datetimes sammeln und sortieren + final allDatetimes = {...pvMap.keys, ...importMap.keys}.toList(); + allDatetimes.sort(); + + // Begrenzen auf die letzten 20 Einträge für bessere Lesbarkeit + // final displayDatetimes = allDatetimes.length > 20 + // ? allDatetimes.sublist(allDatetimes.length - 20) + // : allDatetimes; + final displayDatetimes = allDatetimes; + + if (displayDatetimes.isEmpty) { + return const Center(child: Text('Keine Daten verfügbar')); + } + + // Berechne Maximalwert + final allValues = [...pvMap.values, ...importMap.values]; + final maxValue = allValues.fold(0.0, (a, b) => a > b ? a : b); + + // Maxima für PV und Import separat + double maxPvValue = 0; + int maxPvIndex = 0; + double maxImportValue = 0; + int maxImportIndex = 0; + + for (int i = 0; i < displayDatetimes.length; i++) { + final dt = displayDatetimes[i]; + final pv = pvMap[dt] ?? 0.0; + final imp = importMap[dt] ?? 0.0; + + if (pv > maxPvValue) { + maxPvValue = pv; + maxPvIndex = i; + } + if (imp > maxImportValue) { + maxImportValue = imp; + maxImportIndex = i; + } + } + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 16), + Text( + title, + style: Theme.of(context).textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + // Chart mit fester Höhe + SizedBox( + height: 500, + child: BarChart( + BarChartData( + alignment: BarChartAlignment.spaceAround, + maxY: maxValue * 1.02, + minY: 0, + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: maxValue * 0.2, + ), + titlesData: FlTitlesData( + show: true, + topTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 40, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + + // PV Maximum - oranger Pin + if (index == maxPvIndex) { + return Transform.translate( + offset: const Offset(-5, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.orange, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + blurRadius: 4, + ), + ], + ), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + child: Text( + maxPvValue.toStringAsFixed(0), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + // Pin nach unten + CustomPaint( + size: const Size(20, 10), + painter: PinPainter(Colors.orange), + ), + ], + ), + ); + } + + // Import Maximum - roter Pin + if (index == maxImportIndex) { + return Transform.translate( + offset: const Offset(5, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: Colors.red, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.3), + blurRadius: 4, + ), + ], + ), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + child: Text( + maxImportValue.toStringAsFixed(0), + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + // Pin nach unten + CustomPaint( + size: const Size(20, 10), + painter: PinPainter(Colors.red), + ), + ], + ), + ); + } + + return const SizedBox(); + }, + ), + ), + rightTitles: AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 50, + getTitlesWidget: (value, meta) { + return Text( + '${value.toInt()} kW', + style: const TextStyle(fontSize: 12), + ); + }, + ), + ), + bottomTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + reservedSize: 60, + getTitlesWidget: (value, meta) { + final index = value.toInt(); + if (index < 0 || index >= displayDatetimes.length) { + return const SizedBox(); + } + final datetime = displayDatetimes[index]; + // Extrahiere nur Uhrzeit (HH:mm) + final timePart = datetime.contains('T') + ? datetime.split('T').last.substring(0, 5) + : datetime.substring(0, 5); + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: RotatedBox( + quarterTurns: 1, + child: Text( + timePart, + style: const TextStyle(fontSize: 10), + ), + ), + ); + }, + ), + ), + ), + barGroups: _generateBarGroups( + displayDatetimes, + pvMap, + importMap, + ), + borderData: FlBorderData(show: true), + ), + ), + ), + const SizedBox(height: 24), + // Legende + Wrap( + alignment: WrapAlignment.center, + spacing: 24, + runSpacing: 8, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container(width: 16, height: 16, color: Colors.orange), + const SizedBox(width: 8), + const Text('PV-Anlage(kW)'), + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container(width: 16, height: 16, color: Colors.red), + const SizedBox(width: 8), + const Text('Netzbezug(kW)'), + ], + ), + ], + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + List _generateBarGroups( + List datetimes, + Map pvMap, + Map importMap, + ) { + return List.generate(datetimes.length, (index) { + final datetime = datetimes[index]; + final pvValue = pvMap[datetime] ?? 0.0; + final importValue = importMap[datetime] ?? 0.0; + + return BarChartGroupData( + x: index, + barRods: [ + // Orange Bar für PV + BarChartRodData( + toY: pvValue, + color: Colors.orange, + width: 8, + borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), + ), + // Roter Bar für Import + BarChartRodData( + toY: importValue, + color: Colors.red, + width: 8, + borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), + ), + ], + ); + }); + } +} + +class PinPainter extends CustomPainter { + final Color color; + + PinPainter(this.color); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final path = Path(); + path.moveTo(size.width / 2, size.height); + path.lineTo(0, 0); + path.lineTo(size.width, 0); + path.close(); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(PinPainter oldDelegate) => false; +} diff --git a/lib/widgets/pv_drop_down.dart b/lib/widgets/pv_drop_down.dart new file mode 100644 index 0000000..e27e347 --- /dev/null +++ b/lib/widgets/pv_drop_down.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class StaticLocationDropdown extends StatelessWidget { + const StaticLocationDropdown({super.key, required this.selectedValue}); + + final RxnString selectedValue; + + static const List _locations = [ + 'TraunPV', + 'TraunImport', + 'SarleinsbachPV', + 'SarleinsbachImport', + 'LannachPV', + 'LannachImport', + ]; + + @override + Widget build(BuildContext context) { + return Obx(() => DropdownButtonFormField( + initialValue: selectedValue.value ?? _locations.first, + decoration: const InputDecoration( + labelText: 'Select Location / Data Source', + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(horizontal: 12, vertical: 15), + ), + items: _locations.map((String location) { + return DropdownMenuItem( + value: location, + child: Text(location), + ); + }).toList(), + onChanged: (String? newValue) { + selectedValue.value = newValue; + }, + )); + } +} \ No newline at end of file diff --git a/lib/widgets/views/dashboard.dart b/lib/widgets/views/dashboard.dart index 6913844..16993c2 100644 --- a/lib/widgets/views/dashboard.dart +++ b/lib/widgets/views/dashboard.dart @@ -11,38 +11,69 @@ class DashboardView extends StatelessWidget { var hCtrl = Get.find(); return Center( child: Container( - margin: const EdgeInsets.all(20), + padding: const EdgeInsets.all(20), width: 1000, - child: Wrap( - spacing: 20, - runSpacing: 20, - children: [ - InkWell( - onTap: () { - hCtrl.changeView(MainView.dashboard); - }, - child: Image.asset('assets/images/AskiDashboard.png',width: 450), + child: Wrap( + spacing: 20, + runSpacing: 20, + children: [ + Padding( + padding: const EdgeInsets.only(right: 35.0), + child: TextField( + controller: hCtrl.dateFromToIsoController.value, + decoration: InputDecoration( + labelText: 'Input From ISO', + border: OutlineInputBorder(), + hintText: 'Enter Input From ISO', + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 35.0), + child: TextField( + controller: hCtrl.dateToIsoController.value, + decoration: InputDecoration( + labelText: 'Input To ISO', + border: OutlineInputBorder(), + hintText: 'Enter Input To ISO', + ), + ), + ), + // Padding( + // padding: const EdgeInsets.only(right: 35.0), + // child: StaticLocationDropdown( + // selectedValue: hCtrl.inputSwitchBranches, + // ), + // ), + InkWell( + onTap: () { + hCtrl.changeView(MainView.dashboard); + }, + child: Image.asset('assets/images/AskiDashboard.png', width: 450), + ), + InkWell( + onTap: () { + hCtrl.changeView(MainView.traun); + }, + child: Image.asset('assets/images/AskiTraun.png', width: 450), + ), + InkWell( + onTap: () { + hCtrl.changeView(MainView.sarleinsbach); + }, + child: Image.asset( + 'assets/images/AskiSarleinsbach.png', + width: 450, ), - InkWell( - onTap: () { - hCtrl.changeView(MainView.traun); - }, - child: Image.asset('assets/images/AskiTraun.png',width: 450), - ), - InkWell( - onTap: () { - hCtrl.changeView(MainView.sarleinsbach); - }, - child: Image.asset('assets/images/AskiSarleinsbach.png',width: 450), - ), - InkWell( - onTap: () { - hCtrl.changeView(MainView.lannach); - }, - child: Image.asset('assets/images/AskiLannach.png',width: 450), - ), - ], - ), + ), + InkWell( + onTap: () { + hCtrl.changeView(MainView.lannach); + }, + child: Image.asset('assets/images/AskiLannach.png', width: 450), + ), + ], + ), ), ); } diff --git a/lib/widgets/views/lannach.dart b/lib/widgets/views/lannach.dart index abf2447..79861e7 100644 --- a/lib/widgets/views/lannach.dart +++ b/lib/widgets/views/lannach.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../controller/home_controller.dart'; +import '../comparison_bar_chart.dart'; class LannachView extends StatelessWidget { const LannachView({super.key}); @@ -23,11 +24,19 @@ class LannachView extends StatelessWidget { }, ) ), - body: const Center( - child: Text( - "PV Lannach", - style: TextStyle(fontSize: 30), - ), + body: Obx( + () => hCtrl.responseOutputLannachPV.value.dataResult?.values?.isEmpty ?? true + ? const Center( + child: Text( + 'Keine Daten verfügbar', + style: TextStyle(fontSize: 18), + ), + ) + : ComparisonBarChart( + pvData: hCtrl.responseOutputLannachPV.value, + importData: hCtrl.responseOutputLannachImport.value, + title: 'LannachPV/Import Vergleich', + ), ), ); } diff --git a/lib/widgets/views/sarleinsbach.dart b/lib/widgets/views/sarleinsbach.dart index ca916b0..efe17ee 100644 --- a/lib/widgets/views/sarleinsbach.dart +++ b/lib/widgets/views/sarleinsbach.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../controller/home_controller.dart'; +import '../comparison_bar_chart.dart'; class SarleinsbachView extends StatelessWidget { const SarleinsbachView({super.key}); @@ -24,11 +25,19 @@ class SarleinsbachView extends StatelessWidget { }, ) ), - body: const Center( - child: Text( - "PV Sarleinsbach", - style: TextStyle(fontSize: 30), - ), + body: Obx( + () => hCtrl.responseOutputSarleinsbachPV.value.dataResult?.values?.isEmpty ?? true + ? const Center( + child: Text( + 'Keine Daten verfügbar', + style: TextStyle(fontSize: 18), + ), + ) + : ComparisonBarChart( + pvData: hCtrl.responseOutputSarleinsbachPV.value, + importData: hCtrl.responseOutputSarleinsbachImport.value, + title: 'SarleinsbachPV/Import Vergleich', + ), ), ); } diff --git a/lib/widgets/views/settings.dart b/lib/widgets/views/settings.dart index 0811d0f..b34e982 100644 --- a/lib/widgets/views/settings.dart +++ b/lib/widgets/views/settings.dart @@ -11,7 +11,7 @@ class SettingsView extends StatelessWidget { var hCtrl = Get.find(); return Scaffold( appBar: AppBar( - title: const Text('Data Settings'), + title: const Text('System Settings'), centerTitle: true, backgroundColor: Colors.blueGrey, foregroundColor: Colors.white, @@ -41,43 +41,34 @@ class SettingsView extends StatelessWidget { children: [ SizedBox(height: 10), Text( - "Please enter User and Password", + "Please enter User and Password for Web Methodes", style: TextStyle(fontSize: 18), ), SizedBox(height: 10), TextField( - controller: hCtrl.askiUserController.value, + controller: hCtrl.userWebMethodesController.value, decoration: InputDecoration( labelText: 'Input User', border: OutlineInputBorder(), - hintText: 'Enter ASKI User', + hintText: 'Enter Admin User', ), ), SizedBox(height: 10), TextField( - controller: hCtrl.askiPasswordController.value, + controller: hCtrl.passwordWebMethodesController.value, obscureText: true, decoration: InputDecoration( labelText: 'Input Password', border: OutlineInputBorder(), - hintText: 'Enter ASKI Password', + hintText: 'Enter Admin Password', ), ), SizedBox(height: 10), Text( - "Please enter the URLs for the ASKI login and power data", + "Please enter the URLs for WebMethodes PV data Url", style: TextStyle(fontSize: 18), ), SizedBox(height: 10), - TextField( - controller: hCtrl.loginUrlController.value, - decoration: InputDecoration( - labelText: 'Login URL', - border: OutlineInputBorder(), - hintText: 'Enter ASKI Login URL', - ), - ), - SizedBox(height: 10), TextField( controller: hCtrl.powerUrlController.value, decoration: InputDecoration( @@ -85,21 +76,7 @@ class SettingsView extends StatelessWidget { border: OutlineInputBorder(), hintText: 'Enter ASKI Power URL', ), - ), - SizedBox(height: 10), - Text( - "Please enter the device for PV Traun and PV Sarleinsbach and PV Lannach", - style: TextStyle(fontSize: 18), - ), - SizedBox(height: 10), - TextField( - controller: hCtrl.pvTraunDeviceController.value, - decoration: InputDecoration( - labelText: 'PV Traun Device', - border: OutlineInputBorder(), - hintText: 'Enter PV Traun Device', - ), - ), + ) ], ), ), diff --git a/lib/widgets/views/traun.dart b/lib/widgets/views/traun.dart index 16a6437..25676a3 100644 --- a/lib/widgets/views/traun.dart +++ b/lib/widgets/views/traun.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../controller/home_controller.dart'; +import '../comparison_bar_chart.dart'; class TraunView extends StatelessWidget { const TraunView({super.key}); @@ -23,11 +24,19 @@ class TraunView extends StatelessWidget { }, ) ), - body: const Center( - child: Text( - "PV Traun", - style: TextStyle(fontSize: 30), - ), + body: Obx( + () => hCtrl.responseOutputTraunPV.value.dataResult?.values?.isEmpty ?? true + ? const Center( + child: Text( + 'Keine Daten verfügbar', + style: TextStyle(fontSize: 18), + ), + ) + : ComparisonBarChart( + pvData: hCtrl.responseOutputTraunPV.value, + importData: hCtrl.responseOutputTraunImport.value, + title: 'TraunPV/Import Vergleich', + ), ), ); } diff --git a/pubspec.lock b/pubspec.lock index 8d12e21..a573617 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.9" + equatable: + dependency: transitive + description: + name: equatable + sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2" + url: "https://pub.dev" + source: hosted + version: "2.1.0" fake_async: dependency: transitive description: @@ -89,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb + url: "https://pub.dev" + source: hosted + version: "0.68.0" flutter: dependency: "direct main" description: flutter @@ -131,6 +147,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" intl: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index d7e96f9..a6ca27c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,10 +10,12 @@ environment: dependencies: cupertino_icons: ^1.0.8 + fl_chart: ^0.68.0 flutter: sdk: flutter get: ^4.7.3 get_storage: ^2.1.1 + http: ^1.6.0 intl: ^0.20.3 dev_dependencies: