timetracker/lib/features/settings/data/export_service.dart
2026-08-03 21:51:48 +02:00

173 lines
5.5 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'dart:io';
import 'package:csv/csv.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:share_plus/share_plus.dart';
import 'package:timetrack/features/entries/data/drift_entries_repository.dart';
import 'package:timetrack/features/entries/data/entries_repository.dart';
import 'package:timetrack/features/entries/domain/time_entry.dart';
import 'package:timetrack/features/projects/data/drift_projects_repository.dart';
import 'package:timetrack/features/projects/data/projects_repository.dart';
import 'package:timetrack/features/projects/domain/project.dart';
part 'export_service.g.dart';
enum ExportFormat { csv, pdf, json }
@riverpod
ExportService exportService(ExportServiceRef ref) {
return ExportService(
entriesRepo: ref.watch(timeEntriesRepositoryProvider),
projectsRepo: ref.watch(projectsRepositoryProvider),
);
}
class ExportService {
ExportService({
required this.entriesRepo,
required this.projectsRepo,
});
final TimeEntriesRepository entriesRepo;
final ProjectsRepository projectsRepo;
Future<void> export({
required ExportFormat format,
required DateTime from,
required DateTime to,
int? projectId,
}) async {
final entries = await entriesRepo.watchByDateRange(from, to).first;
final allProjects = await projectsRepo.watchAll().first;
final projectMap = {for (final p in allProjects) p.id: p};
final filtered = projectId != null
? entries.where((e) => e.projectId == projectId).toList()
: entries;
switch (format) {
case ExportFormat.csv:
await _exportCsv(filtered, projectMap);
break;
case ExportFormat.pdf:
await _exportPdf(filtered, projectMap, from, to);
break;
case ExportFormat.json:
await _exportJson(filtered, allProjects);
break;
}
}
Future<void> _exportCsv(
List<TimeEntry> entries,
Map<int, Project> projectMap,
) async {
final rows = <List<dynamic>>[
['id', 'project', 'start_time', 'end_time', 'duration_seconds', 'note', 'tags'],
...entries.map((e) => [
e.id,
projectMap[e.projectId]?.name ?? '',
e.startTime.toIso8601String(),
e.endTime?.toIso8601String() ?? '',
e.durationSeconds ?? '',
e.note ?? '',
e.tags.join('|'),
]),
];
final csvStr = const ListToCsvConverter().convert(rows);
// UTF-8 BOM for Excel compatibility
final bom = '\uFEFF';
final content = bom + csvStr;
final file = await _tempFile('timetrack_export', 'csv');
await file.writeAsString(content, encoding: utf8);
await Share.shareXFiles([XFile(file.path)],
subject: 'Timetrack Export');
}
Future<void> _exportPdf(
List<TimeEntry> entries,
Map<int, Project> projectMap,
DateTime from,
DateTime to,
) async {
final doc = pw.Document();
doc.addPage(pw.Page(
build: (ctx) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Timetrack Export',
style: pw.TextStyle(fontSize: 20, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 4),
pw.Text(
'${from.toIso8601String().substring(0, 10)} ${to.toIso8601String().substring(0, 10)}'),
pw.SizedBox(height: 16),
pw.TableHelper.fromTextArray(
headers: ['Project', 'Start', 'End', 'Duration', 'Note'],
data: entries.map((e) {
final dur = Duration(seconds: e.durationSeconds ?? 0);
return [
projectMap[e.projectId]?.name ?? '',
e.startTime.toIso8601String().substring(0, 16),
e.endTime?.toIso8601String().substring(0, 16) ?? '',
'${dur.inHours}h ${dur.inMinutes % 60}m',
e.note ?? '',
];
}).toList(),
),
],
),
));
final file = await _tempFile('timetrack_report', 'pdf');
await file.writeAsBytes(await doc.save());
await Share.shareXFiles([XFile(file.path)],
subject: 'Timetrack Report');
}
Future<void> _exportJson(
List<TimeEntry> entries,
List<Project> projects,
) async {
final data = {
'exportedAt': DateTime.now().toIso8601String(),
'version': 1,
'projects': projects
.map((p) => {
'id': p.id,
'name': p.name,
'color': p.colorValue,
'description': p.description,
})
.toList(),
'entries': entries
.map((e) => {
'id': e.id,
'projectId': e.projectId,
'startTime': e.startTime.toIso8601String(),
'endTime': e.endTime?.toIso8601String(),
'durationSeconds': e.durationSeconds,
'note': e.note,
'tags': e.tags,
})
.toList(),
};
final file = await _tempFile('timetrack_backup', 'json');
await file.writeAsString(const JsonEncoder.withIndent(' ').convert(data));
await Share.shareXFiles([XFile(file.path)],
subject: 'Timetrack Backup');
}
Future<File> _tempFile(String name, String ext) async {
final dir = await getTemporaryDirectory();
final date = DateTime.now().toIso8601String().substring(0, 10);
return File('${dir.path}/${name}_$date.$ext');
}
}