Files
TaskManagerMobile/lib/features/tasks/screens/add_task_screen.dart
T

532 lines
17 KiB
Dart

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
/// showAddTaskSheet(context);
/// ```
void showAddTaskSheet(BuildContext context) {
showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => const _AddTaskSheet(),
);
}
class _AddTaskSheet extends ConsumerStatefulWidget {
const _AddTaskSheet();
@override
ConsumerState<_AddTaskSheet> createState() => _AddTaskSheetState();
}
class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
late final TextEditingController _titleController;
late final TextEditingController _noteController;
@override
void initState() {
super.initState();
_titleController = TextEditingController();
_noteController = TextEditingController();
}
@override
void dispose() {
_titleController.dispose();
_noteController.dispose();
super.dispose();
}
void _saveTask() {
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);
ref.invalidate(taskNoteProvider);
ref.invalidate(selectedTaskDayProvider);
ref.invalidate(selectedTaskTimeProvider);
ref.invalidate(selectedEnergyProvider);
_titleController.clear();
_noteController.clear();
Navigator.of(context).pop();
}
void _addAnother() {
ref.invalidate(addTaskStepProvider);
ref.invalidate(taskTitleProvider);
ref.invalidate(taskNoteProvider);
ref.invalidate(selectedTaskDayProvider);
ref.invalidate(selectedTaskTimeProvider);
ref.invalidate(selectedEnergyProvider);
_titleController.clear();
_noteController.clear();
}
@override
Widget build(BuildContext context) {
final step = ref.watch(addTaskStepProvider);
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Container(
decoration: const BoxDecoration(
color: KittieColors.cream,
borderRadius: BorderRadius.vertical(
top: Radius.circular(AppConstants.bottomSheetBorderRadius),
),
),
padding: EdgeInsets.only(
left: 24,
right: 24,
top: 16,
bottom: 24 + bottomInset,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: KittieColors.creamBorder,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(height: 16),
// Step dots
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (i) {
final isActive = i == step;
final isPast = i < step;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 3),
width: isActive ? 20 : 6,
height: 6,
decoration: BoxDecoration(
color: isActive
? KittieColors.brown
: isPast
? KittieColors.sageGreen
: KittieColors.creamBorder,
borderRadius: BorderRadius.circular(3),
),
);
}),
),
const SizedBox(height: 20),
// Step content
AnimatedSwitcher(
duration: const Duration(milliseconds: 250),
child: switch (step) {
0 => _Step1Title(
key: const ValueKey(0),
titleController: _titleController,
noteController: _noteController,
onNext: () {
if (_titleController.text.trim().isNotEmpty) {
ref
.read(taskTitleProvider.notifier)
.update(_titleController.text.trim());
ref
.read(taskNoteProvider.notifier)
.update(_noteController.text.trim());
ref.read(addTaskStepProvider.notifier).next();
}
},
),
1 => _Step2When(
key: const ValueKey(1),
onNext: () =>
ref.read(addTaskStepProvider.notifier).next(),
onBack: () =>
ref.read(addTaskStepProvider.notifier).previous(),
),
2 => _Step3Energy(
key: const ValueKey(2),
onSave: _saveTask,
onBack: () =>
ref.read(addTaskStepProvider.notifier).previous(),
),
3 => _Step4Confirmation(
key: const ValueKey(3),
onClose: _resetAndClose,
onAddAnother: _addAnother,
),
_ => const SizedBox.shrink(),
},
),
],
),
);
}
}
// ─── Step 1: Title & Note ───────────────────────────────────────────────────
class _Step1Title extends StatelessWidget {
const _Step1Title({
super.key,
required this.titleController,
required this.noteController,
required this.onNext,
});
final TextEditingController titleController;
final TextEditingController noteController;
final VoidCallback onNext;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Nový úkol', style: KittieTypography.h2),
const SizedBox(height: 20),
Text('NÁZEV', style: KittieTypography.labelMedium),
const SizedBox(height: 8),
TextField(
controller: titleController,
autofocus: true,
style: KittieTypography.bodyLarge
.copyWith(color: KittieColors.brownDark),
decoration: const InputDecoration(hintText: 'Co potřebuješ udělat?'),
),
const SizedBox(height: 16),
Text('POZNÁMKA', style: KittieTypography.labelMedium),
const SizedBox(height: 8),
TextField(
controller: noteController,
style: KittieTypography.bodyMedium
.copyWith(color: KittieColors.brownDark),
maxLines: 3,
decoration: const InputDecoration(hintText: 'Volitelné...'),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onNext,
child: const Text('Pokračovat'),
),
),
],
);
}
}
// ─── Step 2: When ───────────────────────────────────────────────────────────
class _Step2When extends ConsumerWidget {
const _Step2When({
super.key,
required this.onNext,
required this.onBack,
});
final VoidCallback onNext;
final VoidCallback onBack;
static const _days = [
(day: TaskDay.today, label: 'Dnes'),
(day: TaskDay.tomorrow, label: 'Zítra'),
(day: TaskDay.other, label: 'Jindy'),
(day: TaskDay.someday, label: 'Někdy'),
];
static const _timePresets = [8, 10, 14, 18];
@override
Widget build(BuildContext context, WidgetRef ref) {
final selectedDay = ref.watch(selectedTaskDayProvider);
final selectedTime = ref.watch(selectedTaskTimeProvider);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Kdy to chceš udělat?', style: KittieTypography.h2),
const SizedBox(height: 20),
// Day chips
Wrap(
spacing: 8,
runSpacing: 8,
children: _days.map((d) {
final isSelected = selectedDay == d.day;
return ChoiceChip(
label: Text(d.label),
selected: isSelected,
onSelected: (_) {
ref.read(selectedTaskDayProvider.notifier).select(d.day);
},
selectedColor: KittieColors.amber,
backgroundColor: KittieColors.creamDark,
labelStyle: KittieTypography.labelLarge.copyWith(
color:
isSelected ? KittieColors.brownDark : KittieColors.brown,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide.none,
showCheckmark: false,
);
}).toList(),
),
const SizedBox(height: 24),
// Time presets
Text('Čas', style: KittieTypography.labelMedium),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: _timePresets.map((hour) {
final time = TimeOfDay(hour: hour, minute: 0);
final isSelected = selectedTime?.hour == hour;
return ChoiceChip(
label: Text('$hour:00'),
selected: isSelected,
onSelected: (_) {
ref.read(selectedTaskTimeProvider.notifier).select(time);
},
selectedColor: KittieColors.amber,
backgroundColor: KittieColors.creamDark,
labelStyle: KittieTypography.labelLarge.copyWith(
color:
isSelected ? KittieColors.brownDark : KittieColors.brown,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
side: BorderSide.none,
showCheckmark: false,
);
}).toList(),
),
const SizedBox(height: 24),
Row(
children: [
TextButton(
onPressed: onBack,
child: const Text('Zpět'),
),
const Spacer(),
ElevatedButton(
onPressed: onNext,
child: const Text('Pokračovat'),
),
],
),
],
);
}
}
// ─── Step 3: Energy ─────────────────────────────────────────────────────────
class _Step3Energy extends ConsumerWidget {
const _Step3Energy({
super.key,
required this.onSave,
required this.onBack,
});
final VoidCallback onSave;
final VoidCallback onBack;
static const _energyOptions = [
(level: 1, label: 'Lehké', emoji: '🍃', color: KittieColors.sageGreen),
(level: 2, label: 'Střední', emoji: '⚡', color: KittieColors.orange),
(level: 3, label: 'Náročné', emoji: '🔥', color: KittieColors.red),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final selected = ref.watch(selectedEnergyProvider);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Pet bubble
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: KittieColors.creamDark,
borderRadius: BorderRadius.circular(16),
),
child: Row(
children: [
const Text('🐱', style: TextStyle(fontSize: 28)),
const SizedBox(width: 12),
Expanded(
child: Text(
'Kolik energie to bude stát?',
style: KittieTypography.bodyLarge,
),
),
],
),
),
const SizedBox(height: 24),
// Energy picker
Row(
children: _energyOptions.map((opt) {
final isSelected = selected == opt.level;
return Expanded(
child: GestureDetector(
onTap: () {
ref.read(selectedEnergyProvider.notifier).select(opt.level);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: isSelected
? opt.color.withValues(alpha: 0.15)
: KittieColors.creamDark,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isSelected ? opt.color : Colors.transparent,
width: 2,
),
),
child: Column(
children: [
Text(opt.emoji, style: const TextStyle(fontSize: 28)),
const SizedBox(height: 8),
Text(
opt.label,
style: KittieTypography.labelLarge.copyWith(
color: isSelected ? opt.color : KittieColors.brown,
),
),
],
),
),
),
);
}).toList(),
),
const SizedBox(height: 24),
Row(
children: [
TextButton(
onPressed: onBack,
child: const Text('Zpět'),
),
const Spacer(),
ElevatedButton(
onPressed: selected != null ? onSave : null,
style: ElevatedButton.styleFrom(
backgroundColor: KittieColors.sageGreen,
foregroundColor: KittieColors.cream,
disabledBackgroundColor: KittieColors.creamDark,
),
child: const Text('Uložit úkol'),
),
],
),
],
);
}
}
// ─── Step 4: Confirmation ───────────────────────────────────────────────────
class _Step4Confirmation extends StatelessWidget {
const _Step4Confirmation({
super.key,
required this.onClose,
required this.onAddAnother,
});
final VoidCallback onClose;
final VoidCallback onAddAnother;
@override
Widget build(BuildContext context) {
return Column(
children: [
const Icon(
Icons.check_circle_rounded,
color: KittieColors.sageGreen,
size: 64,
),
const SizedBox(height: 16),
Text('Úkol uložen!', style: KittieTypography.h2),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: onClose,
child: const Text('Zpět na seznam'),
),
),
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: onAddAnother,
child: const Text('Přidat další úkol'),
),
),
],
);
}
}