feat: Connect AddTask to Drift DB — remove sample tasks [AITM-232]
This commit is contained in:
@@ -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).valueOrNull ?? [];
|
||||
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user