Added tests, updated widget structure

main
henryhdr 2024-06-12 12:48:30 +02:00
parent 880696ad18
commit a9e7380d69
14 changed files with 1070 additions and 488 deletions

View File

@ -2,3 +2,9 @@ enum PayoutInterval{
yearly,
monthly
}
enum CalculationPerformed{
noFirstTimeItLoaded,
no,
yes
}

View File

@ -6,6 +6,7 @@ import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/widgets/input_widget.dart';
import 'package:flutter_application_1/widgets/interval_widget.dart';
import 'package:flutter_application_1/widgets/result_widget.dart';
import 'package:flutter_application_1/widgets/error_widget.dart';
void main() {
runApp(const MyApp());
@ -17,7 +18,7 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'Zinseszinsrechner',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: CupertinoColors.white, background: CupertinoColors.white),
useMaterial3: true,
@ -157,7 +158,7 @@ class _MyHomePageState extends State<MyHomePage> {
});
}
bool calculationPerformed = false;
CalculationPerformed _isCalculated = CalculationPerformed.noFirstTimeItLoaded;
@override
Widget build(BuildContext context) {
@ -170,10 +171,38 @@ class _MyHomePageState extends State<MyHomePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildInputWidget('Anfangskapital', _initialCapitalController, _initialCapitalFocusNode, _isInitialCapitalEntered, '', 'Das Anfangskapital ist der Betrag, den Sie zu Beginn Ihrer Anlage haben.'),
buildInputWidget('Monatliche Sparrate', _monthlySavingsRateController, _monthlySavingsRateFocusNode, _isMonthlySavingsRateEntered, '', 'Die monatliche Sparrate ist der Betrag, den Sie jeden Monat zu Ihrer Investition hinzufügen.'),
buildInputWidget('Jährlicher Zinssatz', _interestRateController, _interestRateFocusNode, _isInterestRateEntered, '%', 'Der jährliche Zinssatz ist der Prozentsatz, zu dem Ihr investiertes Kapital jedes Jahr wächst.'),
buildInputWidget('Anlagezeitraum', _timeController, _timeFocusNode, _isTimeEntered, 'Jahre', 'Der Anlagezeitraum ist die Zeitspanne, für die Sie planen, Ihr Geld anzulegen.'),
InputWidget(
label: 'Anfangskapital',
controller: _initialCapitalController,
focusNode: _initialCapitalFocusNode,
isValid: _isInitialCapitalEntered,
suffixText: '',
tooltipText: 'Das Anfangskapital ist der Betrag, den Sie zu Beginn Ihrer Anlage haben.'
),
InputWidget(
label: 'Monatliche Sparrate',
controller: _monthlySavingsRateController,
focusNode: _monthlySavingsRateFocusNode,
isValid: _isMonthlySavingsRateEntered,
suffixText: '',
tooltipText: 'Die monatliche Sparrate ist der Betrag, den Sie jeden Monat zu Ihrer Investition hinzufügen.'
),
InputWidget(
label: 'Jährlicher Zinssatz',
controller: _interestRateController,
focusNode: _interestRateFocusNode,
isValid: _isInterestRateEntered,
suffixText: '%',
tooltipText: 'Der jährliche Zinssatz ist der Prozentsatz, zu dem Ihr investiertes Kapital jedes Jahr wächst.'
),
InputWidget(
label: 'Anlagezeitraum',
controller: _timeController,
focusNode: _timeFocusNode,
isValid: _isTimeEntered,
suffixText: 'Jahre',
tooltipText: 'Der Anlagezeitraum ist die Zeitspanne, für die Sie planen, Ihr Geld anzulegen.'
),
IntervalWidget(
selectedInterval: translateInterval(_payoutInterval),
onChanged: (newInterval) {
@ -184,13 +213,29 @@ class _MyHomePageState extends State<MyHomePage> {
),
ElevatedButton(
onPressed: () {
if (_isInitialCapitalEntered &&
_isMonthlySavingsRateEntered &&
_isInterestRateEntered &&
_isTimeEntered) {
setInitialCapital();
setMonthlySavingsRate();
setInterestRate();
setTime();
_investedMoney = calculateInvestedMoney(_initialCapital, _monthlySavingsRate, _time, _investedMoneyList);
_compoundInterest = calculateCompoundInterest(_initialCapital, _monthlySavingsRate, _interestRate, _time, _payoutInterval, _investedMoneyList, _compoundInterestList);
calculationPerformed = true;
_compoundInterest = calculateCompoundInterest(
_initialCapital,
_monthlySavingsRate,
_interestRate,
_time,
_payoutInterval,
_investedMoneyList,
_compoundInterestList
);
_isCalculated = CalculationPerformed.yes;
} else {
_isCalculated = CalculationPerformed.no;
}
setState(() {});
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(CupertinoColors.black),
@ -209,8 +254,21 @@ class _MyHomePageState extends State<MyHomePage> {
),
),
const SizedBox(height: 20),
if (calculationPerformed)
buildResultWidget(context, '$_compoundInterest', '$_investedMoney', '$_time', '$_monthlySavingsRate', '$_interestRate', _payoutInterval, _investedMoneyList, _compoundInterestList),
if(_isCalculated == CalculationPerformed.yes)
ResultWidget(
compoundInterest: '$_compoundInterest',
investedMoney: '$_investedMoney',
time: '$_time',
monthlySavingsRate: '$_monthlySavingsRate',
interestRate: '$_interestRate',
payoutInterval: _payoutInterval,
investedMoneyList: _investedMoneyList,
compoundInterestList: _compoundInterestList,
),
if(_isCalculated == CalculationPerformed.no)
const ErrWidget(
errorMessage: 'Ungültige Eingabe',
),
],
),
)

View File

@ -0,0 +1,179 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_application_1/widgets/chart_widget.dart';
// Widget für die Seite, die das gestapelte Säulendiagramm anzeigt
class ChartPage extends StatelessWidget {
final List<double> investedMoneyList; // Liste der investierten Geldbeträge
final List<double> compoundInterestList; // Liste der zusammengesetzten Zinsen
const ChartPage({
super.key,
required this.investedMoneyList,
required this.compoundInterestList,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(
left: 10,
right: 10,
top: MediaQuery.of(context).padding.top + 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(CupertinoIcons.chevron_left, size: 15),
onPressed: () {
Navigator.pop(context); // Zurück zur vorherigen Seite
},
),
const Text(
'Grafik',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40),
],
),
),
),
// Anzeige des gestapelten Säulendiagramms
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: StackedColumnChart(
lowerValues: investedMoneyList,
upperValues: compoundInterestList,
),
),
),
// Tabelle mit Einzahlungen, Zinsen und Endkapital
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Container(
decoration: BoxDecoration(
color: CupertinoColors.black,
borderRadius: BorderRadius.circular(5),
),
clipBehavior: Clip.antiAlias,
child: Table(
columnWidths: const {
0: FixedColumnWidth(60),
1: FlexColumnWidth(),
2: FlexColumnWidth(),
3: FlexColumnWidth(),
},
border: TableBorder.symmetric(
inside: const BorderSide(
color: CupertinoColors.white,
width: 1,
),
),
children: [
// Tabellenkopf
const TableRow(
decoration: BoxDecoration(
color: CupertinoColors.darkBackgroundGray,
),
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Jahr',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Einzahlungen',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Zinsen',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Endkapital',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
],
),
// Datenzeilen
for (int i = 0; i < investedMoneyList.length; i++)
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${i + 1}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${investedMoneyList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i] + investedMoneyList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
],
),
],
),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,55 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/widgets/milestone_timeline_widget.dart';
// Widget für die Seite, die die Meilenstein-Timeline anzeigt
class MilestonePage extends StatelessWidget {
final String compoundInterest; // Gesamte zusammengesetzte Zinsen
final String investedMoney; // Gesamte investierte Geldmenge
final List<Map<String, dynamic>> milestoneList; // Liste von Meilensteinen
const MilestonePage({
super.key,
required this.compoundInterest,
required this.investedMoney,
required this.milestoneList,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(left: 10, right: 10, top: MediaQuery.of(context).padding.top + 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(CupertinoIcons.chevron_left, size: 15),
onPressed: () {
Navigator.pop(context); // Zurück zur vorherigen Seite
},
),
const Text(
'Meilensteine',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40),
],
),
),
),
// Anzeige der Meilenstein-Timeline
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: MilestoneTimeline(milestones: milestoneList, totalInterest: double.parse(compoundInterest) - double.parse(investedMoney)),
),
),
],
),
);
}
}

