timetracker/lib/features/settings/presentation/widgets/export_sheet.dart
2026-08-03 21:51:48 +02:00

167 lines
5.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:timetrack/features/projects/domain/projects_provider.dart';
import 'package:timetrack/features/settings/data/export_service.dart';
class ExportSheet extends ConsumerStatefulWidget {
const ExportSheet({super.key});
static Future<void> show(BuildContext context) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => const ExportSheet(),
);
}
@override
ConsumerState<ExportSheet> createState() => _ExportSheetState();
}
class _ExportSheetState extends ConsumerState<ExportSheet> {
ExportFormat _format = ExportFormat.csv;
_Range _range = _Range.thisWeek;
int? _projectId;
bool _exporting = false;
@override
Widget build(BuildContext context) {
final projectsAsync = ref.watch(allProjectsProvider);
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
top: 16,
left: 16,
right: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Export data', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 16),
// Format
Text('Format', style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: ExportFormat.values.map((f) {
final label = f.name.toUpperCase();
return ChoiceChip(
label: Text(label),
selected: _format == f,
onSelected: (_) => setState(() => _format = f),
);
}).toList(),
),
const SizedBox(height: 12),
// Date range
Text('Date range', style: Theme.of(context).textTheme.labelLarge),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: _Range.values.map((r) {
return ChoiceChip(
label: Text(r.label),
selected: _range == r,
onSelected: (_) => setState(() => _range = r),
);
}).toList(),
),
const SizedBox(height: 12),
// Project filter
projectsAsync.when(
data: (projects) => DropdownButtonFormField<int?>(
initialValue: _projectId,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Project (optional)',
),
items: [
const DropdownMenuItem(value: null, child: Text('All projects')),
...projects.map((p) => DropdownMenuItem(
value: p.id,
child: Text(p.name),
)),
],
onChanged: (id) => setState(() => _projectId = id),
),
loading: () => const LinearProgressIndicator(),
error: (e, _) => const SizedBox.shrink(),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _exporting ? null : _doExport,
icon: _exporting
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.share),
label: const Text('Share'),
),
),
],
),
);
}
Future<void> _doExport() async {
setState(() => _exporting = true);
try {
final (from, to) = _range.bounds;
await ref.read(exportServiceProvider).export(
format: _format,
from: from,
to: to,
projectId: _projectId,
);
if (mounted) Navigator.pop(context);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Export failed: $e')),
);
}
} finally {
if (mounted) setState(() => _exporting = false);
}
}
}
enum _Range {
today('Today'),
thisWeek('This week'),
thisMonth('This month');
const _Range(this.label);
final String label;
(DateTime, DateTime) get bounds {
final now = DateTime.now();
switch (this) {
case _Range.today:
final start = DateTime(now.year, now.month, now.day);
return (start, start.add(const Duration(days: 1)));
case _Range.thisWeek:
final monday = now.subtract(Duration(days: now.weekday - 1));
final start = DateTime(monday.year, monday.month, monday.day);
return (start, start.add(const Duration(days: 7)));
case _Range.thisMonth:
final start = DateTime(now.year, now.month);
return (start, DateTime(now.year, now.month + 1));
}
}
}