204 lines
6.3 KiB
Dart
204 lines
6.3 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
|
|
import 'package:go_router/go_router.dart';
|
||
|
|
|
||
|
|
import '../../domain/entities/create_todo_params.dart';
|
||
|
|
import '../../domain/entities/update_todo_params.dart';
|
||
|
|
import '../providers/todo_providers.dart';
|
||
|
|
|
||
|
|
class TodoDetailScreen extends ConsumerStatefulWidget {
|
||
|
|
final String? todoId;
|
||
|
|
|
||
|
|
const TodoDetailScreen({super.key, this.todoId});
|
||
|
|
|
||
|
|
@override
|
||
|
|
ConsumerState<TodoDetailScreen> createState() => _TodoDetailScreenState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _TodoDetailScreenState extends ConsumerState<TodoDetailScreen> {
|
||
|
|
final _formKey = GlobalKey<FormState>();
|
||
|
|
late final TextEditingController _titleController;
|
||
|
|
late final TextEditingController _descriptionController;
|
||
|
|
String _priority = 'medium';
|
||
|
|
DateTime? _dueDate;
|
||
|
|
bool _isLoading = false;
|
||
|
|
|
||
|
|
bool get isEditing => widget.todoId != null;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_titleController = TextEditingController();
|
||
|
|
_descriptionController = TextEditingController();
|
||
|
|
|
||
|
|
if (isEditing) {
|
||
|
|
_loadTodo();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _loadTodo() async {
|
||
|
|
final todoAsync = ref.read(todoDetailProvider(widget.todoId!));
|
||
|
|
todoAsync.whenData((todo) {
|
||
|
|
_titleController.text = todo.title;
|
||
|
|
_descriptionController.text = todo.description ?? '';
|
||
|
|
_priority = todo.priority;
|
||
|
|
_dueDate = todo.dueDate;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_titleController.dispose();
|
||
|
|
_descriptionController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _save() async {
|
||
|
|
if (!_formKey.currentState!.validate()) return;
|
||
|
|
|
||
|
|
setState(() => _isLoading = true);
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (isEditing) {
|
||
|
|
await ref.read(todoListProvider.notifier).updateTodo(
|
||
|
|
widget.todoId!,
|
||
|
|
UpdateTodoParams(
|
||
|
|
title: _titleController.text,
|
||
|
|
description: _descriptionController.text.isEmpty
|
||
|
|
? null
|
||
|
|
: _descriptionController.text,
|
||
|
|
priority: _priority,
|
||
|
|
dueDate: _dueDate,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
await ref.read(todoListProvider.notifier).createTodo(
|
||
|
|
CreateTodoParams(
|
||
|
|
title: _titleController.text,
|
||
|
|
description: _descriptionController.text.isEmpty
|
||
|
|
? null
|
||
|
|
: _descriptionController.text,
|
||
|
|
priority: _priority,
|
||
|
|
dueDate: _dueDate,
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (mounted) context.pop();
|
||
|
|
} catch (e) {
|
||
|
|
if (mounted) {
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
||
|
|
SnackBar(content: Text('Failed to save: $e')),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
if (mounted) setState(() => _isLoading = false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _pickDueDate() async {
|
||
|
|
final date = await showDatePicker(
|
||
|
|
context: context,
|
||
|
|
initialDate: _dueDate ?? DateTime.now().add(const Duration(days: 1)),
|
||
|
|
firstDate: DateTime.now(),
|
||
|
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||
|
|
);
|
||
|
|
if (date != null) {
|
||
|
|
setState(() => _dueDate = date);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
final theme = Theme.of(context);
|
||
|
|
|
||
|
|
return Scaffold(
|
||
|
|
appBar: AppBar(
|
||
|
|
title: Text(isEditing ? 'Edit Todo' : 'New Todo'),
|
||
|
|
),
|
||
|
|
body: Form(
|
||
|
|
key: _formKey,
|
||
|
|
child: ListView(
|
||
|
|
padding: const EdgeInsets.all(16),
|
||
|
|
children: [
|
||
|
|
TextFormField(
|
||
|
|
controller: _titleController,
|
||
|
|
decoration: const InputDecoration(
|
||
|
|
labelText: 'Title',
|
||
|
|
border: OutlineInputBorder(),
|
||
|
|
),
|
||
|
|
validator: (value) {
|
||
|
|
if (value == null || value.trim().isEmpty) {
|
||
|
|
return 'Title is required';
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
},
|
||
|
|
autofocus: !isEditing,
|
||
|
|
),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
TextFormField(
|
||
|
|
controller: _descriptionController,
|
||
|
|
decoration: const InputDecoration(
|
||
|
|
labelText: 'Description (optional)',
|
||
|
|
border: OutlineInputBorder(),
|
||
|
|
),
|
||
|
|
maxLines: 3,
|
||
|
|
),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
DropdownButtonFormField<String>(
|
||
|
|
initialValue: _priority,
|
||
|
|
decoration: const InputDecoration(
|
||
|
|
labelText: 'Priority',
|
||
|
|
border: OutlineInputBorder(),
|
||
|
|
),
|
||
|
|
items: const [
|
||
|
|
DropdownMenuItem(value: 'low', child: Text('Low')),
|
||
|
|
DropdownMenuItem(value: 'medium', child: Text('Medium')),
|
||
|
|
DropdownMenuItem(value: 'high', child: Text('High')),
|
||
|
|
],
|
||
|
|
onChanged: (value) {
|
||
|
|
if (value != null) setState(() => _priority = value);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
InkWell(
|
||
|
|
onTap: _pickDueDate,
|
||
|
|
child: InputDecorator(
|
||
|
|
decoration: const InputDecoration(
|
||
|
|
labelText: 'Due Date (optional)',
|
||
|
|
border: OutlineInputBorder(),
|
||
|
|
suffixIcon: Icon(Icons.calendar_today),
|
||
|
|
),
|
||
|
|
child: Text(
|
||
|
|
_dueDate != null
|
||
|
|
? '${_dueDate!.year}-${_dueDate!.month.toString().padLeft(2, '0')}-${_dueDate!.day.toString().padLeft(2, '0')}'
|
||
|
|
: 'No date selected',
|
||
|
|
style: theme.textTheme.bodyLarge?.copyWith(
|
||
|
|
color: _dueDate != null ? null : theme.colorScheme.onSurfaceVariant,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
if (_dueDate != null)
|
||
|
|
TextButton(
|
||
|
|
onPressed: () => setState(() => _dueDate = null),
|
||
|
|
child: const Text('Clear date'),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 24),
|
||
|
|
FilledButton(
|
||
|
|
onPressed: _isLoading ? null : _save,
|
||
|
|
child: _isLoading
|
||
|
|
? const SizedBox(
|
||
|
|
height: 20,
|
||
|
|
width: 20,
|
||
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||
|
|
)
|
||
|
|
: Text(isEditing ? 'Save Changes' : 'Create Todo'),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|