Files
tasq/lib/screens/programmer_tasks/monitor/help_panel.dart
T
redz1029 7beb8e8713 feat(programmer): admin monitor tab + drill-down
- Add isStrictAdminProvider (role == 'admin' only)
- Add programmer_tasks_tabs.dart with ProgrammerTasksTab enum,
  programmerTasksTabsFor, parseProgrammerTasksTab, programmerTasksTabLabel
- Make ProgrammerTasksListScreen role-aware via initialTab param and
  programmerTasksTabsFor; admin sees Monitor/Mine/All tabs
- Add MonitorTab with AppStatusSummaryRow + responsive programmer grid
  (1/2/3 cols) + HelpPanel side/bottom
- Add ProgrammerMonitorCard with state pills (Working/Paused/Idle/On leave/Pass slip)
- Add HelpPanel ConsumerStatefulWidget with day stepper and help-edge rows
- Add ProgrammerDrillDownScreen (/programmer-tasks/programmers/:userId);
  non-admins see lock empty view
- Router: pass tab query param; add programmers/:userId before :id route
- Tests: 7/7 pass; analyze: 0 new issues

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-09-27 22:39:19 +08:00

160 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../providers/programmer_day_data_provider.dart';
import '../../../providers/programmer_tasks_provider.dart';
import '../../../widgets/app_section_header.dart';
import 'monitor_model.dart';
final _dateFmt = DateFormat('EEE, MMM d');
/// Side panel that shows "who's helping whom" for a selected day, with a
/// ← / → stepper that cannot advance past today.
///
/// Uses a [ValueNotifier<DateTime>] initialized to [todayKeyProvider] so the
/// displayed day is independent of the parent; the family provider is
/// re-watched whenever the day changes.
class HelpPanel extends ConsumerStatefulWidget {
const HelpPanel({super.key});
@override
ConsumerState<HelpPanel> createState() => _HelpPanelState();
}
class _HelpPanelState extends ConsumerState<HelpPanel> {
late final ValueNotifier<DateTime> _day;
@override
void initState() {
super.initState();
final today = ref.read(todayKeyProvider);
_day = ValueNotifier(today);
}
@override
void dispose() {
_day.dispose();
super.dispose();
}
void _stepDay(int delta) {
final today = ref.read(todayKeyProvider);
final next = _day.value.add(Duration(days: delta));
if (delta > 0 && !next.isBefore(today) && next != today) return;
if (delta > 0 && next.isAfter(today)) return;
setState(() {
_day.value = next;
});
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
final today = ref.watch(todayKeyProvider);
final tasksAsync = ref.watch(programmerTasksProvider);
final dayDataAsync = ref.watch(programmerDayDataProvider(_day.value));
final isAtToday = !_day.value.isBefore(today);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const AppSectionHeader("Who's helping whom"),
// Day stepper
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.chevron_left),
onPressed: () => _stepDay(-1),
tooltip: 'Previous day',
),
Text(
_dateFmt.format(_day.value),
style: tt.labelMedium?.copyWith(color: cs.onSurface),
),
IconButton(
icon: Icon(
Icons.chevron_right,
color: isAtToday ? cs.onSurface.withValues(alpha: 0.3) : null,
),
onPressed: isAtToday ? null : () => _stepDay(1),
tooltip: 'Next day',
),
],
),
const SizedBox(height: 8),
// Help edges
Expanded(
child: Builder(
builder: (context) {
if (dayDataAsync.isLoading && !dayDataAsync.hasValue) {
return const Center(child: CircularProgressIndicator());
}
final dayData = dayDataAsync.valueOrNull;
if (dayData == null) {
return _emptyText(
'No data for this day.',
cs: cs,
tt: tt,
);
}
final tasks = tasksAsync.valueOrNull ?? [];
final taskById = {for (final t in tasks) t.id: t};
final edges = buildHelpEdges(
workLogs: dayData.workLogs,
taskById: taskById,
day: _day.value,
);
if (edges.isEmpty) {
return _emptyText(
'No help logged this day.',
cs: cs,
tt: tt,
);
}
return ListView.separated(
itemCount: edges.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, i) {
final e = edges[i];
final helperName = e.helperId;
final assigneeName = e.assigneeId;
final timeStr = TimeOfDay.fromDateTime(e.at).format(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Text(
'$helperName → $assigneeName · ${e.taskTitle} · ${e.minutes}m · $timeStr',
style: tt.bodySmall?.copyWith(color: cs.onSurface),
),
);
},
);
},
),
),
],
);
}
Widget _emptyText(
String msg, {
required ColorScheme cs,
required TextTheme tt,
}) {
return Center(
child: Text(
msg,
style: tt.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
);
}
}