View File

@ -22,6 +22,7 @@ void restoreDefaultValuesIfEmpty(TextEditingController controller) {
}
}
// Übersetzt das Auszahlungsintervall
String translateInterval(PayoutInterval interval) {
switch (interval) {
case PayoutInterval.yearly:

View File

@ -1,8 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
// Widget zur Erstellung eines gestapelten Säulendiagramms
// Widget für die Erstellung eines gestapelten Säulendiagramms
class StackedColumnChart extends StatelessWidget {
final List<double> lowerValues; // Liste der unteren Werte für das Diagramm
final List<double> upperValues; // Liste der oberen Werte für das Diagramm
@ -44,7 +43,6 @@ class StackedColumnChart extends StatelessWidget {
color: CupertinoColors.systemGreen.highContrastColor,
),
],
// Konfiguration für die Interaktivität des Diagramms
trackballBehavior: TrackballBehavior(
enable: true,
tooltipSettings: const InteractiveTooltip(enable: true),
@ -75,160 +73,8 @@ class StackedColumnChart extends StatelessWidget {
// Klasse zur Modellierung der Datenpunkte im Diagramm
class SalesData {
final int year;
final double value;
final int year; // Jahr des Datenpunkts
final double value; // Wert des Datenpunkts
SalesData(this.year, this.value);
}
// Widget, welches das Diagramm auf eine neue Seite auslagert
@override
Widget buildChartPage(BuildContext context, List<double> investedMoneyList, List<double> compoundInterestList) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(left: 10, right: 10, top: MediaQuery.of(context).padding.top + 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(CupertinoIcons.chevron_left, size: 15), // Zurück-Pfeil
onPressed: () {
Navigator.pop(context);
},
),
const Text(
'Grafik',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40), // Platzhalter für zentrierte Ausrichtung
],
),
),
),
SliverToBoxAdapter( // Diagramm Anzeigen
child: Padding(
padding: const EdgeInsets.all(10.0),
child: StackedColumnChart(
lowerValues: investedMoneyList,
upperValues: compoundInterestList,
),
),
),
SliverToBoxAdapter( // Tabelle mit allen Werten anzeigen
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Container(
decoration: BoxDecoration(
color: CupertinoColors.black,
borderRadius: BorderRadius.circular(5),
),
clipBehavior: Clip.antiAlias,
child: Table(
columnWidths: const {
0: FixedColumnWidth(60), // Spaltenbreite für die erste Spalte verringern
1: FlexColumnWidth(),
2: FlexColumnWidth(),
3: FlexColumnWidth(),
},
border: TableBorder.symmetric(
inside: const BorderSide(color: CupertinoColors.white, width: 1),
),
children: [
const TableRow(
decoration: BoxDecoration(color: CupertinoColors.darkBackgroundGray),
children: [
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Jahr',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Einzahlungen',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Zinsen',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Padding(
padding: EdgeInsets.all(8.0),
child: Text(
'Endkapital',
style: TextStyle(
color: CupertinoColors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
],
),
for (int i = 0; i < investedMoneyList.length; i++)
TableRow(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${i + 1}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${investedMoneyList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${compoundInterestList[i] + investedMoneyList[i]}',
style: const TextStyle(color: CupertinoColors.white),
textAlign: TextAlign.center,
),
),
],
),
],
),
),
),
),
],
),
);
}

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
// Widget für eine benutzerdefinierten Button mit einem Hintergrundbild
class CustomImageButton extends StatelessWidget {
final VoidCallback onPressed;
final Widget child;
final String backgroundImage;
final VoidCallback onPressed; // Callback-Funktion für den Tastendruck
final Widget child; // Das Widget, das innerhalb der Schaltfläche angezeigt werden soll
final String backgroundImage; // Pfad zum Hintergrundbild
const CustomImageButton({
super.key,

View File

@ -0,0 +1,26 @@
import 'package:flutter/cupertino.dart';
// Klasse für die Erstellung eines Fehler-Widgets mit einer Fehlermeldung
class ErrWidget extends StatelessWidget {
final String errorMessage; // Fehlermeldung, die angezeigt werden soll
const ErrWidget({super.key, required this.errorMessage});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
CupertinoIcons.exclamationmark_circle_fill,
color: CupertinoColors.systemRed,
),
const SizedBox(width: 8),
Text(
errorMessage,
style: const TextStyle(color: CupertinoColors.systemRed),
),
],
);
}
}

