135 lines
4.3 KiB
Dart
135 lines
4.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'package:timetrack/features/entries/domain/entries_provider.dart';
|
|
import 'package:timetrack/features/entries/domain/time_entry.dart';
|
|
import 'package:timetrack/features/entries/presentation/widgets/date_range_filter.dart';
|
|
import 'package:timetrack/features/entries/presentation/widgets/day_header.dart';
|
|
import 'package:timetrack/features/entries/presentation/widgets/entry_form_sheet.dart';
|
|
import 'package:timetrack/features/entries/presentation/widgets/entry_list_tile.dart';
|
|
import 'package:timetrack/features/projects/domain/project.dart';
|
|
import 'package:timetrack/features/projects/domain/projects_provider.dart';
|
|
|
|
class EntriesScreen extends ConsumerStatefulWidget {
|
|
const EntriesScreen({super.key});
|
|
|
|
@override
|
|
ConsumerState<EntriesScreen> createState() => _EntriesScreenState();
|
|
}
|
|
|
|
class _EntriesScreenState extends ConsumerState<EntriesScreen> {
|
|
DateRangeFilter _filter = DateRangeFilter.thisWeek;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final (from, to) = dateRangeBounds(_filter);
|
|
final entriesAsync = ref.watch(
|
|
entriesByDateRangeProvider(rangeFrom: from, rangeTo: to),
|
|
);
|
|
final projectsAsync = ref.watch(allProjectsProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Entries'),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.add),
|
|
tooltip: 'Add entry',
|
|
onPressed: () => EntryFormSheet.show(context),
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
DateRangeFilterChips(
|
|
selected: _filter,
|
|
onChanged: (f) => setState(() => _filter = f),
|
|
),
|
|
Expanded(
|
|
child: entriesAsync.when(
|
|
data: (entries) => projectsAsync.when(
|
|
data: (projects) => _buildList(entries, projects),
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(child: Text('Error: $e')),
|
|
),
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (e, _) => Center(child: Text('Error: $e')),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildList(List<TimeEntry> entries, List<Project> projects) {
|
|
if (entries.isEmpty) {
|
|
return const Center(
|
|
key: Key('entries_empty'),
|
|
child: Text('No entries yet.'),
|
|
);
|
|
}
|
|
|
|
final projectMap = {for (final p in projects) p.id: p};
|
|
|
|
// Group by date
|
|
final grouped = <DateTime, List<TimeEntry>>{};
|
|
for (final entry in entries) {
|
|
final day = DateTime(
|
|
entry.startTime.year,
|
|
entry.startTime.month,
|
|
entry.startTime.day,
|
|
);
|
|
grouped.putIfAbsent(day, () => []).add(entry);
|
|
}
|
|
|
|
final sortedDays = grouped.keys.toList()
|
|
..sort((a, b) => b.compareTo(a));
|
|
|
|
return ListView.builder(
|
|
itemCount: sortedDays.length,
|
|
itemBuilder: (context, index) {
|
|
final day = sortedDays[index];
|
|
final dayEntries = grouped[day]!;
|
|
final totalSeconds = dayEntries.fold<int>(
|
|
0,
|
|
(sum, e) => sum + (e.durationSeconds ?? 0),
|
|
);
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
DayHeader(
|
|
date: day,
|
|
totalDuration: Duration(seconds: totalSeconds),
|
|
),
|
|
...dayEntries.map((entry) {
|
|
final project = projectMap[entry.projectId];
|
|
return EntryListTile(
|
|
key: Key('entry_tile_${entry.id}'),
|
|
entry: entry,
|
|
projectName: project?.name ?? '—',
|
|
projectColor: project?.colorValue ?? 0xFF888888,
|
|
onTap: () => EntryFormSheet.show(context, existing: entry),
|
|
onDelete: () => _deleteWithUndo(entry),
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
void _deleteWithUndo(TimeEntry entry) {
|
|
ref.read(entriesNotifierProvider.notifier).delete(entry.id);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: const Text('Entry deleted'),
|
|
action: SnackBarAction(
|
|
label: 'Undo',
|
|
onPressed: () =>
|
|
ref.read(entriesNotifierProvider.notifier).create(entry),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|