UI Phase 2: dogfood follow-ups — section headers, role terms, status semantics

Address the remaining concrete items flagged by the dogfood pass.

Section headers (→ AppSectionHeader, azure accent bar + uppercase):
- Attendance: "Today's Schedule", "Pass Slip History", and two "Pending
  Approvals" rows (were plain bold text / a manual tertiary accent bar).

User Management:
- Roles now display human-friendly labels (it_staff → "IT Staff", standard →
  "Standard", …) in the table cell, mobile tile and edit dropdown, instead of
  raw snake_case (Content/terminology consistency).
- _StatusBadge is now semantic via AppStatusColors: Active → success (green),
  Locked → danger (red), else neutral — previously everything was teal, so a
  Locked account read the same as an Active one.

Verified: flutter analyze lib clean; flutter build web green; visual QA
(Attendance, User Management) in dark.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-26 16:44:39 +08:00
parent 6bde8630d8
commit 4685430406
2 changed files with 39 additions and 69 deletions
+27 -11
View File
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../theme/app_status_colors.dart';
import '../../theme/m3_motion.dart'; import '../../theme/m3_motion.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -197,7 +199,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
), ),
TasQColumn<Profile>( TasQColumn<Profile>(
header: 'Role', header: 'Role',
cellBuilder: (context, profile) => Text(profile.role), cellBuilder: (context, profile) => Text(_roleLabel(profile.role)),
), ),
TasQColumn<Profile>( TasQColumn<Profile>(
header: 'Offices', header: 'Offices',
@@ -245,7 +247,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 2), const SizedBox(height: 2),
Text('Role: ${profile.role}'), Text('Role: ${_roleLabel(profile.role)}'),
Text('Offices: $officesAssigned'), Text('Offices: $officesAssigned'),
Text('Last active: ${_formatLastActiveLabel(lastActive)}'), Text('Last active: ${_formatLastActiveLabel(lastActive)}'),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -386,7 +388,10 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
key: ValueKey('role_${_selectedUserId ?? 'none'}'), key: ValueKey('role_${_selectedUserId ?? 'none'}'),
initialValue: _selectedRole, initialValue: _selectedRole,
items: _roles items: _roles
.map((role) => DropdownMenuItem(value: role, child: Text(role))) .map(
(role) =>
DropdownMenuItem(value: role, child: Text(_roleLabel(role))),
)
.toList(), .toList(),
onChanged: (value) => setDialogState(() => _selectedRole = value), onChanged: (value) => setDialogState(() => _selectedRole = value),
decoration: const InputDecoration(labelText: 'Role'), decoration: const InputDecoration(labelText: 'Role'),
@@ -717,6 +722,16 @@ String _formatLastActiveLabel(DateTime? value) {
return DateFormat('yyyy-MM-dd').format(value); return DateFormat('yyyy-MM-dd').format(value);
} }
/// Human-friendly display label for a stored role value.
String _roleLabel(String role) => switch (role) {
'it_staff' => 'IT Staff',
'standard' => 'Standard',
'dispatcher' => 'Dispatcher',
'programmer' => 'Programmer',
'admin' => 'Admin',
_ => role.replaceAll('_', ' '),
};
class _StatusBadge extends StatelessWidget { class _StatusBadge extends StatelessWidget {
const _StatusBadge({required this.label}); const _StatusBadge({required this.label});
@@ -724,14 +739,15 @@ class _StatusBadge extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme; final status = AppStatusColors.of(context);
final isError = label.toLowerCase().contains('error'); final swatch = switch (label.toLowerCase()) {
final background = isError 'active' => status.success,
? scheme.errorContainer 'locked' => status.danger,
: scheme.secondaryContainer; final l when l.contains('error') => status.danger,
final foreground = isError _ => status.neutral,
? scheme.onErrorContainer };
: scheme.onSecondaryContainer; final background = swatch.container;
final foreground = swatch.onContainer;
return Badge( return Badge(
backgroundColor: background, backgroundColor: background,
+12 -58
View File
@@ -36,6 +36,7 @@ import 'work_log_tab.dart';
import '../../theme/m3_motion.dart'; import '../../theme/m3_motion.dart';
import '../../theme/app_surfaces.dart'; import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart'; import '../../theme/app_status_colors.dart';
import '../../widgets/app_section_header.dart';
import '../../utils/app_time.dart'; import '../../utils/app_time.dart';
import '../../utils/device_security.dart'; import '../../utils/device_security.dart';
import '../../utils/location_permission.dart'; import '../../utils/location_permission.dart';
@@ -885,11 +886,9 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Today's schedule // Today's schedule
Text( const AppSectionHeader(
"Today's Schedule", "Today's Schedule",
style: theme.textTheme.titleMedium?.copyWith( padding: EdgeInsets.zero,
fontWeight: FontWeight.w600,
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -5704,24 +5703,9 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
// Pending slips for admin approval // Pending slips for admin approval
if (isAdmin) ...[ if (isAdmin) ...[
Row( const AppSectionHeader(
children: [ 'Pending Approvals',
Container( padding: EdgeInsets.zero,
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.tertiary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
'Pending Approvals',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
for (int i = 0; i < pendingSlipList.length; i++) for (int i = 0; i < pendingSlipList.length; i++)
@@ -5755,24 +5739,9 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
], ],
// History // History
Row( const AppSectionHeader(
children: [ 'Pass Slip History',
Container( padding: EdgeInsets.zero,
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
'Pass Slip History',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
for (int i = 0; i < historySlipList.length; i++) for (int i = 0; i < historySlipList.length; i++)
@@ -6069,24 +6038,9 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
children: [ children: [
// ── Pending Approvals (admin only) ── // ── Pending Approvals (admin only) ──
if (isAdmin) ...[ if (isAdmin) ...[
Row( const AppSectionHeader(
children: [ 'Pending Approvals',
Container( padding: EdgeInsets.zero,
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.tertiary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
'Pending Approvals',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (pendingApprovals.isEmpty) if (pendingApprovals.isEmpty)