View File

@ -1,8 +1,32 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// Erstellt ein Widget für die Eingabe mit einem Label, einem Texteingabefeld, einer Validierungsanzeige und einem Tooltip
Widget buildInputWidget(String label, TextEditingController controller, FocusNode focusNode, bool isValid, String suffixText, String tooltipText) {
// Widget zur Eingabe mit einem Label, einem Texteingabefeld, einer Validierungsanzeige und einem Tooltip
class InputWidget extends StatefulWidget {
final String label; // Beschriftung des Eingabefelds
final TextEditingController controller; // Controller für die Texteingabe
final FocusNode focusNode; // FocusNode für die Eingabe
final bool isValid; // Validierungsstatus
final String suffixText; // Suffix-Text für das Eingabefeld
final String tooltipText; // Erklärungstext im Tooltip
const InputWidget({
super.key,
required this.label,
required this.controller,
required this.focusNode,
required this.isValid,
required this.suffixText,
required this.tooltipText,
});
@override
InputWidgetState createState() => InputWidgetState();
}
class InputWidgetState extends State<InputWidget> {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -11,37 +35,35 @@ Widget buildInputWidget(String label, TextEditingController controller, FocusNod
children: [
Row(
children: [
Row(
children: [
// Label für das Eingabefeld
Text(
label,
widget.label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(width: 5),
// Tooltip-Icon mit Erklärungstext
// Tooltip mit Erklärungstext
Tooltip(
message: tooltipText,
message: widget.tooltipText,
triggerMode: TooltipTriggerMode.tap,
decoration: const BoxDecoration(
color: CupertinoColors.black,
borderRadius: BorderRadius.all(Radius.circular(5))
borderRadius: BorderRadius.all(Radius.circular(5)),
),
textStyle: const TextStyle(
color: CupertinoColors.white,
),
margin: const EdgeInsets.all(20),
child: const Icon(CupertinoIcons.question_circle_fill, size: 15),
child: const Icon(
CupertinoIcons.question_circle_fill,
size: 15,
),
),
],
),
],
),
// Icon zur Anzeige der Validierung (grün für gültig, rot für ungültig)
isValid
// Icon zur Anzeige der Validierung
widget.isValid
? const Icon(
CupertinoIcons.check_mark_circled_solid,
color: CupertinoColors.systemGreen,
@ -55,15 +77,14 @@ Widget buildInputWidget(String label, TextEditingController controller, FocusNod
],
),
const SizedBox(height: 5),
// Texteingabefeld mit Suffix-Text
CupertinoTextField(
controller: controller,
focusNode: focusNode,
controller: widget.controller,
focusNode: widget.focusNode,
keyboardType: TextInputType.number,
suffix: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Text(
suffixText,
widget.suffixText,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
@ -79,3 +100,4 @@ Widget buildInputWidget(String label, TextEditingController controller, FocusNod
],
);
}
}

View File

@ -3,9 +3,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/utils.dart';
// Widget zur Auswahl des Ausschüttungsintervalls
class IntervalWidget extends StatefulWidget {
final String selectedInterval;
final Function(String) onChanged;
final String selectedInterval; // Das aktuell ausgewählte Intervall
final Function(String) onChanged; // Funktion, die aufgerufen wird, wenn das Intervall geändert wird
const IntervalWidget({
super.key,
@ -51,10 +52,11 @@ class IntervalWidgetState extends State<IntervalWidget> {
),
]
),
CupertinoTextField(
placeholder: 'Ausschüttungsintervall auswählen',
readOnly: true,
onTap: () {
Container(
alignment: Alignment.centerLeft,
width: 160,
child: ElevatedButton(
onPressed: () {
// Öffnet das ModalBottomSheet zur Auswahl des Ausschüttungsintervalls
showModalBottomSheet(
context: context,
@ -111,17 +113,33 @@ class IntervalWidgetState extends State<IntervalWidget> {
isExpanded = true; // Setzt den Zustand auf erweitert
});
},
// Suffix-Icon, das den Zustand anzeigt (erweitert oder nicht)
suffix: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5.0),
child: Icon(isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down),
),
controller: TextEditingController(text: widget.selectedInterval), // Setzt den Text des Textfeldes auf das ausgewählte Intervall
decoration: BoxDecoration(
color: CupertinoColors.extraLightBackgroundGray,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(CupertinoColors.black),
foregroundColor: MaterialStateProperty.all<Color>(CupertinoColors.white),
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
cursorColor: CupertinoColors.black,
),
),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
widget.selectedInterval, // Text des ausgewählten Intervalls
textAlign: TextAlign.left,
),
),
),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Icon(isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down),
),
],
),
),
),
const SizedBox(height: 20),
],

