Files
tasq/lib/screens/programmer_tasks/monitor/monitor_tab.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

208 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../providers/leave_provider.dart';
import '../../../providers/pass_slip_provider.dart';
import '../../../providers/profile_provider.dart';
import '../../../providers/programmer_day_data_provider.dart';
import '../../../utils/app_time.dart';
import '../../../utils/staff_presence.dart';
import '../../../widgets/app_status_summary.dart';
import '../../../theme/app_status_colors.dart';
import 'help_panel.dart';
import 'monitor_model.dart';
import 'programmer_monitor_card.dart';
/// Summary row + programmer grid + help panel for the Monitor tab.
class MonitorTab extends ConsumerWidget {
const MonitorTab({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Tick every 30 s to keep durations fresh.
final now = ref.watch(clockTickerProvider).valueOrNull ?? AppTime.now();
final todayKey = ref.watch(todayKeyProvider);
final profilesAsync = ref.watch(profilesProvider);
final dayDataAsync = ref.watch(programmerDayDataProvider(todayKey));
final runStates = ref.watch(programmerRunStatesProvider);
final leavesAsync = ref.watch(leavesProvider);
final passSlipsAsync = ref.watch(passSlipsProvider);
final profiles = profilesAsync.valueOrNull ?? [];
final programmerProfiles =
profiles.where((p) => p.role == 'programmer').toList();
final profileNames = <String, String>{
for (final p in profiles) p.id: p.fullName,
};
final leaves = leavesAsync.valueOrNull ?? [];
final passSlips = passSlipsAsync.valueOrNull ?? [];
final activeLeaves = activeLeaveByUser(leaves, now);
final activePassSlips = activePassSlipByUser(passSlips);
if (dayDataAsync.isLoading && !dayDataAsync.hasValue) {
return const Center(child: CircularProgressIndicator());
}
final dayData = dayDataAsync.valueOrNull;
List<ProgrammerMonitorEntry> entries = [];
if (dayData != null) {
entries = buildMonitorEntries(
programmers: programmerProfiles,
today: dayData,
runStates: runStates,
leaves: activeLeaves,
passSlips: activePassSlips,
sheetStatusByUser: const {},
projectNames: const {},
profileNames: profileNames,
now: now,
);
}
// Compute summary counts.
var working = 0;
var paused = 0;
var idle = 0;
var away = 0;
for (final e in entries) {
switch (e.state) {
case MonitorState.working:
working++;
case MonitorState.paused:
paused++;
case MonitorState.idle:
idle++;
case MonitorState.onLeave:
case MonitorState.passSlip:
away++;
}
}
final summaryItems = [
AppStatusSummaryItem(
label: 'Working',
count: working,
kind: StatusKind.success,
),
AppStatusSummaryItem(
label: 'Paused',
count: paused,
kind: StatusKind.warning,
),
AppStatusSummaryItem(
label: 'Idle',
count: idle,
kind: StatusKind.neutral,
),
AppStatusSummaryItem(
label: 'Away',
count: away,
kind: StatusKind.info,
),
];
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
// Help panel breakpoint: show side-by-side at >= 900.
final sideBySide = width >= 900;
final grid = _ProgrammerGrid(
entries: entries,
width: sideBySide ? width - 320 : width,
);
final body = sideBySide
? Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: grid),
const SizedBox(width: 12),
SizedBox(
width: 300,
child: const HelpPanel(),
),
],
)
: Column(
children: [
grid,
const SizedBox(height: 16),
SizedBox(
height: 340,
child: const HelpPanel(),
),
],
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AppStatusSummaryRow(
items: summaryItems,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: body,
),
),
],
);
},
);
}
}
class _ProgrammerGrid extends StatelessWidget {
const _ProgrammerGrid({
required this.entries,
required this.width,
});
final List<ProgrammerMonitorEntry> entries;
final double width;
@override
Widget build(BuildContext context) {
final crossAxisCount = width < 700
? 1
: width < 1100
? 2
: 3;
if (entries.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text('No programmers found.'),
),
);
}
return GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 10,
crossAxisSpacing: 10,
childAspectRatio: 2.4,
),
itemCount: entries.length,
itemBuilder: (context, i) {
final entry = entries[i];
return ProgrammerMonitorCard(
entry: entry,
onTap: () =>
context.go('/programmer-tasks/programmers/${entry.programmer.id}'),
);
},
);
}
}