Files
tasq/lib/screens/programmer_tasks/dashboard/task_sections.dart
T
redz1029 29bcbcaa4d 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>
2026-09-27 21:52:13 +08:00

197 lines
5.6 KiB
Dart

import 'package:flutter/material.dart';
import '../../../models/profile.model.dart';
import '../../../models/programmer_task.model.dart';
import '../../../providers/programmer_day_data_provider.dart';
import '../../../widgets/app_section_header.dart';
import '../widgets/programmer_task_card.dart';
typedef TaskSectionGroups = ({
List<ProgrammerTask> running,
List<ProgrammerTask> paused,
List<ProgrammerTask> notStarted,
List<ProgrammerTask> completed,
List<ProgrammerTask> cancelled,
});
/// Groups [tasks] into dashboard buckets for [userId], using [runStates] to
/// distinguish running from paused from not-started. Only tasks assigned to
/// [userId] are included.
TaskSectionGroups groupTasksForDashboard(
List<ProgrammerTask> tasks,
Map<String, ProgrammerRunInfo> runStates,
String userId,
) {
final running = <ProgrammerTask>[];
final paused = <ProgrammerTask>[];
final notStarted = <ProgrammerTask>[];
final completed = <ProgrammerTask>[];
final cancelled = <ProgrammerTask>[];
for (final task in tasks) {
if (task.assigneeId != userId) continue;
final state = runStates[task.id]?.state;
if (state == ProgrammerRunState.running) {
running.add(task);
} else if (state == ProgrammerRunState.paused) {
paused.add(task);
} else if (state == ProgrammerRunState.notStarted) {
notStarted.add(task);
} else if (task.status == ProgrammerTaskStatus.completed) {
completed.add(task);
} else if (task.status == ProgrammerTaskStatus.cancelled) {
cancelled.add(task);
}
}
completed.sort(
(a, b) =>
(b.completedAt ?? DateTime(0)).compareTo(a.completedAt ?? DateTime(0)),
);
cancelled.sort(
(a, b) =>
(b.cancelledAt ?? DateTime(0)).compareTo(a.cancelledAt ?? DateTime(0)),
);
return (
running: running,
paused: paused,
notStarted: notStarted,
completed: completed,
cancelled: cancelled,
);
}
/// Renders the five task groups as collapsible / always-visible sections.
///
/// - In progress, Paused, Not started: always visible when non-empty.
/// - Completed, Cancelled: start collapsed; tap header to expand; shows first
/// 10 + "Show all (N)" when N > 10; tap "Show all" to see all.
/// - Empty sections produce no header.
class TaskSections extends StatefulWidget {
const TaskSections({
super.key,
required this.groups,
required this.profileById,
required this.readOnly,
});
final TaskSectionGroups groups;
final Map<String, Profile> profileById;
final bool readOnly;
@override
State<TaskSections> createState() => _TaskSectionsState();
}
class _TaskSectionsState extends State<TaskSections> {
bool _completedExpanded = false;
bool _completedShowAll = false;
bool _cancelledExpanded = false;
bool _cancelledShowAll = false;
@override
Widget build(BuildContext context) {
final g = widget.groups;
final isEmpty = g.running.isEmpty &&
g.paused.isEmpty &&
g.notStarted.isEmpty &&
g.completed.isEmpty &&
g.cancelled.isEmpty;
if (isEmpty) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: Text('No tasks assigned to you')),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
..._alwaysSection('In progress', g.running),
..._alwaysSection('Paused', g.paused),
..._alwaysSection('Not started', g.notStarted),
..._collapsibleSection(
'Completed',
g.completed,
_completedExpanded,
_completedShowAll,
(v) => setState(() => _completedExpanded = v),
(v) => setState(() => _completedShowAll = v),
),
..._collapsibleSection(
'Cancelled',
g.cancelled,
_cancelledExpanded,
_cancelledShowAll,
(v) => setState(() => _cancelledExpanded = v),
(v) => setState(() => _cancelledShowAll = v),
),
],
);
}
List<Widget> _alwaysSection(String name, List<ProgrammerTask> tasks) {
if (tasks.isEmpty) return const [];
return [
AppSectionHeader(name, trailing: Text('${tasks.length}')),
...tasks.map(_taskCard),
const SizedBox(height: 8),
];
}
List<Widget> _collapsibleSection(
String name,
List<ProgrammerTask> tasks,
bool expanded,
bool showAll,
void Function(bool) onToggle,
void Function(bool) onShowAll,
) {
if (tasks.isEmpty) return const [];
List<ProgrammerTask> visible;
if (!expanded) {
visible = const [];
} else if (showAll || tasks.length <= 10) {
visible = tasks;
} else {
visible = tasks.take(10).toList();
}
return [
GestureDetector(
onTap: () => onToggle(!expanded),
behavior: HitTestBehavior.opaque,
child: AppSectionHeader(name, trailing: Text('${tasks.length}')),
),
if (expanded) ...[
...visible.map(_taskCard),
if (tasks.length > 10 && !showAll)
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () => onShowAll(true),
child: Text('Show all (${tasks.length})'),
),
),
const SizedBox(height: 8),
],
];
}
Widget _taskCard(ProgrammerTask t) => Padding(
padding: const EdgeInsets.only(bottom: 10),
child: ProgrammerTaskCard(
task: t,
assignee: t.assigneeId != null
? widget.profileById[t.assigneeId!]
: null,
isMine: true,
readOnly: widget.readOnly,
),
);
}