View File

@ -1,24 +1,37 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// Erstellt eine Zeitleiste für Meilensteine
Widget buildMilestoneTimeline(List<Map<String, dynamic>> milestones, double totalInterest) {
// Widget einer Meilenstein-Timeline basierend auf den übergebenen Meilensteinen und dem gesamten Zinsertrag
class MilestoneTimeline extends StatelessWidget {
final List<Map<String, dynamic>> milestones; // Liste der Meilensteine
final double totalInterest; // Gesamter Zinsertrag
const MilestoneTimeline({
super.key,
required this.milestones,
required this.totalInterest,
});
@override
Widget build(BuildContext context) {
return SizedBox(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(milestones.length, (index) {
double milestoneValue = milestones[index]['value'];
String milestoneEmoji = milestones[index]['emoji'];
String milestoneText = milestones[index]['text'];
bool milestoneReached = totalInterest >= milestoneValue; // Überprüfen, ob Meilenstein erreicht wurde
double milestoneValue = milestones[index]['value']; // Der Wert des aktuellen Meilensteins
String milestoneEmoji = milestones[index]['emoji']; // Das Emoji des aktuellen Meilensteins
String milestoneText = milestones[index]['text']; // Der Text des aktuellen Meilensteins
bool milestoneReached = totalInterest >= milestoneValue; // Gibt an, ob der Meilenstein erreicht wurde
// Widget für den aktuellen Meilenstein
return Column(
children: [
// Trennlinie zwischen den Meilensteinen, außer für den ersten Meilenstein
if (index > 0)
Container(
height: 25,
width: 2,
color: milestoneReached ? CupertinoColors.systemGreen : CupertinoColors.black, // Linie zwischen Meilensteinen
color: milestoneReached ? CupertinoColors.systemGreen : CupertinoColors.black,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
@ -26,7 +39,7 @@ Widget buildMilestoneTimeline(List<Map<String, dynamic>> milestones, double tota
children: [
Text(
milestoneEmoji,
style: const TextStyle(fontSize: 20), // Emoji für Meilenstein
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 5),
Text(
@ -36,12 +49,13 @@ Widget buildMilestoneTimeline(List<Map<String, dynamic>> milestones, double tota
],
),
),
// Widget für das Symbol (Kreis oder Häkchen) je nachdem, ob der Meilenstein erreicht wurde
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Icon(
milestoneReached ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.circle_fill,
size: 20,
color: milestoneReached ? CupertinoColors.systemGreen : CupertinoColors.black, // Icon, das den Status des Meilensteins anzeigt
color: milestoneReached ? CupertinoColors.systemGreen : CupertinoColors.black,
),
),
],
@ -51,41 +65,4 @@ Widget buildMilestoneTimeline(List<Map<String, dynamic>> milestones, double tota
),
);
}
// Widget, welches den Meilenstein-Zeitstrahl auf eine neue Seite auslagert
@override
Widget buildMilestonePage(BuildContext context, String compoundInterest, String investedMoney, List<Map<String, dynamic>> milestoneList) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: Container(
padding: EdgeInsets.only(left: 10, right: 10, top: MediaQuery.of(context).padding.top + 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: const Icon(CupertinoIcons.chevron_left, size: 15), // Zurück-Pfeil
onPressed: () {
Navigator.pop(context);
},
),
const Text(
'Grafik',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(width: 40), // Platzhalter für zentrierte Ausrichtung
],
),
),
),
SliverToBoxAdapter( // Meilenstein-Zeitstrahl anzeigen
child: Padding(
padding: const EdgeInsets.all(10.0),
child: buildMilestoneTimeline(milestoneList, double.parse(compoundInterest) - double.parse(investedMoney))
),
),
],
),
);
}

