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
+10
View File
@@ -6,11 +6,13 @@ import '../data/api/api_client.dart';
import '../data/api/auth_api.dart';
import '../data/api/chat_api.dart';
import '../data/api/notes_api.dart';
import '../data/api/projects_api.dart';
import '../data/api/quick_capture_api.dart';
import '../data/api/tasks_api.dart';
import '../data/repositories/auth_repository.dart';
import '../data/repositories/chat_repository.dart';
import '../data/repositories/notes_repository.dart';
import '../data/repositories/projects_repository.dart';
import '../data/repositories/tasks_repository.dart';
import 'settings_provider.dart';
@@ -45,6 +47,10 @@ final quickCaptureApiProvider = Provider<QuickCaptureApi>((ref) {
return QuickCaptureApi(ref.watch(dioProvider));
});
final projectsApiProvider = Provider<ProjectsApi>((ref) {
return ProjectsApi(ref.watch(dioProvider));
});
final authRepositoryProvider = Provider<AuthRepository>((ref) {
return AuthRepository(ref.watch(authApiProvider));
});
@@ -60,3 +66,7 @@ final tasksRepositoryProvider = Provider<TasksRepository>((ref) {
final chatRepositoryProvider = Provider<ChatRepository>((ref) {
return ChatRepository(ref.watch(chatApiProvider));
});
final projectsRepositoryProvider = Provider<ProjectsRepository>((ref) {
return ProjectsRepository(ref.watch(projectsApiProvider));
});
+25 -6
View File
@@ -12,16 +12,35 @@ class NotesNotifier extends AsyncNotifier<List<Note>> {
return ref.watch(notesRepositoryProvider).getAll();
}
Future<Note> create(String title, String body) async {
final note =
await ref.read(notesRepositoryProvider).create(title, body);
Future<Note> create(
String title,
String body, {
List<String> tags = const [],
int? projectId,
}) async {
final note = await ref
.read(notesRepositoryProvider)
.create(title, body, tags: tags, projectId: projectId);
state = AsyncData([...state.valueOrNull ?? [], note]);
return note;
}
Future<Note> updateNote(int id, String title, String body) async {
final updated =
await ref.read(notesRepositoryProvider).update(id, title, body);
Future<Note> updateNote(
int id,
String title,
String body, {
List<String> tags = const [],
int? projectId,
bool clearProject = false,
}) async {
final updated = await ref.read(notesRepositoryProvider).update(
id,
title,
body,
tags: tags,
projectId: projectId,
clearProject: clearProject,
);
state = AsyncData([
for (final n in state.valueOrNull ?? [])
if (n.id == id) updated else n,
+49
View File
@@ -0,0 +1,49 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../data/models/project.dart';
import 'api_client_provider.dart';
final projectsProvider =
AsyncNotifierProvider<ProjectsNotifier, List<Project>>(
ProjectsNotifier.new);
class ProjectsNotifier extends AsyncNotifier<List<Project>> {
@override
Future<List<Project>> build() async {
return ref.watch(projectsRepositoryProvider).getAll();
}
Future<Project> create({
required String title,
String? description,
String? goal,
String? color,
}) async {
final project = await ref.read(projectsRepositoryProvider).create(
title: title,
description: description,
goal: goal,
color: color,
);
state = AsyncData([...state.valueOrNull ?? [], project]);
return project;
}
Future<Project> updateProject(int id, Map<String, dynamic> fields) async {
final updated =
await ref.read(projectsRepositoryProvider).update(id, fields);
state = AsyncData([
for (final p in state.valueOrNull ?? [])
if (p.id == id) updated else p,
]);
return updated;
}
Future<void> delete(int id) async {
await ref.read(projectsRepositoryProvider).delete(id);
state = AsyncData([
for (final p in state.valueOrNull ?? [])
if (p.id != id) p,
]);
}
}
+2
View File
@@ -18,6 +18,7 @@ class TasksNotifier extends AsyncNotifier<List<Task>> {
required TaskStatus status,
required TaskPriority priority,
DateTime? dueDate,
int? projectId,
}) async {
final task = await ref.read(tasksRepositoryProvider).create(
title: title,
@@ -25,6 +26,7 @@ class TasksNotifier extends AsyncNotifier<List<Task>> {
status: status,
priority: priority,
dueDate: dueDate,
projectId: projectId,
);
state = AsyncData([...state.valueOrNull ?? [], task]);
return task;