979 lines
31 KiB
Dart
979 lines
31 KiB
Dart
import 'dart:developer';
|
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
|
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:timetracker/src/rust/api.dart';
|
|
import 'package:timetracker/time_tracking_service.dart';
|
|
|
|
int dateTimeToUnixSeconds(DateTime dt) =>
|
|
dt.toUtc().millisecondsSinceEpoch ~/ 1000;
|
|
DateTime unixSecondsToDateTime(int ts) =>
|
|
DateTime.fromMillisecondsSinceEpoch(ts * 1000, isUtc: true).toLocal();
|
|
|
|
extension TimeEntryFormatting on TimeEntry {
|
|
DateTime get startDateTime =>
|
|
DateTime.fromMillisecondsSinceEpoch(
|
|
startTime.toInt() * 1000,
|
|
isUtc: true,
|
|
).toLocal();
|
|
|
|
DateTime? get endDateTime =>
|
|
endTime == null
|
|
? null
|
|
: DateTime.fromMillisecondsSinceEpoch(
|
|
endTime!.toInt() * 1000,
|
|
isUtc: true,
|
|
).toLocal();
|
|
|
|
String get durationFormatted {
|
|
final durationInSeconds = durationSecs?.toInt();
|
|
if (durationInSeconds == null || durationInSeconds < 0) return '--:--:--';
|
|
final duration = Duration(seconds: durationInSeconds);
|
|
|
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
|
final hours = twoDigits(duration.inHours);
|
|
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
|
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
|
return "$hours:$minutes:$seconds";
|
|
}
|
|
}
|
|
|
|
extension ReportDataFormatting on ReportData {
|
|
String get totalDurationFormatted {
|
|
final durationInSeconds = totalDurationSecs.toInt();
|
|
if (durationInSeconds < 0) return '--:--:--';
|
|
final duration = Duration(seconds: durationInSeconds);
|
|
|
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
|
final hours = twoDigits(duration.inHours);
|
|
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
|
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
|
return "$hours:$minutes:$seconds";
|
|
}
|
|
}
|
|
|
|
enum ReportPeriod { day, week, month, year, custom }
|
|
|
|
class ReportScreen extends StatefulWidget {
|
|
const ReportScreen({super.key});
|
|
|
|
@override
|
|
State<ReportScreen> createState() => _ReportScreenState();
|
|
}
|
|
|
|
class _ReportScreenState extends State<ReportScreen> {
|
|
final DateTime _selectedDate = DateTime.now();
|
|
ReportPeriod _selectedPeriod = ReportPeriod.day;
|
|
DateTime _customStartDate = DateTime.now();
|
|
DateTime _customEndDate = DateTime.now();
|
|
Tag? _selectedTag;
|
|
ReportData? _reportData;
|
|
bool _isLoadingReport = false;
|
|
TimeEntry? _editingEntry;
|
|
|
|
String _formatDuration(Duration duration) {
|
|
String twoDigits(int n) => n.toString().padLeft(2, "0");
|
|
if (duration.isNegative) return "--:--:--";
|
|
final hours = twoDigits(duration.inHours);
|
|
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
|
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
|
return "$hours:$minutes:$seconds";
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final now = DateTime.now();
|
|
_customStartDate = DateTime(now.year, now.month, now.day);
|
|
_customEndDate = DateTime(now.year, now.month, now.day);
|
|
_generateReport();
|
|
}
|
|
|
|
Map<DateTime, Map<String, Duration>> _aggregateDataForBarChart(
|
|
List<TimeEntry> entries,
|
|
) {
|
|
final Map<DateTime, Map<String, Duration>> dailyTagDurations = {};
|
|
|
|
for (final entry in entries) {
|
|
if (entry.durationSecs == null || entry.durationSecs!.toInt() <= 0) {
|
|
continue;
|
|
}
|
|
|
|
final entryDay = DateTime(
|
|
entry.startDateTime.year,
|
|
entry.startDateTime.month,
|
|
entry.startDateTime.day,
|
|
);
|
|
|
|
final tagName = entry.tagName ?? 'Ohne Tag';
|
|
final duration = Duration(seconds: entry.durationSecs!.toInt());
|
|
|
|
final dailyMap = dailyTagDurations.putIfAbsent(entryDay, () => {});
|
|
final currentDuration = dailyMap.putIfAbsent(
|
|
tagName,
|
|
() => Duration.zero,
|
|
);
|
|
dailyMap[tagName] = currentDuration + duration;
|
|
}
|
|
|
|
return dailyTagDurations;
|
|
}
|
|
|
|
Widget _buildBarChart(
|
|
ReportData reportData,
|
|
DateTime startDate,
|
|
DateTime endDate,
|
|
) {
|
|
final aggregatedData = _aggregateDataForBarChart(reportData.entries);
|
|
|
|
if (aggregatedData.isEmpty) {
|
|
return Center(
|
|
child: PlatformText("Keine Daten für Balkendiagramm vorhanden."),
|
|
);
|
|
}
|
|
|
|
final uniqueTags = <String>{};
|
|
for (var dailyMap in aggregatedData.values) {
|
|
uniqueTags.addAll(dailyMap.keys);
|
|
}
|
|
final sortedTags = uniqueTags.toList()..sort();
|
|
final List<Color> predefinedColors = [
|
|
Colors.blue,
|
|
Colors.red,
|
|
Colors.green,
|
|
Colors.orange,
|
|
Colors.purple,
|
|
Colors.teal,
|
|
Colors.pink,
|
|
Colors.amber,
|
|
Colors.indigo,
|
|
Colors.cyan,
|
|
];
|
|
final tagColors = <String, Color>{};
|
|
for (int i = 0; i < sortedTags.length; i++) {
|
|
tagColors[sortedTags[i]] = predefinedColors[i % predefinedColors.length];
|
|
}
|
|
|
|
final sortedDays = aggregatedData.keys.toList()..sort();
|
|
double maxY = 0;
|
|
for (var day in sortedDays) {
|
|
double dailyTotalHours = 0;
|
|
for (var duration in aggregatedData[day]!.values) {
|
|
dailyTotalHours += duration.inMinutes / 60.0;
|
|
}
|
|
if (dailyTotalHours > maxY) {
|
|
maxY = dailyTotalHours;
|
|
}
|
|
}
|
|
maxY = (maxY * 1.1).ceilToDouble();
|
|
if (maxY < 1) maxY = 1;
|
|
|
|
final List<BarChartGroupData> barGroups = [];
|
|
final double barWidth = 12;
|
|
final double spaceBetweenBars = 2;
|
|
final double groupWidth = (barWidth + spaceBetweenBars) * sortedTags.length;
|
|
|
|
for (int dayIndex = 0; dayIndex < sortedDays.length; dayIndex++) {
|
|
final day = sortedDays[dayIndex];
|
|
final dailyMap = aggregatedData[day]!;
|
|
final List<BarChartRodData> barRods = [];
|
|
|
|
for (int tagIndex = 0; tagIndex < sortedTags.length; tagIndex++) {
|
|
final tagName = sortedTags[tagIndex];
|
|
final duration = dailyMap[tagName] ?? Duration.zero;
|
|
final hours = duration.inMinutes / 60.0;
|
|
|
|
barRods.add(
|
|
BarChartRodData(
|
|
toY: hours,
|
|
color: tagColors[tagName],
|
|
width: barWidth,
|
|
borderRadius: BorderRadius.zero,
|
|
),
|
|
);
|
|
}
|
|
|
|
barGroups.add(
|
|
BarChartGroupData(
|
|
x: dayIndex,
|
|
barsSpace: spaceBetweenBars,
|
|
barRods: barRods,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget bottomTitleWidgets(double value, TitleMeta meta) {
|
|
final index = value.toInt();
|
|
final showEvery = (sortedDays.length / 7).ceil();
|
|
if (index < 0 || index >= sortedDays.length || index % showEvery != 0) {
|
|
return Container();
|
|
}
|
|
final day = sortedDays[index];
|
|
String text;
|
|
if (_selectedPeriod == ReportPeriod.day ||
|
|
_selectedPeriod == ReportPeriod.week) {
|
|
text = DateFormat('E', 'de_DE').format(day);
|
|
} else {
|
|
text = DateFormat('dd.MM').format(day);
|
|
}
|
|
|
|
return SideTitleWidget(
|
|
space: 4,
|
|
meta: TitleMeta(
|
|
min: 0.0,
|
|
max: 10.0,
|
|
parentAxisSize: 10,
|
|
axisPosition: 0.0,
|
|
appliedInterval: 10,
|
|
sideTitles: SideTitles(),
|
|
formattedValue: '',
|
|
axisSide: AxisSide.left,
|
|
rotationQuarterTurns: 0,
|
|
),
|
|
child: Text(text, style: const TextStyle(fontSize: 10)),
|
|
);
|
|
}
|
|
|
|
Widget leftTitleWidgets(double value, TitleMeta meta) {
|
|
if (value == 0 || value == meta.max) {
|
|
return Container();
|
|
}
|
|
if (value % 1 == 0) {
|
|
return SideTitleWidget(
|
|
space: 4,
|
|
meta: TitleMeta(
|
|
min: 0.0,
|
|
max: 10.0,
|
|
parentAxisSize: 10,
|
|
axisPosition: 0.0,
|
|
appliedInterval: 10,
|
|
sideTitles: SideTitles(),
|
|
formattedValue: '',
|
|
axisSide: AxisSide.left,
|
|
rotationQuarterTurns: 0,
|
|
),
|
|
child: Text(
|
|
'${value.toInt()}h',
|
|
style: const TextStyle(fontSize: 10),
|
|
),
|
|
);
|
|
}
|
|
return Container();
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: BarChart(
|
|
BarChartData(
|
|
maxY: maxY,
|
|
barGroups: barGroups,
|
|
groupsSpace: groupWidth + 10,
|
|
titlesData: FlTitlesData(
|
|
show: true,
|
|
rightTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
topTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: bottomTitleWidgets,
|
|
reservedSize: 20,
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 30,
|
|
getTitlesWidget: leftTitleWidgets,
|
|
),
|
|
),
|
|
),
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
getDrawingHorizontalLine: (value) {
|
|
return FlLine(
|
|
color: Colors.grey.withOpacity(0.3),
|
|
strokeWidth: 1,
|
|
);
|
|
},
|
|
),
|
|
borderData: FlBorderData(show: false),
|
|
barTouchData: BarTouchData(
|
|
touchTooltipData: BarTouchTooltipData(
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
final tagName = sortedTags[rodIndex];
|
|
final duration = Duration(minutes: (rod.toY * 60).round());
|
|
return BarTooltipItem(
|
|
'$tagName\n',
|
|
const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
children: <TextSpan>[
|
|
TextSpan(
|
|
text: _formatDuration(duration),
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.normal,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
(DateTime, DateTime) _calculateDateRange(ReportPeriod period) {
|
|
final nowLocal = DateTime.now().toLocal();
|
|
final startOfDay = DateTime(nowLocal.year, nowLocal.month, nowLocal.day);
|
|
|
|
switch (_selectedPeriod) {
|
|
case ReportPeriod.day:
|
|
final endOfDay = startOfDay.add(const Duration(days: 1));
|
|
return (startOfDay, endOfDay);
|
|
case ReportPeriod.week:
|
|
final startOfWeek = startOfDay.subtract(
|
|
Duration(days: startOfDay.weekday - 1),
|
|
);
|
|
final endOfWeek = startOfWeek.add(const Duration(days: 7));
|
|
return (startOfWeek, endOfWeek);
|
|
case ReportPeriod.month:
|
|
final startOfMonth = DateTime(nowLocal.year, nowLocal.month, 1);
|
|
final endOfMonth = DateTime(nowLocal.year, nowLocal.month + 1, 1);
|
|
return (startOfMonth, endOfMonth);
|
|
case ReportPeriod.year:
|
|
final startOfYear = DateTime(nowLocal.year, 1, 1);
|
|
final endOfYear = DateTime(nowLocal.year + 1, 1, 1);
|
|
return (startOfYear, endOfYear);
|
|
case ReportPeriod.custom:
|
|
final endExclusive = DateTime(
|
|
_customEndDate.year,
|
|
_customEndDate.month,
|
|
_customEndDate.day,
|
|
).add(const Duration(days: 1));
|
|
return (_customStartDate, endExclusive);
|
|
}
|
|
}
|
|
|
|
Future<void> _generateReport() async {
|
|
if (!mounted) return;
|
|
setState(() {
|
|
_isLoadingReport = true;
|
|
_reportData = null;
|
|
});
|
|
|
|
final timeService = context.read<TimeTrackingService>();
|
|
final tagId = _selectedTag?.id;
|
|
|
|
late DateTime reportStartDate;
|
|
late DateTime reportEndDateExclusive;
|
|
|
|
if (_selectedPeriod == ReportPeriod.custom) {
|
|
reportStartDate = _customStartDate;
|
|
reportEndDateExclusive = DateTime(
|
|
_customEndDate.year,
|
|
_customEndDate.month,
|
|
_customEndDate.day,
|
|
).add(const Duration(days: 1));
|
|
} else {
|
|
final (start, end) = _calculateDateRange(_selectedPeriod);
|
|
reportStartDate = start;
|
|
reportEndDateExclusive = end;
|
|
}
|
|
|
|
log(
|
|
"Generating report for Period: $_selectedPeriod, TagID: $tagId, Start: $reportStartDate, End (Exclusive): $reportEndDateExclusive",
|
|
);
|
|
|
|
final data = await timeService.flGetReport(
|
|
tagId,
|
|
reportStartDate,
|
|
reportEndDateExclusive,
|
|
);
|
|
|
|
if (mounted) {
|
|
setState(() {
|
|
_reportData = data;
|
|
_isLoadingReport = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Future<void> _generateReport() async {
|
|
// if (!mounted) return;
|
|
// setState(() {
|
|
// _isLoadingReport = true;
|
|
// _reportData = null;
|
|
// });
|
|
|
|
// final timeService = context.read<TimeTrackingService>();
|
|
// final (start, end) = _calculateDateRange();
|
|
// final tagId = _selectedTag?.id;
|
|
|
|
// log("Generating report for TagID: $tagId, Start: $start, End: $end");
|
|
|
|
// final data = await timeService.flGetReport(tagId, start, end);
|
|
|
|
// if (mounted) {
|
|
// setState(() {
|
|
// _reportData = data;
|
|
// _isLoadingReport = false;
|
|
// });
|
|
// }
|
|
// }
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final tags = context.watch<TimeTrackingService>().tags;
|
|
final List<Tag?> tagOptions = [null, ...tags];
|
|
|
|
return PlatformScaffold(
|
|
appBar: PlatformAppBar(title: PlatformText('Reports')),
|
|
body: Column(
|
|
children: [
|
|
_buildFilterControls(tagOptions),
|
|
Expanded(
|
|
child: RefreshIndicator.adaptive(
|
|
onRefresh: _generateReport,
|
|
child:
|
|
_isLoadingReport
|
|
? Center(child: PlatformCircularProgressIndicator())
|
|
: _reportData == null
|
|
? ListView(
|
|
children: [
|
|
SizedBox(
|
|
height: MediaQuery.of(context).size.height * 0.5,
|
|
child: Text(
|
|
'Keine Reportdaten gefunden oder Fehler.',
|
|
),
|
|
),
|
|
],
|
|
)
|
|
: _buildReportView(_reportData!),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFilterControls(List<Tag?> tagOptions) {
|
|
final DateFormat formatter = DateFormat('dd.MM.yyyy');
|
|
final String dateRangeButtonText =
|
|
'${formatter.format(_customStartDate)} - ${formatter.format(_customEndDate)}';
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Wrap(
|
|
spacing: 8.0,
|
|
runSpacing: 4.0,
|
|
alignment: WrapAlignment.start,
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
children: [
|
|
PlatformWidget(
|
|
material:
|
|
(_, __) => DropdownButton<ReportPeriod>(
|
|
value: _selectedPeriod,
|
|
items: [
|
|
...ReportPeriod.values.map((period) {
|
|
String text;
|
|
switch (period) {
|
|
case ReportPeriod.day:
|
|
text = 'Tag';
|
|
break;
|
|
case ReportPeriod.week:
|
|
text = 'Woche';
|
|
break;
|
|
case ReportPeriod.month:
|
|
text = 'Monat';
|
|
break;
|
|
case ReportPeriod.year:
|
|
text = 'Jahr';
|
|
break;
|
|
case ReportPeriod.custom:
|
|
text = 'Benutzerdefiniert';
|
|
break;
|
|
}
|
|
return DropdownMenuItem<ReportPeriod>(
|
|
value: period,
|
|
child: PlatformText(text),
|
|
);
|
|
}),
|
|
],
|
|
onChanged: (ReportPeriod? newValue) {
|
|
if (newValue != null && newValue != _selectedPeriod) {
|
|
setState(() {
|
|
_selectedPeriod = newValue;
|
|
if (_selectedPeriod != ReportPeriod.custom) {
|
|
_generateReport();
|
|
} else {
|
|
// Optional: Direkt den Picker öffnen, wenn "Custom" gewählt wird?
|
|
// WidgetsBinding.instance.addPostFrameCallback((_) => _selectDateRange());
|
|
}
|
|
});
|
|
}
|
|
},
|
|
),
|
|
// TODO: Cupertino Dropdown Alternative (komplexer, oft wird Button+Picker genutzt)
|
|
// Fürs Erste verwenden wir auch auf iOS das Material Dropdown
|
|
cupertino:
|
|
(_, __) => CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
child: Text(_selectedPeriod.toString().split('.').last),
|
|
onPressed: () {
|
|
/* Hier müsste ein Picker geöffnet werden */
|
|
},
|
|
),
|
|
),
|
|
|
|
DropdownButton<Tag?>(
|
|
value: _selectedTag,
|
|
hint: PlatformText("Alle Tags"),
|
|
onChanged: (Tag? newValue) {
|
|
setState(() {
|
|
_selectedTag = newValue;
|
|
});
|
|
_generateReport();
|
|
},
|
|
items:
|
|
tagOptions.map((Tag? tag) {
|
|
return DropdownMenuItem<Tag?>(
|
|
value: tag,
|
|
child: PlatformText(tag?.name ?? "Alle Tags"),
|
|
);
|
|
}).toList(),
|
|
),
|
|
|
|
if (_selectedPeriod == ReportPeriod.custom)
|
|
PlatformElevatedButton(
|
|
onPressed: _selectDateRange,
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(FontAwesomeIcons.calendar), // PlatformIcons(context)),
|
|
const SizedBox(width: 8),
|
|
PlatformText(dateRangeButtonText),
|
|
],
|
|
),
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget _buildFilterControls(List<Tag?> tagOptions) {
|
|
// final DateFormat formatter = DateFormat('dd.MM.yyyy');
|
|
// final (start, end) = _calculateDateRange();
|
|
|
|
// return Padding(
|
|
// padding: const EdgeInsets.all(8.0),
|
|
// child: Wrap(
|
|
// spacing: 8.0,
|
|
// runSpacing: 4.0,
|
|
// alignment: WrapAlignment.center,
|
|
// children: [
|
|
// DropdownButton<ReportPeriod>(
|
|
// value: _selectedPeriod,
|
|
// onChanged: (ReportPeriod? newValue) {
|
|
// if (newValue != null) {
|
|
// setState(() {
|
|
// _selectedPeriod = newValue;
|
|
// });
|
|
// _generateReport();
|
|
// }
|
|
// },
|
|
// items:
|
|
// ReportPeriod.values.map((ReportPeriod period) {
|
|
// return DropdownMenuItem<ReportPeriod>(
|
|
// value: period,
|
|
// child: PlatformText(
|
|
// period.toString().split('.').last.toUpperCase(),
|
|
// ),
|
|
// );
|
|
// }).toList(),
|
|
// ),
|
|
|
|
// DropdownButton<Tag?>(
|
|
// value: _selectedTag,
|
|
// hint: PlatformText("Alle Tags"),
|
|
// onChanged: (Tag? newValue) {
|
|
// setState(() {
|
|
// _selectedTag = newValue;
|
|
// });
|
|
// _generateReport();
|
|
// },
|
|
// items:
|
|
// tagOptions.map((Tag? tag) {
|
|
// return DropdownMenuItem<Tag?>(
|
|
// value: tag,
|
|
// child: PlatformText(tag?.name ?? "Alle Tags"),
|
|
// );
|
|
// }).toList(),
|
|
// ),
|
|
|
|
// Chip(
|
|
// label: PlatformText(
|
|
// '${formatter.format(start)} - ${formatter.format(end.subtract(const Duration(seconds: 1)))}',
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// );
|
|
// }
|
|
|
|
Future<void> _selectDateRange() async {
|
|
final DateTimeRange initialRange = DateTimeRange(
|
|
start: _customStartDate,
|
|
end: _customEndDate,
|
|
);
|
|
|
|
final DateTimeRange? picked = await showDateRangePicker(
|
|
context: context,
|
|
locale: const Locale('de', 'DE'),
|
|
initialDateRange: initialRange,
|
|
firstDate: DateTime(2020),
|
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
|
helpText: 'Zeitraum auswählen',
|
|
cancelText: 'Abbrechen',
|
|
confirmText: 'Ok',
|
|
saveText: 'Speichern',
|
|
);
|
|
|
|
if (picked != null) {
|
|
if (picked.start != _customStartDate ||
|
|
picked.end != _customEndDate ||
|
|
_selectedPeriod != ReportPeriod.custom) {
|
|
setState(() {
|
|
_customStartDate = DateTime(
|
|
picked.start.year,
|
|
picked.start.month,
|
|
picked.start.day,
|
|
);
|
|
_customEndDate = DateTime(
|
|
picked.end.year,
|
|
picked.end.month,
|
|
picked.end.day,
|
|
);
|
|
_selectedPeriod = ReportPeriod.custom;
|
|
});
|
|
_generateReport();
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _buildReportView(ReportData reportData) {
|
|
if (_reportData == null) {
|
|
return Center(
|
|
child: PlatformText('Keine Reportdaten geladen oder Fehler.'),
|
|
);
|
|
}
|
|
final (startDate, endDate) = _calculateDateRange(
|
|
_selectedPeriod == ReportPeriod.custom
|
|
? ReportPeriod.custom
|
|
: _selectedPeriod,
|
|
);
|
|
|
|
return ListView(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: PlatformText(
|
|
'Gesamtdauer: ${_reportData!.totalDurationFormatted}',
|
|
style: platformThemeData(
|
|
context,
|
|
material: (data) => data.textTheme.titleLarge,
|
|
cupertino: (data) => data.textTheme.navTitleTextStyle,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(
|
|
height: 250,
|
|
child: _buildBarChart(_reportData!, startDate, endDate),
|
|
),
|
|
const Divider(),
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
|
child: PlatformText(
|
|
"Einträge (zum Löschen wischen)",
|
|
style: platformThemeData(
|
|
context,
|
|
material: (data) => data.textTheme.titleMedium,
|
|
cupertino:
|
|
(data) =>
|
|
data.textTheme.navTitleTextStyle.copyWith(fontSize: 18),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(height: 600, child: _buildReportList(_reportData!.entries)),
|
|
],
|
|
);
|
|
}
|
|
|
|
void _confirmAndDeleteEntry(TimeEntry entry, BuildContext context) {
|
|
showPlatformDialog(
|
|
context: context,
|
|
builder:
|
|
(_) => PlatformAlertDialog(
|
|
title: Text('Eintrag löschen?'),
|
|
actions: [
|
|
PlatformDialogAction(
|
|
child: PlatformText('Abbrechen'),
|
|
onPressed: () => Navigator.pop(context),
|
|
),
|
|
PlatformDialogAction(
|
|
child: PlatformText('Löschen'),
|
|
onPressed: () async {
|
|
Navigator.pop(context);
|
|
await _deleteEntry(entry);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _deleteEntry(TimeEntry entry) async {
|
|
final service = Provider.of<TimeTrackingService>(context, listen: false);
|
|
try {
|
|
await service.flDeleteTimeEntry(entry.id);
|
|
if (mounted) {
|
|
setState(() => _reportData!.entries.remove(entry));
|
|
}
|
|
await _generateReport();
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(
|
|
context,
|
|
).showSnackBar(SnackBar(content: Text('Fehler beim Löschen: $e')));
|
|
}
|
|
}
|
|
|
|
Widget _buildReportList(List<TimeEntry> entries) {
|
|
return ListView.builder(
|
|
// shrinkWrap: true,
|
|
// physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: entries.length,
|
|
itemBuilder: (context, index) {
|
|
final entry = entries[index];
|
|
return Builder(
|
|
builder:
|
|
(slidableContext) => Slidable(
|
|
endActionPane: ActionPane(
|
|
motion: const ScrollMotion(),
|
|
children: [
|
|
SlidableAction(
|
|
onPressed:
|
|
(_) => _confirmAndDeleteEntry(entry, slidableContext),
|
|
backgroundColor: Colors.red,
|
|
icon: Icons.delete,
|
|
label: 'Löschen',
|
|
),
|
|
SlidableAction(
|
|
onPressed:
|
|
(context) => _showEditEntryDialog(context, entry),
|
|
backgroundColor: Colors.blue,
|
|
icon: Icons.edit,
|
|
label: 'Bearbeiten',
|
|
),
|
|
],
|
|
),
|
|
child: ListTile(
|
|
title: Text(entry.tagName ?? 'Ohne Tag'),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Start: ${DateFormat('HH:mm').format(entry.startDateTime)}',
|
|
),
|
|
if (entry.endDateTime != null)
|
|
Text(
|
|
'Ende: ${DateFormat('HH:mm').format(entry.endDateTime!)}',
|
|
),
|
|
Text('Dauer: ${entry.durationFormatted}'),
|
|
],
|
|
),
|
|
trailing: const Icon(Icons.swipe_left),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _showEditEntryDialog(
|
|
BuildContext context,
|
|
TimeEntry entry,
|
|
) async {
|
|
DateTime? startTime = entry.startDateTime;
|
|
DateTime? endTime = entry.endDateTime;
|
|
|
|
await showPlatformDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return PlatformAlertDialog(
|
|
title: Text('Eintrag bearbeiten'),
|
|
content: StatefulBuilder(
|
|
builder: (BuildContext context, StateSetter setState) {
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: <Widget>[
|
|
PlatformText('Startzeit:'),
|
|
PlatformElevatedButton(
|
|
child: Text(
|
|
DateFormat('yyyy-MM-dd HH:mm').format(startTime!),
|
|
),
|
|
onPressed: () async {
|
|
DateTime? pickedDate = await showPlatformDatePicker(
|
|
context: context,
|
|
initialDate: startTime!,
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2101),
|
|
);
|
|
if (pickedDate != null) {
|
|
TimeOfDay? pickedTime = await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay.fromDateTime(startTime!),
|
|
);
|
|
if (pickedTime != null) {
|
|
setState(() {
|
|
startTime = DateTime(
|
|
pickedDate.year,
|
|
pickedDate.month,
|
|
pickedDate.day,
|
|
pickedTime.hour,
|
|
pickedTime.minute,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
),
|
|
PlatformText('Endzeit:'),
|
|
PlatformElevatedButton(
|
|
child: Text(
|
|
endTime != null
|
|
? DateFormat('yyyy-MM-dd HH:mm').format(endTime!)
|
|
: 'Keine Endzeit',
|
|
),
|
|
onPressed: () async {
|
|
DateTime? pickedDate = await showPlatformDatePicker(
|
|
context: context,
|
|
initialDate: endTime ?? DateTime.now(),
|
|
firstDate: DateTime(2000),
|
|
lastDate: DateTime(2101),
|
|
);
|
|
if (pickedDate != null) {
|
|
TimeOfDay? pickedTime = await showTimePicker(
|
|
context: context,
|
|
initialTime: TimeOfDay.fromDateTime(
|
|
endTime ?? DateTime.now(),
|
|
),
|
|
);
|
|
if (pickedTime != null) {
|
|
setState(() {
|
|
endTime = DateTime(
|
|
pickedDate.year,
|
|
pickedDate.month,
|
|
pickedDate.day,
|
|
pickedTime.hour,
|
|
pickedTime.minute,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
actions: [
|
|
PlatformDialogAction(
|
|
child: PlatformText('Abbrechen'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
PlatformDialogAction(
|
|
child: PlatformText('Speichern'),
|
|
onPressed: () async {
|
|
// Hier die Logik zum Speichern der bearbeiteten Daten
|
|
Navigator.of(context).pop();
|
|
_updateTimeEntry(entry, startTime, endTime);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _updateTimeEntry(
|
|
TimeEntry entry,
|
|
DateTime? startTime,
|
|
DateTime? endTime,
|
|
) async {
|
|
final timeService = Provider.of<TimeTrackingService>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
|
|
if (startTime == null || endTime == null) {
|
|
log("Start- oder Endzeit darf nicht null sein.");
|
|
return;
|
|
}
|
|
|
|
bool success = await timeService.flUpdateTimeEntry(
|
|
entryId: entry.id,
|
|
tagId: entry.tagId,
|
|
startTime: startTime,
|
|
endTime: endTime,
|
|
);
|
|
|
|
if (success) {
|
|
_generateReport();
|
|
} else {
|
|
_showPlatformFeedbackDialog(
|
|
context,
|
|
'Error',
|
|
'Fehler beim Aktualisieren des TimeEntry.',
|
|
);
|
|
}
|
|
}
|
|
|
|
void _showPlatformFeedbackDialog(
|
|
BuildContext context,
|
|
String title,
|
|
String message,
|
|
) {
|
|
showPlatformDialog(
|
|
context: context,
|
|
useRootNavigator: true,
|
|
builder:
|
|
(dialogContext) => PlatformAlertDialog(
|
|
title: PlatformText(title),
|
|
content: PlatformText(message),
|
|
actions: <Widget>[
|
|
PlatformDialogAction(
|
|
child: PlatformText('OK'),
|
|
onPressed: () => Navigator.pop(dialogContext),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|