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 '../../theme/app_status_colors.dart';
import '../../theme/m3_motion.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
@@ -197,7 +199,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
),
TasQColumn<Profile>(
header: 'Role',
cellBuilder: (context, profile) => Text(profile.role),
cellBuilder: (context, profile) => Text(_roleLabel(profile.role)),
),
TasQColumn<Profile>(
header: 'Offices',
@@ -245,7 +247,7 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 2),
Text('Role: ${profile.role}'),
Text('Role: ${_roleLabel(profile.role)}'),
Text('Offices: $officesAssigned'),
Text('Last active: ${_formatLastActiveLabel(lastActive)}'),
const SizedBox(height: 4),
@@ -386,7 +388,10 @@ class _UserManagementScreenState extends ConsumerState<UserManagementScreen> {
key: ValueKey('role_${_selectedUserId ?? 'none'}'),
initialValue: _selectedRole,
items: _roles
.map((role) => DropdownMenuItem(value: role, child: Text(role)))
.map(
(role) =>
DropdownMenuItem(value: role, child: Text(_roleLabel(role))),
)
.toList(),
onChanged: (value) => setDialogState(() => _selectedRole = value),
decoration: const InputDecoration(labelText: 'Role'),
@@ -717,6 +722,16 @@ String _formatLastActiveLabel(DateTime? 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 {
const _StatusBadge({required this.label});
@@ -724,14 +739,15 @@ class _StatusBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final isError = label.toLowerCase().contains('error');
final background = isError
? scheme.errorContainer
: scheme.secondaryContainer;
final foreground = isError
? scheme.onErrorContainer
: scheme.onSecondaryContainer;
final status = AppStatusColors.of(context);
final swatch = switch (label.toLowerCase()) {
'active' => status.success,
'locked' => status.danger,
final l when l.contains('error') => status.danger,
_ => status.neutral,
};
final background = swatch.container;
final foreground = swatch.onContainer;
return Badge(
backgroundColor: background,
+9 -55
View File
@@ -36,6 +36,7 @@ import 'work_log_tab.dart';
import '../../theme/m3_motion.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
import '../../widgets/app_section_header.dart';
import '../../utils/app_time.dart';
import '../../utils/device_security.dart';
import '../../utils/location_permission.dart';
@@ -885,11 +886,9 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
const SizedBox(height: 24),
// Today's schedule
Text(
const AppSectionHeader(
"Today's Schedule",
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
padding: EdgeInsets.zero,
),
const SizedBox(height: 8),
@@ -5704,24 +5703,9 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
// Pending slips for admin approval
if (isAdmin) ...[
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.tertiary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
const AppSectionHeader(
'Pending Approvals',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
padding: EdgeInsets.zero,
),
const SizedBox(height: 8),
for (int i = 0; i < pendingSlipList.length; i++)
@@ -5755,24 +5739,9 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
],
// History
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.outlineVariant,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
const AppSectionHeader(
'Pass Slip History',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
padding: EdgeInsets.zero,
),
const SizedBox(height: 8),
for (int i = 0; i < historySlipList.length; i++)
@@ -6069,24 +6038,9 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
children: [
// ── Pending Approvals (admin only) ──
if (isAdmin) ...[
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: colors.tertiary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
const AppSectionHeader(
'Pending Approvals',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
],
padding: EdgeInsets.zero,
),
const SizedBox(height: 8),
if (pendingApprovals.isEmpty)