Merge NDM-App-232-connect-addtask-to-drift-db-remove-sample-tasks into dev

This commit is contained in:
TrochtaOndrej
2026-03-18 21:08:17 +01:00
5 changed files with 181 additions and 201 deletions
@@ -1,51 +1,33 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../data/db/app_database.dart';
import '../../../data/repositories/task_repository.dart';
import '../../../domain/entities/task_entity.dart';
import '../../../domain/entities/task_status.dart';
/// Notifier managing the task list.
class TasksNotifier extends Notifier<List<TaskEntity>> {
@override
List<TaskEntity> build() => _sampleTasks;
// ── Infrastructure providers ──
void completeTask(String id) {
state = [
for (final task in state)
if (task.id == id)
task.copyWith(
status: TaskStatus.done,
updatedAt: DateTime.now(),
)
else
task,
];
}
/// Singleton Drift database instance.
final appDatabaseProvider = Provider<AppDatabase>((ref) {
final db = AppDatabase();
ref.onDispose(() => db.close());
return db;
});
void uncompleteTask(String id) {
state = [
for (final task in state)
if (task.id == id)
task.copyWith(
status: TaskStatus.pending,
updatedAt: DateTime.now(),
)
else
task,
];
}
/// Task repository backed by Drift.
final taskRepositoryProvider = Provider<TaskRepository>((ref) {
return TaskRepository(ref.watch(appDatabaseProvider));
});
void skipTask(String id) {
final skipped = state.firstWhere((t) => t.id == id);
state = [
...state.where((t) => t.id != id),
skipped.copyWith(sortOrder: skipped.sortOrder + 100),
];
}
}
// ── Task list (reactive stream from DB) ──
/// All tasks.
final tasksProvider =
NotifierProvider<TasksNotifier, List<TaskEntity>>(TasksNotifier.new);
const _userId = 'user-1';
/// All tasks for the current user, streamed from Drift.
final tasksProvider = StreamProvider<List<TaskEntity>>((ref) {
final repo = ref.watch(taskRepositoryProvider);
return repo.watchTasks(_userId);
});
/// Alias for [tasksProvider]. Returns all tasks — no date filtering in MVP.
/// Screens are responsible for splitting into pending/completed.
@@ -53,7 +35,7 @@ final todayTasksProvider = tasksProvider;
/// Pending tasks only, ordered by sort order.
final pendingTasksProvider = Provider<List<TaskEntity>>((ref) {
final tasks = ref.watch(tasksProvider);
final tasks = ref.watch(tasksProvider).value ?? [];
return tasks
.where((t) => t.status == TaskStatus.pending)
.toList()
@@ -71,63 +53,8 @@ final nextZenTaskProvider = Provider<TaskEntity?>((ref) {
/// Count of completed self-care tasks.
final selfCareCountProvider = Provider<int>((ref) {
final tasks = ref.watch(tasksProvider);
final tasks = ref.watch(tasksProvider).value ?? [];
return tasks
.where((t) => t.isSelfCare && t.status == TaskStatus.done)
.length;
});
// — Sample data for development —
final _now = DateTime.now();
final _sampleTasks = <TaskEntity>[
TaskEntity(
id: '1',
userId: '1',
title: 'Nakoupit potraviny',
energyLevel: 1,
sortOrder: 0,
createdAt: _now,
updatedAt: _now,
),
TaskEntity(
id: '2',
userId: '1',
title: 'Udělat prezentaci',
energyLevel: 3,
sortOrder: 1,
createdAt: _now,
updatedAt: _now,
),
TaskEntity(
id: '3',
userId: '1',
title: 'Protáhnout se',
energyLevel: 1,
isSelfCare: true,
sortOrder: 2,
createdAt: _now,
updatedAt: _now,
),
TaskEntity(
id: '4',
userId: '1',
title: 'Napsat email klientovi',
energyLevel: 2,
sortOrder: 3,
createdAt: _now,
updatedAt: _now,
),
TaskEntity(
id: '5',
userId: '1',
title: 'Meditace',
energyLevel: 1,
isSelfCare: true,
status: TaskStatus.done,
sortOrder: 4,
createdAt: _now,
updatedAt: _now,
),
];
@@ -1,10 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uuid/uuid.dart';
import '../../../core/constants/app_constants.dart';
import '../../../core/theme/kittie_colors.dart';
import '../../../core/theme/kittie_typography.dart';
import '../../../domain/entities/task_entity.dart';
import '../providers/add_task_providers.dart';
import '../providers/task_providers.dart';
/// Shows the Add Task bottom sheet. Call from any screen:
/// ```dart
@@ -45,10 +48,43 @@ class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
}
void _saveTask() {
// TODO: persist task via repository
final title = ref.read(taskTitleProvider);
final note = ref.read(taskNoteProvider);
final energy = ref.read(selectedEnergyProvider) ?? 2;
final taskDay = ref.read(selectedTaskDayProvider);
final taskTime = ref.read(selectedTaskTimeProvider);
final deadline = _resolveDeadline(taskDay, taskTime);
final now = DateTime.now();
final tasks = ref.read(tasksProvider).value ?? [];
final entity = TaskEntity(
id: const Uuid().v4(),
userId: 'user-1',
title: title,
description: note.isEmpty ? null : note,
energyLevel: energy,
sortOrder: tasks.length,
deadline: deadline,
createdAt: now,
updatedAt: now,
);
ref.read(taskRepositoryProvider).insertTask(entity);
ref.read(addTaskStepProvider.notifier).next();
}
DateTime? _resolveDeadline(TaskDay? day, TimeOfDay? time) {
final now = DateTime.now();
final int hour = time?.hour ?? 9;
final int minute = time?.minute ?? 0;
return switch (day) {
TaskDay.today => DateTime(now.year, now.month, now.day, hour, minute),
TaskDay.tomorrow => DateTime(
now.year, now.month, now.day + 1, hour, minute),
_ => null,
};
}
void _resetAndClose() {
ref.invalidate(addTaskStepProvider);
ref.invalidate(taskTitleProvider);
+115 -101
View File
@@ -6,6 +6,7 @@ import 'package:google_fonts/google_fonts.dart';
import '../../../core/theme/kittie_colors.dart';
import '../screens/add_task_screen.dart';
import '../../../domain/entities/pet_type.dart';
import '../../../domain/entities/task_status.dart';
import '../../../shared/utils/czech_date.dart';
import '../../../shared/widgets/energy_bar.dart';
import '../../../shared/widgets/pet_bubble.dart';
@@ -21,129 +22,142 @@ class TasksScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tasks = ref.watch(todayTasksProvider);
final tasksAsync = ref.watch(todayTasksProvider);
final petState = ref.watch(petStateProvider);
final userState = ref.watch(userProvider);
final petMood = ref.watch(petMoodProvider);
final userName = ref.watch(userNameProvider);
final pendingTasks = tasks.where((t) => !t.isCompleted).toList();
final completedTasks = tasks.where((t) => t.isCompleted).toList();
final pendingCount = pendingTasks.length;
return Scaffold(
body: SafeArea(
child: tasks.isEmpty
? _EmptyState(petType: userState.petType)
: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Date header
Text(
czechDateHeader(DateTime.now()),
style: GoogleFonts.lexend(
fontSize: 12,
fontWeight: FontWeight.w500,
color: KittieColors.brownLight,
letterSpacing: 1.2,
),
child: tasksAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('Chyba: $e')),
data: (tasks) {
final pendingTasks =
tasks.where((t) => !t.isCompleted).toList();
final completedTasks =
tasks.where((t) => t.isCompleted).toList();
final pendingCount = pendingTasks.length;
if (tasks.isEmpty) {
return _EmptyState(petType: userState.petType);
}
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. Date header
Text(
czechDateHeader(DateTime.now()),
style: GoogleFonts.lexend(
fontSize: 12,
fontWeight: FontWeight.w500,
color: KittieColors.brownLight,
letterSpacing: 1.2,
),
const SizedBox(height: 4),
),
const SizedBox(height: 4),
// 2. Greeting
Text(
'${czechGreeting()}, $userName',
style: GoogleFonts.lexend(
fontSize: 22,
fontWeight: FontWeight.w600,
color: KittieColors.brownDark,
),
// 2. Greeting
Text(
'${czechGreeting()}, $userName',
style: GoogleFonts.lexend(
fontSize: 22,
fontWeight: FontWeight.w600,
color: KittieColors.brownDark,
),
const SizedBox(height: 20),
),
const SizedBox(height: 20),
// 3. Energy bar
EnergyBar(energyLevel: petState.energyToday),
const SizedBox(height: 20),
// 3. Energy bar
EnergyBar(energyLevel: petState.energyToday),
const SizedBox(height: 20),
// 4. Pet area
PetBubble(
petType: userState.petType,
petName: userState.petName,
mood: petMood.emoji,
message: _petMessage(petMood, userState.petName),
),
const SizedBox(height: 16),
// 4. Pet area
PetBubble(
petType: userState.petType,
petName: userState.petName,
mood: petMood.emoji,
message: _petMessage(petMood, userState.petName),
),
const SizedBox(height: 16),
// 5. Streak row
StreakDots(streakDays: petState.streakDays),
const SizedBox(height: 24),
// 5. Streak row
StreakDots(streakDays: petState.streakDays),
const SizedBox(height: 24),
// 6. Section header
Row(
children: [
Text(
'DNEŠNÍ ÚKOLY',
style: GoogleFonts.lexend(
fontSize: 12,
fontWeight: FontWeight.w600,
color: KittieColors.brownLight,
letterSpacing: 1.2,
),
// 6. Section header
Row(
children: [
Text(
'DNEŠNÍ ÚKOLY',
style: GoogleFonts.lexend(
fontSize: 12,
fontWeight: FontWeight.w600,
color: KittieColors.brownLight,
letterSpacing: 1.2,
),
const SizedBox(width: 8),
Container(
width: 22,
height: 22,
decoration: const BoxDecoration(
color: KittieColors.amber,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$pendingCount',
style: GoogleFonts.lexend(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
const SizedBox(width: 8),
Container(
width: 22,
height: 22,
decoration: const BoxDecoration(
color: KittieColors.amber,
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$pendingCount',
style: GoogleFonts.lexend(
fontSize: 11,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
],
),
const SizedBox(height: 12),
),
],
),
const SizedBox(height: 12),
// 7. Task list — pending first, completed at bottom
...pendingTasks.map(
(task) => TaskCard(
task: task,
onComplete: () {
HapticFeedback.mediumImpact();
ref
.read(todayTasksProvider.notifier)
.completeTask(task.id);
ref
.read(petStateProvider.notifier)
.onTaskCompleted();
},
),
// 7. Task list — pending first, completed at bottom
...pendingTasks.map(
(task) => TaskCard(
task: task,
onComplete: () {
HapticFeedback.mediumImpact();
ref
.read(taskRepositoryProvider)
.completeTask(task.id);
ref
.read(petStateProvider.notifier)
.onTaskCompleted();
},
),
...completedTasks.map(
(task) => TaskCard(
task: task,
onComplete: () {
HapticFeedback.lightImpact();
ref
.read(todayTasksProvider.notifier)
.uncompleteTask(task.id);
},
),
),
...completedTasks.map(
(task) => TaskCard(
task: task,
onComplete: () {
HapticFeedback.lightImpact();
ref.read(taskRepositoryProvider).updateTask(
task.copyWith(
status: TaskStatus.pending,
updatedAt: DateTime.now(),
),
);
},
),
],
),
),
],
),
);
},
),
),
// 8. FAB
@@ -56,7 +56,7 @@ class ZenScreen extends ConsumerWidget {
Expanded(
child: ElevatedButton(
onPressed: () {
ref.read(tasksProvider.notifier).completeTask(task.id);
ref.read(taskRepositoryProvider).completeTask(task.id);
ref.read(petStateProvider.notifier).incrementTasksDone();
},
style: ElevatedButton.styleFrom(
@@ -76,7 +76,10 @@ class ZenScreen extends ConsumerWidget {
Expanded(
child: TextButton(
onPressed: () {
ref.read(tasksProvider.notifier).skipTask(task.id);
ref.read(taskRepositoryProvider).updateTask(
task.copyWith(
sortOrder: task.sortOrder + 100),
);
},
style: TextButton.styleFrom(
foregroundColor: KittieColors.brownLight,
+1 -1
View File
@@ -9,7 +9,7 @@ void main() {
await tester.pumpWidget(
const ProviderScope(child: KittieApp()),
);
await tester.pumpAndSettle();
await tester.pump();
// App renders without crashing
expect(find.byType(MaterialApp), findsOneWidget);