Add Projects feature; sync Note/Task models with backend additions

## New: Projects
- Project model, API, repository, Riverpod provider
- ProjectListScreen: active/archived sections, create dialog, long-press status/delete
- ProjectSelector widget: DropdownButtonFormField for note + task editors
- Projects tab added to shell (bottom nav + navigation rail, 4th position)
- /projects route registered in GoRouter ShellRoute

## Updated: Note model
- Added tags: List<String>, projectId: int?, milestoneId: int?
- NotesApi.create/update pass tags and project_id
- NotesRepository and NotesNotifier signatures updated
- NoteEditScreen: chip-based tag input + ProjectSelector

## Updated: Task model
- Added projectId: int?, milestoneId: int?, parentId: int?
- TasksApi.create passes project_id; update payload includes project_id
- TasksRepository and TasksNotifier signatures updated
- TaskEditScreen: ProjectSelector added; project_id sent on save

## Provider fix
- ProjectsNotifier.update renamed to updateProject to avoid conflict
  with AsyncNotifier.update(FutureOr<State> Function(State)) base method

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 20:52:59 -05:00
parent f30aa8d273
commit fceae5529d
19 changed files with 821 additions and 21 deletions
+11 -1
View File
@@ -6,6 +6,7 @@ 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;
@@ -22,6 +23,7 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
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.
@@ -49,6 +51,7 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
_status = task.status;
_priority = task.priority;
_dueDate = task.dueDate;
_projectId = task.projectId;
}
Future<void> _save() async {
@@ -64,16 +67,18 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
status: _status,
priority: _priority,
dueDate: _dueDate,
projectId: _projectId,
);
} else {
await ref.read(tasksProvider.notifier).updateTask(widget.taskId!, {
'title': _titleController.text.trim(),
'description': _descController.text.trim().isEmpty
'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();
@@ -216,6 +221,11 @@ class _TaskEditScreenState extends ConsumerState<TaskEditScreen> {
: null,
onTap: _pickDate,
),
const SizedBox(height: 16),
ProjectSelector(
value: _projectId,
onChanged: (id) => setState(() => _projectId = id),
),
],
),
),