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:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user