Add milestone data layer and redesign Projects tab

Milestone data layer:
- lib/data/models/milestone.dart: Milestone model with progress fields
  (total, completed, pct) from backend progress endpoint
- lib/data/api/milestones_api.dart: CRUD client for
  /api/projects/:id/milestones
- lib/data/repositories/milestones_repository.dart: thin repository wrapper
- lib/providers/milestones_provider.dart: FutureProvider.family keyed by
  project ID — lazily fetched per project when expanded
- api_client_provider.dart: registers milestonesApiProvider and
  milestonesRepositoryProvider

Projects tab redesign (project_list_screen.dart):
- Each project is now an ExpansionTile showing open task count in subtitle
- Expanding a project fetches its milestones and groups tasks under milestone
  headers with inline progress bar (completed/total count)
- Tasks show status icon, due date with overdue highlighting, priority dot
- 'No milestone' section at bottom for unassigned tasks
- Tapping a task navigates to the task editor
- Long-press on project title opens archive/complete/delete bottom sheet
- 'New task' button at bottom of each expanded project section

Bump version to 26.03.02+1

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 21:33:45 -05:00
parent fceae5529d
commit 868cb0e49e
7 changed files with 508 additions and 34 deletions
@@ -0,0 +1,26 @@
import '../api/milestones_api.dart';
import '../models/milestone.dart';
class MilestonesRepository {
final MilestonesApi _api;
const MilestonesRepository(this._api);
Future<List<Milestone>> getAll(int projectId, {String? status}) =>
_api.getAll(projectId, status: status);
Future<Milestone> create(
int projectId, {
required String title,
String? description,
int orderIndex = 0,
}) =>
_api.create(projectId,
title: title, description: description, orderIndex: orderIndex);
Future<Milestone> update(
int projectId, int milestoneId, Map<String, dynamic> fields) =>
_api.update(projectId, milestoneId, fields);
Future<void> delete(int projectId, int milestoneId) =>
_api.delete(projectId, milestoneId);
}