29bcbcaa4d
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>
197 lines
5.6 KiB
Dart
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,
|
|
),
|
|
);
|
|
}
|