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 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 _exportCsv( List entries, Map projectMap, ) async { final rows = >[ ['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 _exportPdf( List entries, Map 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 _exportJson( List entries, List 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 _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'); } }