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:
@@ -0,0 +1,156 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../models/programmer_task.model.dart';
|
||||
import '../../../providers/programmer_day_data_provider.dart';
|
||||
import '../../../providers/programmer_tasks_provider.dart';
|
||||
import '../../../utils/programmer_task_ledger.dart';
|
||||
import '../../../widgets/m3_card.dart';
|
||||
import '../../../widgets/tech_chip.dart';
|
||||
import '../programmer_task_actions.dart';
|
||||
|
||||
/// Hero card at the top of the programmer dashboard showing the currently
|
||||
/// running task (or an idle state when nothing is running).
|
||||
class NowWorkingCard extends ConsumerWidget {
|
||||
const NowWorkingCard({
|
||||
super.key,
|
||||
required this.running,
|
||||
required this.nextUp,
|
||||
required this.todayOnTask,
|
||||
required this.totalOnTask,
|
||||
required this.readOnly,
|
||||
});
|
||||
|
||||
/// The currently running task, or null when nothing is running.
|
||||
final ProgrammerTask? running;
|
||||
|
||||
/// First queued (not-started) task, shown as a start target when idle.
|
||||
final ProgrammerTask? nextUp;
|
||||
|
||||
/// How long the user has worked on [running] today.
|
||||
final Duration todayOnTask;
|
||||
|
||||
/// Total worked time on [running] across all days.
|
||||
final Duration totalOnTask;
|
||||
|
||||
final bool readOnly;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Subscribe to the clock so durations re-render every 30 s.
|
||||
ref.watch(clockTickerProvider);
|
||||
|
||||
return M3Card.elevated(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: running != null
|
||||
? _RunningView(
|
||||
task: running!,
|
||||
todayOnTask: todayOnTask,
|
||||
totalOnTask: totalOnTask,
|
||||
readOnly: readOnly,
|
||||
)
|
||||
: _IdleView(nextUp: nextUp, readOnly: readOnly),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RunningView extends ConsumerWidget {
|
||||
const _RunningView({
|
||||
required this.task,
|
||||
required this.todayOnTask,
|
||||
required this.totalOnTask,
|
||||
required this.readOnly,
|
||||
});
|
||||
|
||||
final ProgrammerTask task;
|
||||
final Duration todayOnTask;
|
||||
final Duration totalOnTask;
|
||||
final bool readOnly;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tt = Theme.of(context).textTheme;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
task.title,
|
||||
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
TechChip(task.category, icon: Icons.category_outlined, dense: true),
|
||||
Text(
|
||||
'Today ${formatDurationHm(todayOnTask)} · '
|
||||
'Total ${formatDurationHm(totalOnTask)}',
|
||||
style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!readOnly) ...[
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => ref
|
||||
.read(programmerTasksControllerProvider)
|
||||
.pauseTask(taskId: task.id),
|
||||
icon: const Icon(Icons.pause_rounded, size: 18),
|
||||
label: const Text('Pause'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: () =>
|
||||
completeProgrammerTaskWithConfirm(context, ref, task),
|
||||
icon: const Icon(Icons.check_rounded, size: 18),
|
||||
label: const Text('Complete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IdleView extends ConsumerWidget {
|
||||
const _IdleView({required this.nextUp, required this.readOnly});
|
||||
|
||||
final ProgrammerTask? nextUp;
|
||||
final bool readOnly;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tt = Theme.of(context).textTheme;
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Nothing running',
|
||||
style: tt.bodyLarge?.copyWith(color: cs.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
if (nextUp != null && !readOnly)
|
||||
FilledButton.icon(
|
||||
onPressed: () =>
|
||||
startProgrammerTaskWithPausePrompt(context, ref, nextUp!),
|
||||
icon: const Icon(Icons.play_arrow_rounded, size: 18),
|
||||
label: const Text('Start'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user