View File

@ -1,14 +1,36 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/pages/chart_page.dart';
import 'package:flutter_application_1/pages/milestone_page.dart';
import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/widgets/custom_image_button_widget.dart';
import 'package:flutter_application_1/widgets/chart_widget.dart';
import 'package:flutter_application_1/widgets/milestone_timeline_widget.dart';
// Erstellt ein Widget, das die Berechnungsergebnisse anzeigt
Widget buildResultWidget(BuildContext context, String compoundInterest, String investedMoney, String time, String monthlySavingsRate, String interestRate, PayoutInterval payoutInterval, List<double> investedMoneyList, List<double> compoundInterestList) {
// Liste von Meilensteinen mit Werten, Emojis und Beschreibungen
// Widget zur Anzeige der Ergebnisse der Berechnungen und zur Navigation zu anderen Seiten
class ResultWidget extends StatelessWidget {
final String compoundInterest;
final String investedMoney;
final String time;
final String monthlySavingsRate;
final String interestRate;
final PayoutInterval payoutInterval;
final List<double> investedMoneyList;
final List<double> compoundInterestList;
const ResultWidget({
super.key,
required this.compoundInterest,
required this.investedMoney,
required this.time,
required this.monthlySavingsRate,
required this.interestRate,
required this.payoutInterval,
required this.investedMoneyList,
required this.compoundInterestList,
});
@override
Widget build(BuildContext context) {
List<Map<String, dynamic>> milestoneList = [
{'value': 700.0, 'emoji': '📱', 'text': 'Smartphone\nPreis: 700€'},
{'value': 3250.0, 'emoji': '🚲', 'text': 'eBike\nPreis: 3250€'},
@ -32,7 +54,10 @@ Widget buildResultWidget(BuildContext context, String compoundInterest, String i
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => buildChartPage(context, investedMoneyList, compoundInterestList),
builder: (context) => ChartPage(
investedMoneyList: investedMoneyList,
compoundInterestList: compoundInterestList,
),
),
);
},
@ -48,7 +73,11 @@ Widget buildResultWidget(BuildContext context, String compoundInterest, String i
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => buildMilestonePage(context, compoundInterest, investedMoney, milestoneList),
builder: (context) => MilestonePage(
compoundInterest: compoundInterest,
investedMoney: investedMoney,
milestoneList: milestoneList
),
),
);
},
@ -60,10 +89,9 @@ Widget buildResultWidget(BuildContext context, String compoundInterest, String i
),
],
);
}
// Erstellt eine Zeile mit einem Label und einem Wert
// Widget zum Aufbau einer Ergebniszeile
Widget _buildResultRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
@ -93,3 +121,4 @@ Widget _buildResultRow(String label, String value) {
),
);
}
}

