import 'package:flutter/material.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../core/constants.dart'; import '../../core/exceptions.dart'; import '../../core/theme.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; final int? initialProjectId; const TaskEditScreen({super.key, this.taskId, this.initialProjectId}); @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; List _subTasks = []; late final Future _initFuture; @override void initState() { super.initState(); _projectId = widget.initialProjectId; _initFuture = widget.taskId != null ? _loadExisting() : Future.value(); } @override void dispose() { _titleController.dispose(); _descController.dispose(); super.dispose(); } Future _loadExisting() async { final repo = ref.read(tasksRepositoryProvider); final task = await repo.getOne(widget.taskId!); _titleController.text = task.title; _descController.text = task.description ?? ''; _status = task.status; _priority = task.priority; _dueDate = task.dueDate; _projectId = task.projectId; // Fetch sub-tasks final api = ref.read(tasksApiProvider); final subs = await api.getSubTasks(widget.taskId!); setState(() => _subTasks = subs); } 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), style: TextButton.styleFrom( foregroundColor: Theme.of(dialogContext).extension()!.destructive, ), 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); } Future _addSubTask() async { final titleCtrl = TextEditingController(); final result = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Add sub-task'), content: TextField( controller: titleCtrl, autofocus: true, decoration: const InputDecoration( labelText: 'Title', border: OutlineInputBorder(), ), textInputAction: TextInputAction.done, onSubmitted: (v) => Navigator.pop(ctx, v.trim()), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Cancel')), FilledButton( onPressed: () => Navigator.pop(ctx, titleCtrl.text.trim()), child: const Text('Add')), ], ), ); titleCtrl.dispose(); if (result == null || result.isEmpty || !mounted) return; try { final api = ref.read(tasksApiProvider); final sub = await api.create( title: result, status: TaskStatus.todo, priority: TaskPriority.none, projectId: _projectId, parentId: widget.taskId, ); ref.invalidate(tasksProvider); setState(() => _subTasks = [..._subTasks, sub]); } on AppException catch (e) { if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(e.message))); } } } @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(LucideIcons.trash2), onPressed: _delete, ), IconButton( icon: _saving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(LucideIcons.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( // ignore: deprecated_member_use value: _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( // ignore: deprecated_member_use value: _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(LucideIcons.calendarDays), trailing: _dueDate != null ? IconButton( icon: const Icon(LucideIcons.x), onPressed: () => setState(() => _dueDate = null), ) : null, onTap: _pickDate, ), const SizedBox(height: 16), ProjectSelector( value: _projectId, onChanged: (id) => setState(() => _projectId = id), ), // Sub-tasks (only when editing an existing task) if (widget.taskId != null) ...[ const SizedBox(height: 24), _SubTasksSection( subTasks: _subTasks, onAdd: _addSubTask, ), ], ], ), ), ), ), ); }, ); } } // ─── Sub-tasks section ───────────────────────────────────────────────────────── class _SubTasksSection extends ConsumerWidget { final List subTasks; final VoidCallback onAdd; const _SubTasksSection({required this.subTasks, required this.onAdd}); @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Sub-tasks', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const Spacer(), TextButton.icon( onPressed: onAdd, icon: const Icon(LucideIcons.plus, size: 16), label: const Text('Add'), style: TextButton.styleFrom( foregroundColor: colorScheme.primary, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), textStyle: const TextStyle(fontSize: 13), ), ), ], ), if (subTasks.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( 'No sub-tasks yet.', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.outline, ), ), ) else ...subTasks.map((sub) => _SubTaskTile(task: sub, ref: ref)), ], ); } } class _SubTaskTile extends StatelessWidget { final Task task; final WidgetRef ref; const _SubTaskTile({required this.task, required this.ref}); @override Widget build(BuildContext context) { final isDone = task.status == TaskStatus.done; return ListTile( dense: true, contentPadding: EdgeInsets.zero, leading: Checkbox( value: isDone, onChanged: (_) async { final newStatus = isDone ? TaskStatus.todo : TaskStatus.done; await ref.read(tasksProvider.notifier).updateTask( task.id, {'status': newStatus.value}); }, ), title: Text( task.title, style: TextStyle( decoration: isDone ? TextDecoration.lineThrough : null, color: isDone ? Theme.of(context).colorScheme.outline : null, ), ), onTap: () => context.push( Routes.taskEdit.replaceFirst(':id', '${task.id}'), ), ); } }