142 lines
4.7 KiB
Dart
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(),
|
|
);
|
|
}
|
|
}
|