View File

@ -1,15 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/calculator.dart';
void main() {
group('Calculation Tests', () {
test('Test calculateInvestedMoney function', () {
// Testen mit verschiedenen Eingabewerten
test('Calculates correct invested money', () {
expect(calculateInvestedMoney(1000, 100, 5, []), equals(7000.0));
expect(calculateInvestedMoney(2000, 50, 10, []), equals(8000.0));
});
test('Test calculateCompoundInterest function', () {
test('Calculates correct compound interest', () {
// Testen mit jährlicher Auszahlung
List<double> investedMoneyListYearly = [];
List<double> compoundInterestListYearly = [];
@ -22,23 +23,55 @@ void main() {
calculateInvestedMoney(2000, 50, 10, investedMoneyListMonthly);
expect(calculateCompoundInterest(2000, 50, 5, 10, PayoutInterval.monthly, investedMoneyListMonthly, compoundInterestListMonthly), equals(11058.0));
});
});
group('Edge Case Tests', () {
test('Test calculateInvestedMoney function with extremely high input values', () {
// Testen mit extrem hohen Eingabewerten
test('Calculates correct invested money with extremely high input values', () {
expect(calculateInvestedMoney(1e15, 1e12, 100, []), equals(2.2e15));
});
test('Test calculateInvestedMoney function with extremely low input values', () {
// Testen mit extrem niedrigen Eingabewerten
test('Calculates correct invested money with extremely low input values', () {
expect(calculateInvestedMoney(0, 0, 0, []), equals(0.0));
expect(calculateInvestedMoney(0.1, 0.01, 0.001, []), equals(0.0));
});
test('Test calculateCompoundInterest function with extremely high interest rate', () {
// Testen mit extrem hohen Zinssatz
test('Calculates correct compound interest with extremely high interest rate', () {
List<double> investedMoneyListHighInterest = [];
List<double> compoundInterestListHighInterest = [];
calculateInvestedMoney(1000, 100, 10, investedMoneyListHighInterest);
expect(calculateCompoundInterest(1000, 100, 1000, 10, PayoutInterval.yearly, investedMoneyListHighInterest, compoundInterestListHighInterest), equals(29049915553000.0));
});
});
group('Utility Methods Tests', () {
test('roundToInteger should round the value to the nearest integer', () {
final controller = TextEditingController();
controller.text = '2.7';
roundToInteger(controller);
expect(controller.text, '3');
controller.text = '2.3';
roundToInteger(controller);
expect(controller.text, '2');
});
test('isNumeric should check if a string is a valid number', () {
expect(isNumeric('123'), true);
expect(isNumeric('12.3'), true);
expect(isNumeric('12,3'), true);
expect(isNumeric('abc'), false);
expect(isNumeric('12a'), false);
});
test('restoreDefaultValuesIfEmpty should set default value 0 if the field is empty', () {
final controller = TextEditingController();
controller.text = '';
restoreDefaultValuesIfEmpty(controller);
expect(controller.text, '0');
controller.text = '5';
restoreDefaultValuesIfEmpty(controller);
expect(controller.text, '5');
});
test('translateInterval should return correct translation', () {
expect(translateInterval(PayoutInterval.yearly), 'jährlich');
expect(translateInterval(PayoutInterval.monthly), 'monatlich');
});
});
}

View File

@ -1 +1,332 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_application_1/enums.dart';
import 'package:flutter_application_1/pages/chart_page.dart';
import 'package:flutter_application_1/pages/milestone_page.dart';
import 'package:flutter_application_1/utils.dart';
import 'package:flutter_application_1/widgets/chart_widget.dart';
import 'package:flutter_application_1/widgets/error_widget.dart';
import 'package:flutter_application_1/widgets/input_widget.dart';
import 'package:flutter_application_1/widgets/interval_widget.dart';
import 'package:flutter_application_1/widgets/milestone_timeline_widget.dart';
import 'package:flutter_application_1/widgets/result_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
void main() {
group('InputWidget Tests', () {
testWidgets('Displays label, tooltip, and input field', (WidgetTester tester) async {
const label = 'Test Label';
const tooltipText = 'This is a tooltip';
const suffixText = 'suffix';
final controller = TextEditingController();
final focusNode = FocusNode();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Test InputWidget'),
),
child: InputWidget(
label: label,
controller: controller,
focusNode: focusNode,
isValid: false,
suffixText: suffixText,
tooltipText: tooltipText,
),
),
),
);
expect(find.text(label), findsOneWidget);
expect(find.text(suffixText), findsOneWidget);
expect(find.byType(CupertinoTextField), findsOneWidget);
expect(find.byType(Tooltip), findsOneWidget);
});
testWidgets('Displays validation icon correctly', (WidgetTester tester) async {
final controller = TextEditingController();
final focusNode = FocusNode();
await tester.pumpWidget(
CupertinoApp(
home: InputWidget(
label: 'Test Label',
controller: controller,
focusNode: focusNode,
isValid: false,
suffixText: 'suffix',
tooltipText: 'This is a tooltip',
),
),
);
expect(find.byIcon(CupertinoIcons.clear_circled_solid), findsOneWidget);
expect(find.byIcon(CupertinoIcons.check_mark_circled_solid), findsNothing);
await tester.pumpWidget(
CupertinoApp(
home: InputWidget(
label: 'Test Label',
controller: controller,
focusNode: focusNode,
isValid: true,
suffixText: 'suffix',
tooltipText: 'This is a tooltip',
),
),
);
expect(find.byIcon(CupertinoIcons.check_mark_circled_solid), findsOneWidget);
expect(find.byIcon(CupertinoIcons.clear_circled_solid), findsNothing);
});
testWidgets('Text input updates controller', (WidgetTester tester) async {
final controller = TextEditingController();
final focusNode = FocusNode();
await tester.pumpWidget(
CupertinoApp(
home: InputWidget(
label: 'Test Label',
controller: controller,
focusNode: focusNode,
isValid: false,
suffixText: 'suffix',
tooltipText: 'This is a tooltip',
),
),
);
await tester.enterText(find.byType(CupertinoTextField), '12345');
expect(controller.text, '12345');
});
});
group('Interal Widget Tests', () {
testWidgets('Displays label and selected interval', (WidgetTester tester) async {
final selectedInterval = translateInterval(PayoutInterval.yearly);
final Widget widget = CupertinoApp(
home: Scaffold(
body: IntervalWidget(
selectedInterval: selectedInterval,
onChanged: (interval) {},
),
),
);
await tester.pumpWidget(widget);
expect(find.text('Ausschüttungsintervall'), findsOneWidget);
expect(find.text(selectedInterval), findsOneWidget);
});
testWidgets('Expands on button press', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: IntervalWidget(
selectedInterval: translateInterval(PayoutInterval.yearly),
onChanged: (interval) {},
),
),
),
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
expect(find.text(translateInterval(PayoutInterval.yearly)), findsNWidgets(2));
expect(find.text(translateInterval(PayoutInterval.monthly)), findsOneWidget);
});
testWidgets('Selects interval on tap', (WidgetTester tester) async {
String selectedInterval = '';
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: IntervalWidget(
selectedInterval: selectedInterval,
onChanged: (interval) {
selectedInterval = interval;
},
),
),
),
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
await tester.tap(find.text(translateInterval(PayoutInterval.monthly)));
await tester.pump();
expect(find.text('monatlich'), findsOneWidget);
});
});
group('Chart Widget Tests', () {
testWidgets('Displays chart correctly', (WidgetTester tester) async {
final List<double> lowerValues = [100, 200, 300, 400];
final List<double> upperValues = [50, 150, 250, 350];
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: StackedColumnChart(
lowerValues: lowerValues,
upperValues: upperValues,
),
),
),
);
expect(find.byType(SfCartesianChart), findsOneWidget);
expect(find.text('Einzahlungen'), findsOneWidget);
expect(find.text('Zinsen'), findsOneWidget);
expect(find.byType(CategoryAxis), findsOneWidget);
expect(find.byType(NumericAxis), findsOneWidget);
expect(find.text('Einzahlungen'), findsOneWidget);
expect(find.text('Zinsen'), findsOneWidget);
await tester.pumpAndSettle();
});
});
group('Milestone Timeline Widget Tests', () {
testWidgets('Milestone timeline displays correctly', (WidgetTester tester) async {
final List<Map<String, dynamic>> milestones = [
{'value': 100.0, 'emoji': '😊', 'text': 'First milestone'},
{'value': 200.0, 'emoji': '😃', 'text': 'Second milestone'},
{'value': 300.0, 'emoji': '😁', 'text': 'Third milestone'},
];
const double totalInterest = 250.0;
await tester.pumpWidget(
CupertinoApp(
home: Material(
child: MilestoneTimeline(
milestones: milestones,
totalInterest: totalInterest,
),
),
),
);
expect(find.text('First milestone'), findsOneWidget);
expect(find.text('Second milestone'), findsOneWidget);
expect(find.text('Third milestone'), findsOneWidget);
expect(find.byIcon(CupertinoIcons.check_mark_circled_solid), findsNWidgets(2));
expect(find.byIcon(CupertinoIcons.circle_fill), findsOneWidget);
});
});
group('Result Widget Tests', () {
testWidgets('Displays result values correctly', (WidgetTester tester) async {
const compoundInterest = '1000';
const investedMoney = '500';
const time = '5';
const monthlySavingsRate = '100';
const interestRate = '5';
const payoutInterval = PayoutInterval.monthly;
final List<double> investedMoneyList = [100, 200, 300, 400, 500];
final List<double> compoundInterestList = [100, 200, 300, 400, 1000];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ResultWidget(
compoundInterest: compoundInterest,
investedMoney: investedMoney,
time: time,
monthlySavingsRate: monthlySavingsRate,
interestRate: interestRate,
payoutInterval: payoutInterval,
investedMoneyList: investedMoneyList,
compoundInterestList: compoundInterestList,
),
),
),
);
expect(find.text('Endkapital:'), findsOneWidget);
expect(find.text('$compoundInterest'), findsOneWidget);
expect(find.text('Einzahlungen:'), findsOneWidget);
expect(find.text('$investedMoney'), findsOneWidget);
expect(find.text('Erhaltene Zinsen:'), findsOneWidget);
expect(find.text('${double.parse(compoundInterest) + double.parse(investedMoney)}'), findsOneWidget);
});
testWidgets('Navigates to ChartPage on "Grafik" button press', (WidgetTester tester) async {
final List<double> investedMoneyList = [100, 200, 300, 400, 500];
final List<double> compoundInterestList = [100, 200, 300, 400, 1000];
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ResultWidget(
compoundInterest: '1000',
investedMoney: '500',
time: '5',
monthlySavingsRate: '100',
interestRate: '5',
payoutInterval: PayoutInterval.monthly,
investedMoneyList: investedMoneyList,
compoundInterestList: compoundInterestList,
),
),
),
);
await tester.tap(find.text('Grafik'));
await tester.pumpAndSettle();
expect(find.byType(ChartPage), findsOneWidget);
});
testWidgets('Navigates to MilestonePage on "Meilensteine" button press', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: ResultWidget(
compoundInterest: '1000',
investedMoney: '500',
time: '5',
monthlySavingsRate: '100',
interestRate: '5',
payoutInterval: PayoutInterval.monthly,
investedMoneyList: [],
compoundInterestList: [],
),
),
),
);
await tester.tap(find.text('Meilensteine'));
await tester.pumpAndSettle();
expect(find.byType(MilestonePage), findsOneWidget);
});
});
group('Error Widget Tests', () {
testWidgets('Displays error message correctly', (WidgetTester tester) async {
const String errorMessage = 'This is an error message';
await tester.pumpWidget(
const CupertinoApp(
home: Material(
child: ErrWidget(
errorMessage: errorMessage,
),
),
),
);
expect(find.byIcon(CupertinoIcons.exclamationmark_circle_fill), findsOneWidget);
expect(find.text(errorMessage), findsOneWidget);
});
});
}