feat(programmer): Mine dashboard hero + task sections

Replace the Mine tab flat list with ProgrammerDashboard — a NowWorkingCard
hero (running task with Pause/Complete, or idle with nextUp Start), a
TODO stub for Task 8 stat tiles/badges, and collapsible TaskSections
(In progress, Paused, Not started always-visible; Completed/Cancelled
start collapsed with Show-all at 10+). groupTasksForDashboard excludes
other assignees and sorts completed/cancelled by terminal date desc.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-27 21:52:13 +08:00
parent f8c3422715
commit 29bcbcaa4d
5 changed files with 679 additions and 12 deletions
@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tasq/models/profile.model.dart';
import 'package:tasq/models/programmer_task.model.dart';
import 'package:tasq/providers/programmer_day_data_provider.dart';
import 'package:tasq/screens/programmer_tasks/dashboard/task_sections.dart';
import 'package:tasq/utils/app_time.dart';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
ProgrammerTask _makeTask({
required String id,
required String status,
String assigneeId = 'user1',
DateTime? completedAt,
DateTime? cancelledAt,
}) {
final now = DateTime(2026, 1, 1);
return ProgrammerTask(
id: id,
title: 'Task $id',
category: ProgrammerTaskCategory.bugFix,
status: status,
assigneeId: assigneeId,
createdAt: now,
updatedAt: now,
completedAt: completedAt,
cancelledAt: cancelledAt,
);
}
Widget _buildSections(TaskSectionGroups groups, {bool readOnly = false}) {
return ProviderScope(
overrides: [
programmerRunStatesProvider.overrideWithValue({}),
],
child: MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: TaskSections(
groups: groups,
profileById: const {},
readOnly: readOnly,
),
),
),
),
);
}
// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------
void main() {
setUpAll(AppTime.initialize);
test('groupTasksForDashboard splits by run state and excludes other assignees', () {
final tasks = [
_makeTask(id: 't1', status: ProgrammerTaskStatus.inProgress, assigneeId: 'user1'),
_makeTask(id: 't2', status: ProgrammerTaskStatus.inProgress, assigneeId: 'user1'),
_makeTask(id: 't3', status: ProgrammerTaskStatus.queued, assigneeId: 'user1'),
_makeTask(id: 't4', status: ProgrammerTaskStatus.queued, assigneeId: 'other'),
];
final runStates = <String, ProgrammerRunInfo>{
't1': (state: ProgrammerRunState.running, since: null),
't2': (state: ProgrammerRunState.paused, since: null),
't3': (state: ProgrammerRunState.notStarted, since: null),
't4': (state: ProgrammerRunState.notStarted, since: null),
};
final groups = groupTasksForDashboard(tasks, runStates, 'user1');
expect(groups.running.map((t) => t.id), ['t1']);
expect(groups.paused.map((t) => t.id), ['t2']);
expect(groups.notStarted.map((t) => t.id), ['t3']);
// t4 belongs to 'other' — must not appear
expect(groups.running.any((t) => t.id == 't4'), isFalse);
expect(groups.paused.any((t) => t.id == 't4'), isFalse);
expect(groups.notStarted.any((t) => t.id == 't4'), isFalse);
});
test('completed sorted by completedAt desc, cancelled by cancelledAt desc', () {
final base = DateTime(2026, 1, 1);
final tasks = [
_makeTask(
id: 'c1',
status: ProgrammerTaskStatus.completed,
completedAt: base.add(const Duration(hours: 1)),
),
_makeTask(
id: 'c2',
status: ProgrammerTaskStatus.completed,
completedAt: base.add(const Duration(hours: 3)),
),
_makeTask(
id: 'x1',
status: ProgrammerTaskStatus.cancelled,
cancelledAt: base.add(const Duration(hours: 2)),
),
_makeTask(
id: 'x2',
status: ProgrammerTaskStatus.cancelled,
cancelledAt: base.add(const Duration(hours: 5)),
),
];
final runStates = <String, ProgrammerRunInfo>{
'c1': (state: ProgrammerRunState.completed, since: null),
'c2': (state: ProgrammerRunState.completed, since: null),
'x1': (state: ProgrammerRunState.cancelled, since: null),
'x2': (state: ProgrammerRunState.cancelled, since: null),
};
final groups = groupTasksForDashboard(tasks, runStates, 'user1');
expect(groups.completed.map((t) => t.id).toList(), ['c2', 'c1']);
expect(groups.cancelled.map((t) => t.id).toList(), ['x2', 'x1']);
});
// ---------------------------------------------------------------------------
// Widget tests
// ---------------------------------------------------------------------------
testWidgets('Completed and Cancelled start collapsed', (tester) async {
final base = DateTime(2026, 1, 1);
final completed = List.generate(
12,
(i) => _makeTask(
id: 'c$i',
status: ProgrammerTaskStatus.completed,
completedAt: base.add(Duration(hours: i)),
),
);
final runStates = <String, ProgrammerRunInfo>{
for (final t in completed)
t.id: (state: ProgrammerRunState.completed, since: null),
};
final groups = groupTasksForDashboard(completed, runStates, 'user1');
await tester.pumpWidget(_buildSections(groups));
await tester.pump();
// Header is visible
expect(find.textContaining('COMPLETED'), findsOneWidget);
// First task title is NOT visible (collapsed)
expect(find.text('Task c0'), findsNothing);
// Tap the header to expand
await tester.tap(find.textContaining('COMPLETED'));
await tester.pump();
// 10 titles visible (first 10 of 12)
int visibleCount = 0;
for (int i = 0; i < 12; i++) {
if (tester.any(find.text('Task c${11 - i}'))) visibleCount++;
}
expect(visibleCount, 10);
// "Show all (12)" link is visible
expect(find.text('Show all (12)'), findsOneWidget);
});
testWidgets('empty sections are hidden', (tester) async {
final tasks = [
_makeTask(id: 'r1', status: ProgrammerTaskStatus.inProgress),
];
final runStates = <String, ProgrammerRunInfo>{
'r1': (state: ProgrammerRunState.running, since: null),
};
final groups = groupTasksForDashboard(tasks, runStates, 'user1');
await tester.pumpWidget(_buildSections(groups));
await tester.pump();
// 'In progress' header present (at least one widget contains the text)
expect(find.textContaining('IN PROGRESS'), findsAtLeastNWidgets(1));
// 'Cancelled' header not rendered (no cancelled tasks)
expect(find.textContaining('CANCELLED'), findsNothing);
// 'Paused' header not rendered
expect(find.textContaining('PAUSED'), findsNothing);
});
}