173 lines
5.5 KiB
Dart
173 lines
5.5 KiB
Dart
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');
|
||
}
|
||
}
|