867 lines
27 KiB
Dart
867 lines
27 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: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 {
|
|
/// Konvertiert den startTime (Unix Timestamp) in ein lokales DateTime Objekt.
|
|
DateTime get startDateTime =>
|
|
DateTime.fromMillisecondsSinceEpoch(
|
|
startTime.toInt() * 1000,
|
|
isUtc: true,
|
|
).toLocal();
|
|
|
|
/// Konvertiert den optionalen endTime (Unix Timestamp) in ein lokales DateTime Objekt.
|
|
DateTime? get endDateTime =>
|
|
endTime == null
|
|
? null
|
|
: DateTime.fromMillisecondsSinceEpoch(
|
|
endTime!.toInt() * 1000,
|
|
isUtc: true,
|
|
).toLocal();
|
|
|
|
/// Formatiert die Dauer (durationSecs) als HH:MM:SS String.
|
|
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 {
|
|
/// Formatiert die Gesamtdauer als HH:MM:SS String.
|
|
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 }
|
|
|
|
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;
|
|
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();
|
|
_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() {
|
|
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);
|
|
}
|
|
}
|
|
|
|
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:
|
|
_isLoadingReport
|
|
? Center(child: PlatformCircularProgressIndicator())
|
|
: _reportData == null
|
|
? const Center(
|
|
child: Text('Keine Reportdaten gefunden oder Fehler.'),
|
|
)
|
|
: _buildReportView(),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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,
|
|
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)))}',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildReportView() {
|
|
if (_reportData == null) {
|
|
return Center(
|
|
child: PlatformText('Keine Reportdaten geladen oder Fehler.'),
|
|
);
|
|
}
|
|
final (startDate, endDate) = _calculateDateRange();
|
|
|
|
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: _buildPieChart(_reportData!)),
|
|
const Divider(),
|
|
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: 300, child: _buildReportList(_reportData!.entries)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildPieChart(ReportData data) {
|
|
Map<String, double> durationPerTag = {};
|
|
for (var entry in data.entries) {
|
|
final tagName = entry.tagName ?? 'Ohne Tag';
|
|
final duration = (entry.durationSecs?.toInt() ?? 0).toDouble();
|
|
durationPerTag[tagName] = (durationPerTag[tagName] ?? 0) + duration;
|
|
}
|
|
|
|
if (durationPerTag.isEmpty) {
|
|
return const Center(child: Text("Keine Daten für Chart"));
|
|
}
|
|
|
|
List<Color> colors = Colors.primaries.take(durationPerTag.length).toList();
|
|
if (durationPerTag.length > Colors.primaries.length) {
|
|
colors.addAll(
|
|
Colors.accents.take(durationPerTag.length - Colors.primaries.length),
|
|
);
|
|
}
|
|
|
|
int colorIndex = 0;
|
|
List<PieChartSectionData> sections =
|
|
durationPerTag.entries.map((entry) {
|
|
final isTouched = false;
|
|
final fontSize = isTouched ? 18.0 : 14.0;
|
|
final radius = isTouched ? 60.0 : 50.0;
|
|
final color = colors[colorIndex % colors.length];
|
|
colorIndex++;
|
|
|
|
final hours = (entry.value / 3600).toStringAsFixed(1);
|
|
|
|
return PieChartSectionData(
|
|
color: color,
|
|
value: entry.value,
|
|
title: '${entry.key}\n${hours}h',
|
|
radius: radius,
|
|
titleStyle: TextStyle(
|
|
fontSize: fontSize,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
shadows: [
|
|
Shadow(color: Colors.black.withOpacity(0.7), blurRadius: 2),
|
|
],
|
|
),
|
|
titlePositionPercentageOffset: 0.6,
|
|
);
|
|
}).toList();
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: PieChart(
|
|
PieChartData(
|
|
sections: sections,
|
|
centerSpaceRadius: 40,
|
|
sectionsSpace: 2,
|
|
),
|
|
duration: const Duration(milliseconds: 150),
|
|
curve: Curves.linear,
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget _buildDataTable(List<TimeEntry> entries) {
|
|
// final DateFormat timeFormatter = DateFormat('HH:mm:ss');
|
|
// final DateFormat dateFormatter = DateFormat('dd.MM.yy');
|
|
|
|
// return DataTable(
|
|
// columnSpacing: 10,
|
|
// columns: [
|
|
// DataColumn(label: PlatformText('Tag')),
|
|
// DataColumn(label: PlatformText('Start')),
|
|
// DataColumn(label: PlatformText('Ende')),
|
|
// DataColumn(label: PlatformText('Dauer'), numeric: true),
|
|
// ],
|
|
// rows:
|
|
// entries.map((entry) {
|
|
// return DataRow(
|
|
// cells: [
|
|
// DataCell(PlatformText(entry.tagName ?? '-')),
|
|
// DataCell(
|
|
// PlatformText(
|
|
// '${dateFormatter.format(unixSecondsToDateTime(entry.startTime))}\n${timeFormatter.format(unixSecondsToDateTime(entry.startTime))}',
|
|
// ),
|
|
// ),
|
|
// DataCell(
|
|
// entry.endTime != null
|
|
// ? PlatformText(
|
|
// '${dateFormatter.format(unixSecondsToDateTime(entry.endTime!))}\n${timeFormatter.format(unixSecondsToDateTime(entry.endTime!))}',
|
|
// )
|
|
// : PlatformText('-'),
|
|
// ),
|
|
// DataCell(Text(entry.durationFormatted)),
|
|
// ],
|
|
// );
|
|
// }).toList(),
|
|
// );
|
|
// }
|
|
|
|
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,
|
|
), // Pop using dialog's context
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|