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

160 lines
4.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:timetrack/features/projects/domain/project.dart';
import 'package:timetrack/features/projects/domain/projects_provider.dart';
import 'package:timetrack/features/projects/presentation/widgets/project_form_sheet.dart';
import 'package:timetrack/features/projects/presentation/widgets/project_list_tile.dart';
class ProjectsScreen extends ConsumerStatefulWidget {
const ProjectsScreen({super.key});
@override
ConsumerState<ProjectsScreen> createState() => _ProjectsScreenState();
}
class _ProjectsScreenState extends ConsumerState<ProjectsScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Projects'),
actions: [
IconButton(
icon: const Icon(Icons.add),
tooltip: 'New project',
onPressed: () => ProjectFormSheet.show(context),
),
],
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Active'),
Tab(text: 'Archived'),
],
),
),
body: TabBarView(
controller: _tabController,
children: const [
_ProjectList(archived: false),
_ProjectList(archived: true),
],
),
);
}
}
class _ProjectList extends ConsumerWidget {
const _ProjectList({required this.archived});
final bool archived;
@override
Widget build(BuildContext context, WidgetRef ref) {
final projectsAsync = archived
? ref.watch(allProjectsProvider)
: ref.watch(activeProjectsProvider);
return projectsAsync.when(
data: (allProjects) {
final projects = archived
? allProjects.where((p) => p.archivedAt != null).toList()
: allProjects;
if (projects.isEmpty) {
return Center(
key: Key(archived ? 'archived_empty' : 'active_empty'),
child: Text(
archived ? 'No archived projects.' : 'No projects yet.',
),
);
}
return ListView.builder(
itemCount: projects.length,
itemBuilder: (context, index) {
final project = projects[index];
return ProjectListTile(
key: Key('project_tile_${project.id}'),
project: project,
totalDuration: Duration.zero, // TODO: wire up from reports repo
onTap: () => ProjectFormSheet.show(context, existing: project),
onArchive: () => _toggleArchive(context, ref, project),
onDelete: () => _confirmDelete(context, ref, project),
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Error: $e')),
);
}
void _toggleArchive(BuildContext context, WidgetRef ref, Project project) {
final notifier = ref.read(projectsNotifierProvider.notifier);
if (project.archivedAt == null) {
notifier.archive(project.id);
} else {
notifier.unarchive(project.id);
}
}
Future<void> _confirmDelete(
BuildContext context,
WidgetRef ref,
Project project,
) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete project?'),
content: Text(
'Delete "${project.name}"? This cannot be undone.\n\n'
'Projects with existing entries cannot be deleted.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
style: FilledButton.styleFrom(
backgroundColor: Theme.of(ctx).colorScheme.error,
),
child: const Text('Delete'),
),
],
),
);
if (confirmed == true) {
try {
await ref.read(projectsNotifierProvider.notifier).delete(project.id);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Cannot delete — project has time entries.'),
),
);
}
}
}
}
}