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:
@@ -27,11 +27,18 @@ class NotesApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Note> create(String title, String body) async {
|
||||
Future<Note> create(
|
||||
String title,
|
||||
String body, {
|
||||
List<String> tags = const [],
|
||||
int? projectId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post('/api/notes', data: {
|
||||
'title': title,
|
||||
'body': body,
|
||||
'tags': tags,
|
||||
if (projectId != null) 'project_id': projectId,
|
||||
});
|
||||
return Note.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
@@ -39,11 +46,20 @@ class NotesApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Note> update(int id, String title, String body) async {
|
||||
Future<Note> update(
|
||||
int id,
|
||||
String title,
|
||||
String body, {
|
||||
List<String> tags = const [],
|
||||
int? projectId,
|
||||
bool clearProject = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.put('/api/notes/$id', data: {
|
||||
'title': title,
|
||||
'body': body,
|
||||
'tags': tags,
|
||||
if (clearProject) 'project_id': null else if (projectId != null) 'project_id': projectId,
|
||||
});
|
||||
return Note.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import '../models/project.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
class ProjectsApi {
|
||||
final Dio _dio;
|
||||
const ProjectsApi(this._dio);
|
||||
|
||||
Future<List<Project>> getAll({String? status}) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'/api/projects',
|
||||
queryParameters: status != null ? {'status': status} : null,
|
||||
);
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
final list = data['projects'] as List<dynamic>;
|
||||
return list
|
||||
.map((e) => Project.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
} on DioException catch (e) {
|
||||
throw dioToApp(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Project> getOne(int id) async {
|
||||
try {
|
||||
final response = await _dio.get('/api/projects/$id');
|
||||
return Project.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw dioToApp(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Project> create({
|
||||
required String title,
|
||||
String? description,
|
||||
String? goal,
|
||||
String? color,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post('/api/projects', data: {
|
||||
'title': title,
|
||||
if (description != null && description.isNotEmpty)
|
||||
'description': description,
|
||||
if (goal != null && goal.isNotEmpty) 'goal': goal,
|
||||
if (color != null) 'color': color,
|
||||
});
|
||||
return Project.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw dioToApp(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Project> update(int id, Map<String, dynamic> fields) async {
|
||||
try {
|
||||
final response = await _dio.patch('/api/projects/$id', data: fields);
|
||||
return Project.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw dioToApp(e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> delete(int id) async {
|
||||
try {
|
||||
await _dio.delete('/api/projects/$id');
|
||||
} on DioException catch (e) {
|
||||
throw dioToApp(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ class TasksApi {
|
||||
required TaskStatus status,
|
||||
required TaskPriority priority,
|
||||
DateTime? dueDate,
|
||||
int? projectId,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post('/api/tasks', data: {
|
||||
@@ -41,6 +42,7 @@ class TasksApi {
|
||||
'status': status.value,
|
||||
'priority': priority.value,
|
||||
'due_date': dueDate?.toIso8601String(),
|
||||
if (projectId != null) 'project_id': projectId,
|
||||
});
|
||||
return Task.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user