finishCommit

This commit is contained in:
atseirjo
2026-07-23 09:17:56 +02:00
parent 9f7ee3f356
commit c8a3cc9812
12 changed files with 822 additions and 175 deletions
+333
View File
@@ -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<String, double> pvMap = {
for (var v in pvValues) (v.datetime ?? ''): (v.value ?? 0.0),
};
final Map<String, double> 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<BarChartGroupData> _generateBarGroups(
List<String> datetimes,
Map<String, double> pvMap,
Map<String, double> 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;
}
+38
View File
@@ -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<String> _locations = [
'TraunPV',
'TraunImport',
'SarleinsbachPV',
'SarleinsbachImport',
'LannachPV',
'LannachImport',
];
@override
Widget build(BuildContext context) {
return Obx(() => DropdownButtonFormField<String>(
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<String>(
value: location,
child: Text(location),
);
}).toList(),
onChanged: (String? newValue) {
selectedValue.value = newValue;
},
));
}
}
+61 -30
View File
@@ -11,38 +11,69 @@ class DashboardView extends StatelessWidget {
var hCtrl = Get.find<HomeController>();
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),
),
],
),
),
);
}
+14 -5
View File
@@ -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',
),
),
);
}
+14 -5
View File
@@ -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',
),
),
);
}
+8 -31
View File
@@ -11,7 +11,7 @@ class SettingsView extends StatelessWidget {
var hCtrl = Get.find<HomeController>();
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',
),
),
)
],
),
),
+14 -5
View File
@@ -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',
),
),
);
}