timetracker/lib/features/reports/presentation/reports_screen.dart
2026-08-03 21:51:48 +02:00

142 lines
4.7 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/reports/domain/report_data.dart';
import 'package:timetrack/features/reports/domain/reports_provider.dart';
import 'package:timetrack/features/reports/presentation/widgets/duration_bar_chart.dart';
import 'package:timetrack/features/reports/presentation/widgets/period_navigator.dart';
import 'package:timetrack/features/reports/presentation/widgets/project_breakdown_list.dart';
import 'package:timetrack/features/reports/presentation/widgets/summary_header.dart';
class ReportsScreen extends ConsumerWidget {
const ReportsScreen({super.key});
static const _weekLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
static const _dayLabels = [
'0', '2', '4', '6', '8', '10', '12', '14', '16', '18', '20', '22',
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifier = ref.read(reportsNotifierProvider.notifier);
final reportAsync = ref.watch(reportsNotifierProvider);
final projectsAsync = ref.watch(allProjectsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Reports'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: _PeriodTabs(
current: notifier.period,
onChanged: notifier.selectPeriod,
),
),
),
body: reportAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
data: (report) => _buildBody(context, ref, report, projectsAsync, notifier),
),
);
}
Widget _buildBody(
BuildContext context,
WidgetRef ref,
ReportData report,
AsyncValue projectsAsync,
ReportsNotifier notifier,
) {
final chartLabels = _labelsFor(notifier.period, report.chartValues.length);
return ListView(
children: [
PeriodNavigator(
period: notifier.period,
periodStart: report.periodStart,
periodEnd: report.periodEnd,
onPrevious: notifier.goToPrevious,
onNext: notifier.goToNext,
),
SummaryHeader(
totalDuration: report.totalDuration,
entryCount: report.durationByProject.length,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: SizedBox(
height: 200,
child: report.chartValues.isEmpty ||
report.chartValues.every((v) => v == 0)
? Center(
child: Text(
'No data for this period',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
)
: DurationBarChart(
values: report.chartValues,
labels: chartLabels,
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: Text(
'By project',
style: Theme.of(context).textTheme.labelLarge,
),
),
projectsAsync.when(
data: (projects) => ProjectBreakdownList(
durationByProject: report.durationByProject,
projects: projects,
totalDuration: report.totalDuration,
),
loading: () => const SizedBox.shrink(),
error: (e, _) => const SizedBox.shrink(),
),
],
);
}
List<String> _labelsFor(ReportPeriod period, int count) {
switch (period) {
case ReportPeriod.week:
return _weekLabels;
case ReportPeriod.day:
return _dayLabels;
case ReportPeriod.month:
return List.generate(count, (i) => '${i + 1}');
}
}
}
class _PeriodTabs extends StatelessWidget {
const _PeriodTabs({required this.current, required this.onChanged});
final ReportPeriod current;
final ValueChanged<ReportPeriod> onChanged;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: ReportPeriod.values.map((p) {
final label = p.name[0].toUpperCase() + p.name.substring(1);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: ChoiceChip(
label: Text(label),
selected: current == p,
onSelected: (_) => onChanged(p),
),
);
}).toList(),
);
}
}