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