UI: Cyber/IT design overhaul with semantic status tokens (Material 3)

Re-theme TasQ with a refined enterprise "Cyber/IT" identity on Material 3.
Functionality is unchanged — only color/shape tokens and a few state widgets.

Foundation:
- Hand-tuned light + dark ColorSchemes (deep-slate surfaces, azure/teal/violet
  accents), tightened 12-16dp shape language, flat hairline-outlined cards.
- New AppStatusColors theme extension (success/warning/danger/info/neutral,
  per-brightness) replacing scattered raw Colors.green/orange/red/grey so status
  color is consistent and legible in dark mode.
- New TechChip mono primitive; StatusPill.kind; M3Card hairline variants;
  tightened AppSurfaces radii.

Per-screen:
- Tokenized status colors + radii across dashboard, tickets, tasks, IT service
  requests, attendance, whereabouts, network map, reports charts, auth, and
  shared/admin screens. Added _ticketStatusKind/_taskStatusKind/_isrStatusKind
  mappers so status carries meaning consistently.
- Fixed white pie-slice borders for dark mode; dropped ad-hoc dialog radius
  overrides in favor of the theme default. Categorical palettes (leave-type
  icons, team color picker, distribution charts) intentionally preserved.

Verified: flutter analyze lib clean; flutter build web green; visual QA in
light and dark (dashboard, tickets, attendance).

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-09-26 15:13:47 +08:00
parent 3cb980b629
commit 57377b1b21
28 changed files with 986 additions and 470 deletions
+30 -19
View File
@@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import '../../theme/app_status_colors.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:geolocator/geolocator.dart';
import 'package:flutter_map/flutter_map.dart';
@@ -111,8 +113,8 @@ class _GeofenceTestScreenState extends ConsumerState<GeofenceTestScreen> {
.check_circle
: Icons.cancel,
color: inside
? Colors.green
: Colors.red,
? AppStatusColors.of(context).success.color
: AppStatusColors.of(context).danger.color,
),
const SizedBox(
width: 8,
@@ -166,8 +168,8 @@ class _GeofenceTestScreenState extends ConsumerState<GeofenceTestScreen> {
? Icons.check_circle
: Icons.cancel,
color: inside
? Colors.green
: Colors.red,
? AppStatusColors.of(context).success.color
: AppStatusColors.of(context).danger.color,
),
const SizedBox(width: 8),
Text(
@@ -275,10 +277,10 @@ class _GeofenceTestScreenState extends ConsumerState<GeofenceTestScreen> {
width: 40,
height: 40,
// `child` is used by newer flutter_map Marker API.
child: const Icon(
child: Icon(
Icons.person_pin_circle,
size: 36,
color: Colors.blue,
color: Theme.of(context).colorScheme.primary,
),
),
);
@@ -289,10 +291,12 @@ class _GeofenceTestScreenState extends ConsumerState<GeofenceTestScreen> {
polygons: [
Polygon(
points: polygonPoints,
color: Colors.green.withValues(
alpha: 0.15,
),
borderColor: Colors.green,
color: AppStatusColors.of(context)
.success
.color
.withValues(alpha: 0.15),
borderColor:
AppStatusColors.of(context).success.color,
borderStrokeWidth: 2,
),
],
@@ -304,9 +308,10 @@ class _GeofenceTestScreenState extends ConsumerState<GeofenceTestScreen> {
circles: [
CircleMarker(
point: LatLng(cfg!.lat!, cfg.lng!),
color: Colors.green.withValues(
alpha: 0.10,
),
color: AppStatusColors.of(context)
.success
.color
.withValues(alpha: 0.10),
borderStrokeWidth: 2,
useRadiusInMeter: true,
radius: cfg.radiusMeters ?? 100,
@@ -387,11 +392,15 @@ class _GeofenceTestScreenState extends ConsumerState<GeofenceTestScreen> {
width: 12,
height: 12,
decoration: BoxDecoration(
color: Colors.green.withValues(
color: AppStatusColors.of(
context,
).success.color.withValues(
alpha: 0.25,
),
border: Border.all(
color: Colors.green,
color: AppStatusColors.of(
context,
).success.color,
),
),
),
@@ -420,14 +429,16 @@ class _GeofenceTestScreenState extends ConsumerState<GeofenceTestScreen> {
top: 6.0,
),
child: Row(
children: const [
children: [
Icon(
Icons.person_pin_circle,
size: 14,
color: Colors.blue,
color: Theme.of(
context,
).colorScheme.primary,
),
SizedBox(width: 8),
Text('You'),
const SizedBox(width: 8),
const Text('You'),
],
),
),
+24 -22
View File
@@ -35,6 +35,7 @@ import 'logbook_day_activity.dart';
import 'work_log_tab.dart';
import '../../theme/m3_motion.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
import '../../utils/app_time.dart';
import '../../utils/device_security.dart';
import '../../utils/location_permission.dart';
@@ -787,7 +788,7 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
} else if (diff.inMinutes <= 5) {
timeColor = colors.error;
} else if (diff.inMinutes <= 15) {
timeColor = Colors.orange;
timeColor = AppStatusColors.of(context).warning.color;
} else if (diff.inMinutes <= 30) {
timeColor = colors.tertiary;
}
@@ -847,7 +848,7 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
: Icons.cancel,
size: 16,
color: _insideGeofence || hasGeofenceOverride
? Colors.green
? AppStatusColors.of(context).success.color
: colors.error,
),
const SizedBox(width: 6),
@@ -859,7 +860,7 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
: 'Outside geofence',
style: theme.textTheme.bodySmall?.copyWith(
color: _insideGeofence || hasGeofenceOverride
? Colors.green
? AppStatusColors.of(context).success.color
: colors.error,
fontWeight: FontWeight.w500,
),
@@ -1863,11 +1864,11 @@ class _CheckInTabState extends ConsumerState<_CheckInTab> {
bg = colors.secondaryContainer;
fg = colors.onSecondaryContainer;
case 'Early Out':
bg = Colors.orange.shade100;
fg = Colors.orange.shade900;
bg = AppStatusColors.of(context).warning.container;
fg = AppStatusColors.of(context).warning.onContainer;
case 'Noon Break':
bg = Colors.blue.shade100;
fg = Colors.blue.shade900;
bg = AppStatusColors.of(context).info.container;
fg = AppStatusColors.of(context).info.onContainer;
case 'Absent':
bg = colors.errorContainer;
fg = colors.onErrorContainer;
@@ -2841,13 +2842,14 @@ class _DateGroupTile extends StatelessWidget {
final colors = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
final status = AppStatusColors.of(context);
final statusColor = group.status == 'Absent'
? Colors.red
? status.danger.color
: group.status == 'On duty'
? Colors.orange
? status.warning.color
: group.status == 'On Leave'
? Colors.teal
: Colors.green;
? status.info.color
: status.success.color;
return Card(
margin: const EdgeInsets.fromLTRB(12, 8, 12, 0),
@@ -2960,10 +2962,10 @@ class _DateGroupTile extends StatelessWidget {
String statusLabel;
switch (slip.status) {
case 'approved':
statusColor = Colors.green;
statusColor = AppStatusColors.of(context).success.color;
statusLabel = 'Approved';
case 'pending':
statusColor = Colors.orange;
statusColor = AppStatusColors.of(context).warning.color;
statusLabel = 'Pending';
case 'rejected':
statusColor = colors.error;
@@ -3081,7 +3083,7 @@ class _DateGroupTile extends StatelessWidget {
? Chip(
label: Text(entry.isLeave ? 'On Leave' : 'Absent'),
backgroundColor: entry.isLeave
? Colors.teal.withValues(alpha: 0.15)
? AppStatusColors.of(context).info.container
: colors.errorContainer,
)
: entry.status == 'On duty'
@@ -3107,7 +3109,7 @@ class _DateGroupTile extends StatelessWidget {
switch (entry.verificationStatus) {
case 'verified':
icon = Icons.verified;
color = Colors.green;
color = AppStatusColors.of(context).success.color;
tooltip = 'Verified';
case 'unverified' || 'skipped':
icon = Icons.warning_amber_rounded;
@@ -3115,7 +3117,7 @@ class _DateGroupTile extends StatelessWidget {
tooltip = 'Unverified';
default:
icon = Icons.hourglass_bottom;
color = Colors.orange;
color = AppStatusColors.of(context).warning.color;
tooltip = 'Pending';
}
final badge = Tooltip(
@@ -3526,7 +3528,7 @@ class _CheckInOutMapTab extends StatelessWidget {
title: 'Check In',
lat: entry.checkInLat,
lng: entry.checkInLng,
markerColor: Colors.green,
markerColor: AppStatusColors.of(context).success.color,
),
),
const SizedBox(width: 12),
@@ -3536,7 +3538,7 @@ class _CheckInOutMapTab extends StatelessWidget {
title: 'Check Out',
lat: entry.checkOutLat,
lng: entry.checkOutLng,
markerColor: Colors.red,
markerColor: AppStatusColors.of(context).danger.color,
),
),
],
@@ -5830,7 +5832,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
IconData statusIcon;
switch (slip.status) {
case 'approved':
statusColor = Colors.green;
statusColor = AppStatusColors.of(context).success.color;
statusIcon = Icons.check_circle;
case 'rejected':
statusColor = colors.error;
@@ -5839,7 +5841,7 @@ class _PassSlipTabState extends ConsumerState<_PassSlipTab> {
statusColor = colors.primary;
statusIcon = Icons.task_alt;
default:
statusColor = Colors.orange;
statusColor = AppStatusColors.of(context).warning.color;
statusIcon = Icons.hourglass_top;
}
@@ -6248,7 +6250,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
IconData statusIcon;
switch (leave.status) {
case 'approved':
statusColor = Colors.teal;
statusColor = AppStatusColors.of(context).success.color;
statusIcon = Icons.check_circle;
case 'rejected':
statusColor = colors.error;
@@ -6257,7 +6259,7 @@ class _LeaveTabState extends ConsumerState<_LeaveTab> {
statusColor = colors.onSurfaceVariant;
statusIcon = Icons.remove_circle_outline;
default:
statusColor = Colors.orange;
statusColor = AppStatusColors.of(context).warning.color;
statusIcon = Icons.hourglass_top;
}
@@ -7,6 +7,7 @@ import '../../models/it_service_request.model.dart';
import '../../models/pass_slip.model.dart';
import '../../models/task.model.dart';
import '../../models/ticket.model.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/m3_motion.dart';
import '../../utils/app_time.dart';
@@ -427,7 +428,7 @@ class _SummaryChip extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
+7 -5
View File
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../providers/auth_provider.dart';
import '../../providers/tickets_provider.dart';
import '../../theme/app_status_colors.dart';
import '../../theme/m3_motion.dart';
import '../../utils/snackbar.dart';
@@ -563,23 +564,24 @@ class _SignUpScreenState extends ConsumerState<SignUpScreen>
}
final normalized = (score / 6).clamp(0.0, 1.0);
final s = AppStatusColors.of(context);
String label;
Color color;
if (normalized <= 0.2) {
label = 'Very weak';
color = Colors.red;
color = s.danger.color;
} else if (normalized <= 0.4) {
label = 'Weak';
color = Colors.deepOrange;
color = s.danger.color;
} else if (normalized <= 0.6) {
label = 'Fair';
color = Colors.orange;
color = s.warning.color;
} else if (normalized <= 0.8) {
label = 'Strong';
color = Colors.green;
color = s.info.color;
} else {
label = 'Excellent';
color = Colors.teal;
color = s.success.color;
}
setState(() {
+120 -56
View File
@@ -38,6 +38,7 @@ import '../../widgets/app_breakpoints.dart';
import '../../providers/realtime_controller.dart';
import 'package:skeletonizer/skeletonizer.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/app_page_header.dart';
import '../../utils/app_time.dart';
@@ -776,14 +777,29 @@ class _DashboardScreenState extends State<DashboardScreen> {
}
Widget _sectionTitle(BuildContext context, String title) {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
child: Row(
children: [
Container(
width: 3,
height: 16,
decoration: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 10),
Text(
title.toUpperCase(),
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.8,
color: cs.onSurface,
),
),
],
),
);
}
@@ -918,21 +934,26 @@ class _MetricCard extends ConsumerWidget {
),
),
);
final isDark = cs.brightness == Brightness.dark;
return AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: cs.surfaceContainerLow,
borderRadius: BorderRadius.circular(16),
color: isDark ? cs.surfaceContainer : cs.surfaceContainerLowest,
borderRadius: BorderRadius.circular(
AppSurfaces.of(context).cardRadius,
),
border: Border.all(color: cs.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
title.toUpperCase(),
style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.6,
color: cs.onSurfaceVariant,
),
),
@@ -967,14 +988,16 @@ class _StaffTable extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
// M3 Expressive: tonal surface container, 28 dp radius for large containers.
final isDark = cs.brightness == Brightness.dark;
// Cyber/IT panel: flat tonal fill + hairline outline.
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.surfaceContainerLow,
color: isDark ? cs.surfaceContainer : cs.surfaceContainerLowest,
borderRadius: BorderRadius.circular(
AppSurfaces.of(context).containerRadius,
),
border: Border.all(color: cs.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1059,18 +1082,23 @@ class _StaffTableBody extends ConsumerWidget {
);
if (providerState == 'loading') {
return const Text('Loading staff...');
return _panelHint(context, Icons.hourglass_empty_rounded, 'Loading staff…');
}
if (providerState.startsWith('error:')) {
final err = providerState.substring(6);
return Text('Failed to load staff: $err');
return _panelHint(
context,
Icons.error_outline_rounded,
'Failed to load staff: $err',
);
}
if (staffRows.isEmpty) {
return Text(
return _panelHint(
context,
Icons.groups_outlined,
'No IT staff available.',
style: Theme.of(context).textTheme.bodySmall,
);
}
@@ -1142,20 +1170,24 @@ class _StaffRow extends StatelessWidget {
Expanded(
flex: 4,
child: Center(
child: Text(
row.whereabouts,
style: valueStyle?.copyWith(
color: row.whereabouts == 'In premise'
? Colors.green
: row.whereabouts == 'Outside premise'
? Colors.grey
: row.whereabouts == 'Tracking off'
? Colors.grey
: row.whereabouts.startsWith('Last seen')
? Colors.grey
: null,
fontWeight: FontWeight.w600,
),
child: Builder(
builder: (context) {
final status = AppStatusColors.of(context);
final Color? color = row.whereabouts == 'In premise'
? status.success.color
: (row.whereabouts == 'Outside premise' ||
row.whereabouts == 'Tracking off' ||
row.whereabouts.startsWith('Last seen'))
? status.neutral.color
: null;
return Text(
row.whereabouts,
style: valueStyle?.copyWith(
color: color,
fontWeight: FontWeight.w600,
),
);
},
),
),
),
@@ -1202,21 +1234,24 @@ class _StaffActivityChart extends ConsumerWidget {
final cs = Theme.of(context).colorScheme;
final isDark = cs.brightness == Brightness.dark;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.surfaceContainerLow,
color: isDark ? cs.surfaceContainer : cs.surfaceContainerLowest,
borderRadius: BorderRadius.circular(
AppSurfaces.of(context).containerRadius,
),
border: Border.all(color: cs.outlineVariant),
),
child: metricsAsync.when(
data: (metrics) {
final staffRows = metrics.staffRows;
if (staffRows.isEmpty) {
return Text(
return _panelHint(
context,
Icons.bar_chart_outlined,
'No staff activity data available.',
style: Theme.of(context).textTheme.bodySmall,
);
}
@@ -1393,16 +1428,16 @@ class _StaffActivityChart extends ConsumerWidget {
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.black.withAlpha(
(0.35 * 255).round(),
color: cs.inverseSurface.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(
AppSurfaces.of(context).chipRadius,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
child: MonoText(
total.toString(),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: Colors.white,
color: cs.onInverseSurface,
fontWeight: FontWeight.w700,
),
),
@@ -1534,8 +1569,13 @@ class _StaffActivityChart extends ConsumerWidget {
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Text('Failed to load activity: $error'),
loading: () =>
_panelHint(context, Icons.hourglass_empty_rounded, 'Loading activity…'),
error: (error, stack) => _panelHint(
context,
Icons.error_outline_rounded,
'Failed to load activity: $error',
),
),
);
}
@@ -1556,29 +1596,28 @@ class _PulseStatusPill extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final s = AppStatusColors.of(context);
final (Color bg, Color fg) = switch (label.toLowerCase()) {
'arrival' => (Colors.amber.shade100, Colors.amber.shade900),
'late' => (Colors.red.shade100, Colors.red.shade900),
'noon break' => (Colors.blue.shade100, Colors.blue.shade900),
'vacant' => (Colors.green.shade100, Colors.green.shade900),
'on task' => (Colors.purple.shade100, Colors.purple.shade900),
'on event' => (Colors.purple.shade100, Colors.purple.shade900),
'in triage' => (Colors.orange.shade100, Colors.orange.shade900),
'early out' => (Colors.deepOrange.shade100, Colors.deepOrange.shade900),
'on leave' => (Colors.teal.shade100, Colors.teal.shade900),
'absent' => (Colors.red.shade200, Colors.red.shade900),
'off duty' => (Colors.grey.shade200, Colors.grey.shade700),
_ => (
Theme.of(context).colorScheme.tertiaryContainer,
Theme.of(context).colorScheme.onTertiaryContainer,
),
'arrival' => (s.success.container, s.success.onContainer),
'late' => (s.warning.container, s.warning.onContainer),
'noon break' => (s.info.container, s.info.onContainer),
'vacant' => (s.neutral.container, s.neutral.onContainer),
'on task' => (cs.primaryContainer, cs.onPrimaryContainer),
'on event' => (cs.tertiaryContainer, cs.onTertiaryContainer),
'in triage' => (s.warning.container, s.warning.onContainer),
'early out' => (s.warning.container, s.warning.onContainer),
'on leave' => (s.info.container, s.info.onContainer),
'absent' => (s.danger.container, s.danger.onContainer),
'off duty' => (s.neutral.container, s.neutral.onContainer),
_ => (cs.tertiaryContainer, cs.onTertiaryContainer),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius),
),
child: Text(
label,
@@ -1591,6 +1630,31 @@ class _PulseStatusPill extends StatelessWidget {
}
}
/// Compact, muted, centered hint for empty/loading/error states inside a
/// bounded dashboard panel (too small for the full-screen AppEmptyView).
Widget _panelHint(BuildContext context, IconData icon, String message) {
final cs = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 18, color: cs.onSurfaceVariant),
const SizedBox(width: 8),
Flexible(
child: Text(
message,
textAlign: TextAlign.center,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
),
),
],
),
);
}
Duration? _averageDuration(List<Duration> durations) {
if (durations.isEmpty) {
return null;
@@ -29,6 +29,7 @@ import '../../widgets/gemini_animated_text_field.dart';
import '../../widgets/m3_card.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart';
import '../../theme/app_status_colors.dart';
import '../../widgets/status_pill.dart';
import 'it_service_request_pdf.dart';
@@ -411,13 +412,13 @@ class _ItServiceRequestDetailScreenState
),
)
: _hasRecentSave
? const Padding(
key: ValueKey('isr_saved'),
padding: EdgeInsets.symmetric(horizontal: 8),
? Padding(
key: const ValueKey('isr_saved'),
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Icon(
Icons.check_circle,
size: 18,
color: Colors.green,
color: AppStatusColors.of(context).success.color,
),
)
: const SizedBox(key: ValueKey('isr_idle')),
@@ -555,7 +556,10 @@ class _ItServiceRequestDetailScreenState
if (request.requestNumber != null)
MonoText(request.requestNumber!),
const SizedBox(width: 12),
StatusPill(label: ItServiceRequestStatus.label(request.status)),
StatusPill.kind(
_isrStatusKind(request.status),
label: ItServiceRequestStatus.label(request.status),
),
const Spacer(),
Text(
'Created ${AppTime.formatDate(request.createdAt)}',
@@ -1231,10 +1235,10 @@ class _ItServiceRequestDetailScreenState
child: const Icon(Icons.save, size: 14),
)
: isSaved
? const Icon(
? Icon(
Icons.check_circle,
key: ValueKey('label_saved'),
color: Colors.green,
key: const ValueKey('label_saved'),
color: AppStatusColors.of(context).success.color,
size: 14,
)
: const SizedBox(key: ValueKey('label_idle')),
@@ -1584,9 +1588,6 @@ class _ItServiceRequestDetailScreenState
final controller = TextEditingController();
return AlertDialog(
title: const Text('Cancellation Reason'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
content: TextField(
controller: controller,
decoration: const InputDecoration(
@@ -1836,7 +1837,6 @@ class _StaffPickerDialogState extends State<_StaffPickerDialog> {
.toList();
return AlertDialog(
title: const Text('Select IT Staff'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
content: SizedBox(
width: 350,
height: 400,
@@ -2245,3 +2245,16 @@ class _EvidenceSectionState extends ConsumerState<_EvidenceSection> {
);
}
}
/// Maps an IT service request status to a semantic [StatusKind]
/// (amber=pending approval, blue=scheduled/in-progress, green=completed,
/// grey=cancelled).
StatusKind _isrStatusKind(String status) => switch (status) {
ItServiceRequestStatus.pendingApproval => StatusKind.warning,
ItServiceRequestStatus.scheduled => StatusKind.info,
ItServiceRequestStatus.inProgress => StatusKind.info,
ItServiceRequestStatus.inProgressDryRun => StatusKind.info,
ItServiceRequestStatus.completed => StatusKind.success,
ItServiceRequestStatus.cancelled => StatusKind.neutral,
_ => StatusKind.neutral,
};
@@ -17,6 +17,7 @@ import '../../providers/tickets_provider.dart';
import '../../theme/m3_motion.dart';
import '../../utils/app_time.dart';
import '../../utils/snackbar.dart';
import '../../theme/app_status_colors.dart';
import '../../widgets/m3_card.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/reconnect_overlay.dart';
@@ -381,9 +382,6 @@ class _ItServiceRequestsListScreenState
builder: (ctx, setDialogState) {
return AlertDialog(
title: const Text('New IT Service Request'),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
content: SizedBox(
width: 400,
child: SingleChildScrollView(
@@ -538,6 +536,7 @@ class _StatusSummaryRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final status = AppStatusColors.of(context);
final pending = requests
.where((r) => r.status == ItServiceRequestStatus.pendingApproval)
.length;
@@ -563,7 +562,7 @@ class _StatusSummaryRow extends StatelessWidget {
_SummaryChip(
label: 'Pending',
count: pending,
color: cs.tertiary,
color: status.warning.color,
selected: selectedStatus == ItServiceRequestStatus.pendingApproval,
onTap: () => onStatusTap(ItServiceRequestStatus.pendingApproval),
),
@@ -571,7 +570,7 @@ class _StatusSummaryRow extends StatelessWidget {
_SummaryChip(
label: 'Scheduled',
count: scheduled,
color: cs.primary,
color: status.info.color,
selected: selectedStatus == ItServiceRequestStatus.scheduled,
onTap: () => onStatusTap(ItServiceRequestStatus.scheduled),
),
@@ -587,7 +586,7 @@ class _StatusSummaryRow extends StatelessWidget {
_SummaryChip(
label: 'Completed',
count: completed,
color: Colors.green,
color: status.success.color,
selected: selectedStatus == ItServiceRequestStatus.completed,
onTap: () => onStatusTap(ItServiceRequestStatus.completed),
),
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import '../../models/network/network_device.dart';
import '../../models/network/network_location.dart';
import '../../theme/app_status_colors.dart';
import '../../providers/network_map/network_devices_provider.dart';
import '../../providers/network_map/network_sites_provider.dart';
import '../../widgets/app_state_view.dart';
@@ -231,11 +232,19 @@ class _NetworkMapDeviceEditScreenState
final cs = Theme.of(context).colorScheme;
switch (_status) {
case NetworkDeviceStatus.online:
return const Icon(Icons.circle, size: 14, color: Color(0xFF388E3C));
return Icon(
Icons.circle,
size: 14,
color: AppStatusColors.of(context).success.color,
);
case NetworkDeviceStatus.offline:
return Icon(Icons.circle, size: 14, color: cs.error);
case NetworkDeviceStatus.warning:
return const Icon(Icons.circle, size: 14, color: Color(0xFFF57C00));
return Icon(
Icons.circle,
size: 14,
color: AppStatusColors.of(context).warning.color,
);
case NetworkDeviceStatus.unknown:
return Icon(Icons.circle_outlined, size: 14, color: cs.onSurfaceVariant);
}
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../../models/network/network_device.dart';
import '../../../theme/app_status_colors.dart';
import 'network_device_icon.dart';
/// A styled node representing one network device on the topology canvas.
@@ -41,14 +42,15 @@ class DeviceNode extends StatelessWidget {
}
}
Color? _statusColor(ColorScheme cs, NetworkDeviceStatus status) {
Color? _statusColor(BuildContext context, NetworkDeviceStatus status) {
final s = AppStatusColors.of(context);
switch (status) {
case NetworkDeviceStatus.online:
return const Color(0xFF34A853); // Google green — universally understood
return s.success.color;
case NetworkDeviceStatus.offline:
return cs.error;
return Theme.of(context).colorScheme.error;
case NetworkDeviceStatus.warning:
return const Color(0xFFFBBC04); // Amber
return s.warning.color;
case NetworkDeviceStatus.unknown:
return null; // No dot shown
}
@@ -59,7 +61,7 @@ class DeviceNode extends StatelessWidget {
final cs = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
final accent = _accentForRole(cs, device.role);
final statusColor = _statusColor(cs, device.status);
final statusColor = _statusColor(context, device.status);
final bgColor = isSelected
? cs.primaryContainer
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../../../models/network/network_device.dart';
import '../../../models/network/network_link.dart';
import '../../../theme/app_surfaces.dart';
/// Collapsible legend overlay explaining device role border colours and link
/// type colours. Positioned at the bottom-left of the topology canvas view.
@@ -85,11 +86,15 @@ class _TopologyLegendState extends State<TopologyLegend>
// Expand/collapse toggle chip
Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(
AppSurfaces.of(context).containerRadius,
),
elevation: 2,
child: InkWell(
onTap: _toggle,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(
AppSurfaces.of(context).containerRadius,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
@@ -109,7 +109,7 @@ class _RequestTypeChartState extends ConsumerState<RequestTypeChart> {
radius: isTouched ? 60 : 50,
color: colorFor(i, e.name),
borderSide: isTouched
? const BorderSide(color: Colors.white, width: 2)
? BorderSide(color: Theme.of(context).colorScheme.surface, width: 2)
: BorderSide.none,
);
}).toList(),
@@ -240,7 +240,7 @@ class _RequestCategoryChartState extends ConsumerState<RequestCategoryChart> {
radius: isTouched ? 60 : 50,
color: colorFor(i, e.name),
borderSide: isTouched
? const BorderSide(color: Colors.white, width: 2)
? BorderSide(color: Theme.of(context).colorScheme.surface, width: 2)
: BorderSide.none,
);
}).toList(),
+18 -13
View File
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../providers/reports_provider.dart';
import '../../../theme/app_status_colors.dart';
import 'report_card_wrapper.dart';
/// Donut chart ticket counts per status with hover animation.
@@ -84,7 +85,7 @@ class _TicketsByStatusChartState extends ConsumerState<TicketsByStatusChart> {
radius: isTouched ? 60 : 50,
color: _ticketStatusColor(context, e.status),
borderSide: isTouched
? const BorderSide(color: Colors.white, width: 2)
? BorderSide(color: Theme.of(context).colorScheme.surface, width: 2)
: BorderSide.none,
);
}).toList(),
@@ -116,16 +117,18 @@ class _TicketsByStatusChartState extends ConsumerState<TicketsByStatusChart> {
}
Color _ticketStatusColor(BuildContext context, String status) {
final colors = Theme.of(context).colorScheme;
final s = AppStatusColors.of(context);
switch (status) {
case 'critical':
return s.danger.color;
case 'pending':
return colors.tertiary;
return s.warning.color;
case 'promoted':
return colors.secondary;
return s.info.color;
case 'closed':
return colors.primary;
return s.success.color;
default:
return colors.outlineVariant;
return s.neutral.color;
}
}
}
@@ -208,7 +211,7 @@ class _TasksByStatusChartState extends ConsumerState<TasksByStatusChart> {
radius: isTouched ? 60 : 50,
color: _taskStatusColor(context, e.status),
borderSide: isTouched
? const BorderSide(color: Colors.white, width: 2)
? BorderSide(color: Theme.of(context).colorScheme.surface, width: 2)
: BorderSide.none,
);
}).toList(),
@@ -240,18 +243,20 @@ class _TasksByStatusChartState extends ConsumerState<TasksByStatusChart> {
}
Color _taskStatusColor(BuildContext context, String status) {
final colors = Theme.of(context).colorScheme;
final s = AppStatusColors.of(context);
switch (status) {
case 'critical':
return s.danger.color;
case 'queued':
return colors.surfaceContainerHighest;
return s.neutral.color;
case 'in_progress':
return colors.secondary;
return s.info.color;
case 'completed':
return colors.primary;
return s.success.color;
case 'cancelled':
return colors.error;
return s.danger.color;
default:
return colors.outlineVariant;
return s.neutral.color;
}
}
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../providers/verification_session_provider.dart';
import '../../services/face_verification.dart' as face;
import '../../theme/app_status_colors.dart';
/// Screen opened on mobile when the user scans a QR code for cross-device
/// face verification. Performs liveness detection and uploads the result.
@@ -144,7 +145,11 @@ class _MobileVerificationScreenState
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, size: 64, color: Colors.green),
Icon(
Icons.check_circle,
size: 64,
color: AppStatusColors.of(context).success.color,
),
const SizedBox(height: 16),
Text('Verification Complete', style: theme.textTheme.headlineSmall),
const SizedBox(height: 8),
+5 -1
View File
@@ -1,4 +1,5 @@
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:permission_handler/permission_handler.dart';
@@ -67,7 +68,10 @@ class _PermissionsScreenState extends ConsumerState<PermissionsScreen> {
title: Text(info.label),
subtitle: Text(status?.toString() ?? 'unknown'),
trailing: granted
? const Icon(Icons.check, color: Colors.green)
? Icon(
Icons.check,
color: AppStatusColors.of(context).success.color,
)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
+21 -14
View File
@@ -13,6 +13,8 @@ import '../../providers/teams_provider.dart';
import '../../utils/app_time.dart';
import '../../utils/snackbar.dart';
import '../../widgets/app_breakpoints.dart';
import '../../theme/app_status_colors.dart';
import '../../theme/app_surfaces.dart';
import '../../widgets/m3_card.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/profile_avatar.dart';
@@ -231,24 +233,25 @@ class _ItJobChecklistTabState extends ConsumerState<ItJobChecklistTab> {
}
Widget _buildSkeletonTile(BuildContext context, bool isWide) {
final bone = Theme.of(context).colorScheme.surfaceContainerHighest;
return M3Card.outlined(
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(width: 60, height: 14, color: Colors.grey),
Container(width: 60, height: 14, color: bone),
const SizedBox(width: 12),
Expanded(
child: Container(height: 14, color: Colors.grey),
child: Container(height: 14, color: bone),
),
if (isWide) ...[
const SizedBox(width: 12),
Container(width: 80, height: 22, color: Colors.grey),
Container(width: 80, height: 22, color: bone),
const SizedBox(width: 12),
Container(width: 120, height: 14, color: Colors.grey),
Container(width: 120, height: 14, color: bone),
],
const SizedBox(width: 12),
Container(width: 24, height: 24, color: Colors.grey),
Container(width: 24, height: 24, color: bone),
],
),
),
@@ -292,20 +295,21 @@ class _StatsCard extends StatelessWidget {
),
);
final status = AppStatusColors.of(context);
final submittedChip = _StatChip(
icon: Icons.check_circle_outline,
label: 'Submitted',
count: submitted,
color: cs.primary,
bgColor: cs.primaryContainer,
color: status.success.onContainer,
bgColor: status.success.container,
);
final pendingChip = _StatChip(
icon: Icons.pending_outlined,
label: 'Pending',
count: pending,
color: cs.tertiary,
bgColor: cs.tertiaryContainer,
color: status.warning.onContainer,
bgColor: status.warning.container,
);
return M3Card.filled(
@@ -399,7 +403,7 @@ class _StatChip extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -878,11 +882,14 @@ class _ItJobTileState extends ConsumerState<_ItJobTile> {
_isChecked ? cs.primaryContainer.withValues(alpha: 0.18) : null;
// Status badge
final statusSwatch = _isChecked
? AppStatusColors.of(context).success
: AppStatusColors.of(context).warning;
final statusBadge = Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: _isChecked ? cs.primaryContainer : cs.tertiaryContainer,
borderRadius: BorderRadius.circular(20),
color: statusSwatch.container,
borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -892,13 +899,13 @@ class _ItJobTileState extends ConsumerState<_ItJobTile> {
? Icons.check_circle_outline
: Icons.hourglass_empty_outlined,
size: 12,
color: _isChecked ? cs.primary : cs.tertiary,
color: statusSwatch.onContainer,
),
const SizedBox(width: 4),
Text(
_isChecked ? 'Submitted' : 'Pending',
style: tt.labelSmall?.copyWith(
color: _isChecked ? cs.primary : cs.tertiary,
color: statusSwatch.onContainer,
fontWeight: FontWeight.w600,
),
),
+35 -38
View File
@@ -35,6 +35,7 @@ import '../../widgets/office_picker.dart';
import '../../widgets/mono_text.dart';
import '../../widgets/responsive_body.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
import '../../widgets/sync_pending_badge.dart';
import '../../widgets/task_assignment_section.dart';
import '../../widgets/typing_dots.dart';
@@ -621,9 +622,9 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
const SizedBox(height: 12),
Row(
children: [
const Icon(
Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
color: AppStatusColors.of(context).warning.color,
),
const SizedBox(width: 6),
Expanded(
@@ -2686,16 +2687,24 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
final files =
_attachments ?? [];
if (files.isEmpty) {
return const Center(
return Center(
child: Padding(
padding: EdgeInsets.all(
padding:
const EdgeInsets.all(
16.0,
),
child: Text(
'No attachments yet',
style: TextStyle(
color: Colors.grey,
),
style: Theme.of(context)
.textTheme
.bodySmall
?.copyWith(
color: Theme.of(
context,
)
.colorScheme
.onSurfaceVariant,
),
),
),
);
@@ -2722,9 +2731,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
).textTheme.bodySmall,
),
trailing: IconButton(
icon: const Icon(
icon: Icon(
Icons.delete,
color: Colors.red,
color: Theme.of(
context,
).colorScheme.error,
),
onPressed: () =>
_deleteTaskAttachment(
@@ -2873,7 +2884,11 @@ class _TaskDetailScreenState extends ConsumerState<TaskDetailScreen>
.colorScheme
.surfaceContainerHighest,
borderRadius:
BorderRadius.circular(16),
BorderRadius.circular(
AppSurfaces.of(
context,
).cardRadius,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -4883,39 +4898,21 @@ class _TaskStatusChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final (icon, bg, fg) = switch (status) {
'queued' => (
Icons.inbox_rounded,
cs.secondaryContainer,
cs.onSecondaryContainer,
),
'in_progress' => (
Icons.play_arrow_rounded,
cs.primaryContainer,
cs.onPrimaryContainer,
),
'completed' => (
Icons.check_circle_rounded,
cs.tertiaryContainer,
cs.onTertiaryContainer,
),
'cancelled' => (
Icons.cancel_rounded,
cs.errorContainer,
cs.onErrorContainer,
),
_ => (
Icons.help_outline_rounded,
cs.surfaceContainerHighest,
cs.onSurface,
),
final s = AppStatusColors.of(context);
final (IconData icon, StatusSwatch swatch) = switch (status) {
'queued' => (Icons.inbox_rounded, s.neutral),
'in_progress' => (Icons.play_arrow_rounded, s.info),
'completed' => (Icons.check_circle_rounded, s.success),
'cancelled' => (Icons.cancel_rounded, s.danger),
_ => (Icons.help_outline_rounded, s.neutral),
};
final bg = swatch.container;
final fg = swatch.onContainer;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
+22 -35
View File
@@ -29,6 +29,7 @@ import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart';
import '../../widgets/typing_dots.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
import '../../utils/snackbar.dart';
import '../../widgets/app_breakpoints.dart';
import '../../widgets/app_page_header.dart';
@@ -523,10 +524,10 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
if (task.status == 'completed' &&
task.hasIncompleteDetails) ...[
const SizedBox(width: 4),
const Icon(
Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange,
color: AppStatusColors.of(context).warning.color,
),
],
],
@@ -620,10 +621,10 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen>
if (task.status == 'completed' &&
task.hasIncompleteDetails) ...[
const SizedBox(width: 4),
const Icon(
Icon(
Icons.warning_amber_rounded,
size: 16,
color: Colors.orange,
color: AppStatusColors.of(context).warning.color,
),
],
if (showTyping) ...[
@@ -1353,26 +1354,14 @@ class _StatusSummaryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final background = switch (status) {
'critical' => scheme.errorContainer,
'queued' => scheme.surfaceContainerHighest,
'in_progress' => scheme.secondaryContainer,
'completed' => scheme.primaryContainer,
_ => scheme.surfaceContainerHigh,
};
final foreground = switch (status) {
'critical' => scheme.onErrorContainer,
'queued' => scheme.onSurfaceVariant,
'in_progress' => scheme.onSecondaryContainer,
'completed' => scheme.onPrimaryContainer,
_ => scheme.onSurfaceVariant,
};
final swatch = AppStatusColors.of(context).byKind(_taskStatusKind(status));
final background = swatch.container;
final foreground = swatch.onContainer;
final label = status.replaceAll('_', ' ').toUpperCase();
return Material(
color: background,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
@@ -1401,6 +1390,16 @@ class _StatusSummaryCard extends StatelessWidget {
}
}
/// Maps a task status to a semantic [StatusKind] (red=critical, blue=in
/// progress, green=completed, grey=queued).
StatusKind _taskStatusKind(String status) => switch (status) {
'critical' => StatusKind.danger,
'in_progress' => StatusKind.info,
'completed' => StatusKind.success,
'queued' => StatusKind.neutral,
_ => StatusKind.neutral,
};
String _assignedAgent(Map<String, Profile> profileById, String? userId) {
if (userId == null || userId.isEmpty) {
return 'Unassigned';
@@ -1422,21 +1421,9 @@ class _StatusBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final background = switch (status) {
'critical' => scheme.errorContainer,
'queued' => scheme.surfaceContainerHighest,
'in_progress' => scheme.secondaryContainer,
'completed' => scheme.primaryContainer,
_ => scheme.surfaceContainerHighest,
};
final foreground = switch (status) {
'critical' => scheme.onErrorContainer,
'queued' => scheme.onSurfaceVariant,
'in_progress' => scheme.onSecondaryContainer,
'completed' => scheme.onPrimaryContainer,
_ => scheme.onSurfaceVariant,
};
final swatch = AppStatusColors.of(context).byKind(_taskStatusKind(status));
final background = swatch.container;
final foreground = swatch.onContainer;
return Badge(
backgroundColor: background,
+16 -25
View File
@@ -22,6 +22,7 @@ import '../../widgets/responsive_body.dart';
import '../../widgets/task_assignment_section.dart';
import '../../widgets/typing_dots.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
class TicketDetailScreen extends ConsumerStatefulWidget {
const TicketDetailScreen({super.key, required this.ticketId});
@@ -424,7 +425,9 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(
AppSurfaces.of(context).cardRadius,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
@@ -467,7 +470,9 @@ class _TicketDetailScreenState extends ConsumerState<TicketDetailScreen> {
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
borderRadius: BorderRadius.circular(28),
borderRadius: BorderRadius.circular(
AppSurfaces.of(context).containerRadius,
),
),
padding: const EdgeInsets.only(
left: 16,
@@ -1357,34 +1362,20 @@ class _TicketStatusChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final (icon, bg, fg) = switch (status) {
'pending' => (
Icons.hourglass_empty_rounded,
cs.secondaryContainer,
cs.onSecondaryContainer,
),
'promoted' => (
Icons.trending_up_rounded,
cs.primaryContainer,
cs.onPrimaryContainer,
),
'closed' => (
Icons.lock_rounded,
cs.tertiaryContainer,
cs.onTertiaryContainer,
),
_ => (
Icons.help_outline_rounded,
cs.surfaceContainerHighest,
cs.onSurface,
),
final s = AppStatusColors.of(context);
final (IconData icon, StatusSwatch swatch) = switch (status) {
'pending' => (Icons.hourglass_empty_rounded, s.warning),
'promoted' => (Icons.trending_up_rounded, s.info),
'closed' => (Icons.lock_rounded, s.success),
_ => (Icons.help_outline_rounded, s.neutral),
};
final bg = swatch.container;
final fg = swatch.onContainer;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
+18 -31
View File
@@ -23,6 +23,7 @@ import '../../widgets/responsive_body.dart';
import '../../widgets/tasq_adaptive_list.dart';
import '../../widgets/typing_dots.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
import '../../utils/snackbar.dart';
import '../../widgets/app_breakpoints.dart';
import '../../widgets/app_page_header.dart';
@@ -739,26 +740,14 @@ class _StatusSummaryCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final background = switch (status) {
'critical' => scheme.errorContainer,
'pending' => scheme.tertiaryContainer,
'promoted' => scheme.secondaryContainer,
'closed' => scheme.primaryContainer,
_ => scheme.surfaceContainerHigh,
};
final foreground = switch (status) {
'critical' => scheme.onErrorContainer,
'pending' => scheme.onTertiaryContainer,
'promoted' => scheme.onSecondaryContainer,
'closed' => scheme.onPrimaryContainer,
_ => scheme.onSurfaceVariant,
};
final swatch = AppStatusColors.of(context).byKind(_ticketStatusKind(status));
final background = swatch.container;
final foreground = swatch.onContainer;
final label = status.replaceAll('_', ' ').toUpperCase();
return Material(
color: background,
borderRadius: BorderRadius.circular(20),
borderRadius: BorderRadius.circular(AppSurfaces.of(context).chipRadius),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Row(
@@ -787,6 +776,16 @@ class _StatusSummaryCard extends StatelessWidget {
}
}
/// Maps a ticket status to a semantic [StatusKind] so its color carries meaning
/// (red=critical, amber=pending, blue=promoted/escalated, green=closed).
StatusKind _ticketStatusKind(String status) => switch (status) {
'critical' => StatusKind.danger,
'pending' => StatusKind.warning,
'promoted' => StatusKind.info,
'closed' => StatusKind.success,
_ => StatusKind.neutral,
};
/// First 8 chars of the UUID, like a short git hash; the full id is in the
/// tooltip. Tickets have no human-friendly number.
String _shortId(String id) =>
@@ -813,21 +812,9 @@ class _StatusBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final background = switch (status) {
'critical' => scheme.errorContainer,
'pending' => scheme.tertiaryContainer,
'promoted' => scheme.secondaryContainer,
'closed' => scheme.primaryContainer,
_ => scheme.surfaceContainerHighest,
};
final foreground = switch (status) {
'critical' => scheme.onErrorContainer,
'pending' => scheme.onTertiaryContainer,
'promoted' => scheme.onSecondaryContainer,
'closed' => scheme.onPrimaryContainer,
_ => scheme.onSurfaceVariant,
};
final swatch = AppStatusColors.of(context).byKind(_ticketStatusKind(status));
final background = swatch.container;
final foreground = swatch.onContainer;
return Badge(
backgroundColor: background,
+26 -13
View File
@@ -12,6 +12,7 @@ import '../../providers/profile_provider.dart';
import '../../providers/whereabouts_provider.dart';
import '../../providers/workforce_provider.dart';
import '../../theme/app_surfaces.dart';
import '../../theme/app_status_colors.dart';
import '../../widgets/app_page_header.dart';
import '../../widgets/responsive_body.dart';
import '../../utils/app_time.dart';
@@ -232,8 +233,10 @@ class _WhereaboutsMap extends StatelessWidget {
polygons: [
Polygon(
points: points,
color: Colors.blue.withValues(alpha: 0.1),
borderColor: Colors.blue,
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.12),
borderColor: Theme.of(context).colorScheme.primary,
borderStrokeWidth: 2.0,
),
],
@@ -313,11 +316,20 @@ class _StaffLegendPanel extends StatelessWidget {
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
),
const Spacer(),
_RoleBadge(color: Colors.blue.shade700, label: 'Admin'),
_RoleBadge(
color: Theme.of(context).colorScheme.primary,
label: 'Admin',
),
const SizedBox(width: 12),
_RoleBadge(color: Colors.green.shade700, label: 'IT Staff'),
_RoleBadge(
color: Theme.of(context).colorScheme.secondary,
label: 'IT Staff',
),
const SizedBox(width: 12),
_RoleBadge(color: Colors.orange.shade700, label: 'Dispatcher'),
_RoleBadge(
color: Theme.of(context).colorScheme.tertiary,
label: 'Dispatcher',
),
],
),
),
@@ -416,6 +428,7 @@ class _StaffLegendTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final status = AppStatusColors.of(context);
final roleColor = _roleColor(profile.role, cs);
final hasPosition = position != null;
@@ -445,30 +458,30 @@ class _StaffLegendTile extends StatelessWidget {
? 'In premise (Tracking off) \u00b7 $timeLabel'
: 'Outside premise (Tracking off) \u00b7 $timeLabel';
statusColor = isStale
? Colors.grey
: (isInPremise ? Colors.green : Colors.grey);
? status.neutral.color
: (isInPremise ? status.success.color : status.neutral.color);
} else if (inferredInPremise) {
statusText = 'In premise (Checked in)';
statusColor = Colors.green;
statusColor = status.success.color;
} else {
statusText = 'Tracking off';
statusColor = Colors.grey;
statusColor = status.neutral.color;
}
} else if (isStale && hasPosition) {
final timeLabel = _timeAgo(position!.updatedAt);
statusText = isInPremise
? 'Last seen in premise \u00b7 $timeLabel'
: 'Last seen outside premise \u00b7 $timeLabel';
statusColor = Colors.grey;
statusColor = status.neutral.color;
} else if (isActive) {
statusText = 'In premise \u00b7 ${_timeAgo(position!.updatedAt)}';
statusColor = Colors.green;
statusColor = status.success.color;
} else if (hasPosition) {
statusText = 'Outside premise \u00b7 ${_timeAgo(position!.updatedAt)}';
statusColor = Colors.grey;
statusColor = status.neutral.color;
} else {
statusText = 'No location data';
statusColor = Colors.grey;
statusColor = status.neutral.color;
}
final IconData statusIcon;