import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/exceptions.dart'; import '../../data/models/task.dart'; import '../../providers/api_client_provider.dart'; import '../../providers/tasks_provider.dart'; import '../../widgets/project_selector.dart'; class TaskEditScreen extends ConsumerStatefulWidget { final int? taskId; const TaskEditScreen({super.key, this.taskId}); @override ConsumerState createState() => _TaskEditScreenState(); } class _TaskEditScreenState extends ConsumerState { final _formKey = GlobalKey(); final _titleController = TextEditingController(); final _descController = TextEditingController(); TaskStatus _status = TaskStatus.todo; TaskPriority _priority = TaskPriority.medium; DateTime? _dueDate; int? _projectId; bool _saving = false; // Future is created once in initState so FutureBuilder never restarts it. late final Future _initFuture; @override void initState() { super.initState(); _initFuture = widget.taskId != null ? _loadExisting() : Future.value(); } @override void dispose() { _titleController.dispose(); _descController.dispose(); super.dispose(); } Future _loadExisting() async { final task = await ref.read(tasksRepositoryProvider).getOne(widget.taskId!); _titleController.text = task.title; _descController.text = task.description ?? ''; _status = task.status; _priority = task.priority; _dueDate = task.dueDate; _projectId = task.projectId; } Future _save() async { if (!_formKey.currentState!.validate()) return; setState(() => _saving = true); try { if (widget.taskId == null) { await ref.read(tasksProvider.notifier).create( title: _titleController.text.trim(), description: _descController.text.trim().isEmpty ? null : _descController.text.trim(), status: _status, priority: _priority, dueDate: _dueDate, projectId: _projectId, ); } else { await ref.read(tasksProvider.notifier).updateTask(widget.taskId!, { 'title': _titleController.text.trim(), 'body': _descController.text.trim().isEmpty ? null : _descController.text.trim(), 'status': _status.value, 'priority': _priority.value, 'due_date': _dueDate?.toIso8601String(), 'project_id': _projectId, }); } if (mounted) context.pop(); } on AppException catch (e) { if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(e.message))); } } finally { if (mounted) setState(() => _saving = false); } } Future _delete() async { final confirm = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Delete task?'), content: Text(_titleController.text), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext, false), child: const Text('Cancel')), TextButton( onPressed: () => Navigator.pop(dialogContext, true), child: const Text('Delete')), ], ), ); if (confirm == true) { await ref.read(tasksProvider.notifier).delete(widget.taskId!); if (mounted) context.pop(); } } Future _pickDate() async { final date = await showDatePicker( context: context, initialDate: _dueDate ?? DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime(2100), ); if (date != null) setState(() => _dueDate = date); } @override Widget build(BuildContext context) { return FutureBuilder( future: _initFuture, builder: (context, snapshot) { return Scaffold( appBar: AppBar( title: Text(widget.taskId == null ? 'New Task' : 'Edit Task'), actions: [ if (widget.taskId != null) IconButton( icon: const Icon(Icons.delete), onPressed: _delete, ), IconButton( icon: _saving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.check), onPressed: _saving ? null : _save, ), ], ), body: snapshot.connectionState == ConnectionState.waiting ? const Center(child: CircularProgressIndicator()) : Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 600), child: Form( key: _formKey, child: ListView( padding: const EdgeInsets.all(16), children: [ TextFormField( controller: _titleController, decoration: const InputDecoration( labelText: 'Title', border: OutlineInputBorder(), ), validator: (v) => (v == null || v.trim().isEmpty) ? 'Required' : null, ), const SizedBox(height: 16), TextFormField( controller: _descController, decoration: const InputDecoration( labelText: 'Description (optional)', border: OutlineInputBorder(), ), maxLines: 3, ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: _status, decoration: const InputDecoration( labelText: 'Status', border: OutlineInputBorder(), ), items: TaskStatus.values .map((s) => DropdownMenuItem( value: s, child: Text(s.label))) .toList(), onChanged: (v) => setState(() => _status = v!), ), const SizedBox(height: 16), DropdownButtonFormField( initialValue: _priority, decoration: const InputDecoration( labelText: 'Priority', border: OutlineInputBorder(), ), items: TaskPriority.values .map((p) => DropdownMenuItem( value: p, child: Text(p.label))) .toList(), onChanged: (v) => setState(() => _priority = v!), ), const SizedBox(height: 16), ListTile( contentPadding: EdgeInsets.zero, title: Text(_dueDate == null ? 'No due date' : 'Due: ${_dueDate!.toLocal().toString().substring(0, 10)}'), leading: const Icon(Icons.calendar_today), trailing: _dueDate != null ? IconButton( icon: const Icon(Icons.clear), onPressed: () => setState(() => _dueDate = null), ) : null, onTap: _pickDate, ), const SizedBox(height: 16), ProjectSelector( value: _projectId, onChanged: (id) => setState(() => _projectId = id), ), ], ), ), ), ), ); }, ); } }