diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index 10a48086..925a1455 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -40,7 +40,9 @@ import 'package:skeletonizer/skeletonizer.dart'; import '../../theme/app_surfaces.dart'; import '../../theme/app_status_colors.dart'; import '../../widgets/mono_text.dart'; +import '../../widgets/app_metric_card.dart'; import '../../widgets/app_page_header.dart'; +import '../../widgets/app_section_header.dart'; import '../../utils/app_time.dart'; class DashboardMetrics { @@ -673,13 +675,13 @@ class _DashboardScreenState extends State { builder: (context, constraints) { final sections = [ const SizedBox(height: 16), - _sectionTitle(context, 'IT Staff Pulse'), + const AppSectionHeader('IT Staff Pulse'), const _StaffTable(), const SizedBox(height: 12), - _sectionTitle(context, 'Team Activity'), + const AppSectionHeader('Team Activity'), const _StaffActivityChart(), const SizedBox(height: 20), - _sectionTitle(context, 'Core Daily KPIs'), + const AppSectionHeader('Core Daily KPIs'), _cardGrid(context, [ _MetricCard( title: 'New tickets today', @@ -695,7 +697,7 @@ class _DashboardScreenState extends State { ), ]), const SizedBox(height: 20), - _sectionTitle(context, 'Task Flow'), + const AppSectionHeader('Task Flow'), _cardGrid(context, [ _MetricCard( title: 'Tasks created', @@ -713,7 +715,7 @@ class _DashboardScreenState extends State { ), ]), const SizedBox(height: 20), - _sectionTitle(context, 'TAT / Response'), + const AppSectionHeader('TAT / Response'), _cardGrid(context, [ _MetricCard( title: 'Avg response', @@ -776,33 +778,6 @@ class _DashboardScreenState extends State { ); } - Widget _sectionTitle(BuildContext context, String title) { - final cs = Theme.of(context).colorScheme; - return Padding( - padding: const EdgeInsets.only(bottom: 12), - 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, - ), - ), - ], - ), - ); - } Widget _cardGrid(BuildContext context, List cards) { return LayoutBuilder( @@ -921,7 +896,6 @@ class _MetricCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final cs = Theme.of(context).colorScheme; // Only watch the single string value for this card so unrelated metric // updates don't rebuild the whole card. This makes updates feel much // smoother and avoids full-page refreshes. @@ -934,51 +908,7 @@ 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: 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.toUpperCase(), - style: Theme.of(context).textTheme.labelMedium?.copyWith( - fontWeight: FontWeight.w600, - letterSpacing: 0.6, - color: cs.onSurfaceVariant, - ), - ), - const SizedBox(height: 12), - - // Animate only the metric text (not the whole card) for a - // subtle, smooth update. - AnimatedSwitcher( - duration: const Duration(milliseconds: 400), - switchInCurve: Curves.easeOutCubic, - switchOutCurve: Curves.easeInCubic, - transitionBuilder: (child, anim) => - FadeTransition(opacity: anim, child: child), - child: MonoText( - value, - key: ValueKey(value), - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, - color: cs.onSurface, - ), - ), - ), - ], - ), - ); + return AppMetricCard(title: title, value: value); } } diff --git a/lib/screens/it_service_requests/it_service_requests_list_screen.dart b/lib/screens/it_service_requests/it_service_requests_list_screen.dart index 6f19199a..3a008345 100644 --- a/lib/screens/it_service_requests/it_service_requests_list_screen.dart +++ b/lib/screens/it_service_requests/it_service_requests_list_screen.dart @@ -18,6 +18,7 @@ import '../../theme/m3_motion.dart'; import '../../utils/app_time.dart'; import '../../utils/snackbar.dart'; import '../../theme/app_status_colors.dart'; +import '../../widgets/app_status_summary.dart'; import '../../widgets/m3_card.dart'; import '../../widgets/mono_text.dart'; import '../../widgets/reconnect_overlay.dart'; @@ -535,8 +536,6 @@ 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; @@ -554,88 +553,38 @@ class _StatusSummaryRow extends StatelessWidget { .where((r) => r.status == ItServiceRequestStatus.completed) .length; - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - _SummaryChip( - label: 'Pending', - count: pending, - color: status.warning.color, - selected: selectedStatus == ItServiceRequestStatus.pendingApproval, - onTap: () => onStatusTap(ItServiceRequestStatus.pendingApproval), - ), - const SizedBox(width: 8), - _SummaryChip( - label: 'Scheduled', - count: scheduled, - color: status.info.color, - selected: selectedStatus == ItServiceRequestStatus.scheduled, - onTap: () => onStatusTap(ItServiceRequestStatus.scheduled), - ), - const SizedBox(width: 8), - _SummaryChip( - label: 'In Progress', - count: inProgress, - color: cs.secondary, - selected: selectedStatus == ItServiceRequestStatus.inProgress, - onTap: () => onStatusTap(ItServiceRequestStatus.inProgress), - ), - const SizedBox(width: 8), - _SummaryChip( - label: 'Completed', - count: completed, - color: status.success.color, - selected: selectedStatus == ItServiceRequestStatus.completed, - onTap: () => onStatusTap(ItServiceRequestStatus.completed), - ), - ], - ), - ); - } -} - -class _SummaryChip extends StatelessWidget { - const _SummaryChip({ - required this.label, - required this.count, - required this.color, - required this.selected, - required this.onTap, - }); - - final String label; - final int count; - final Color color; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return M3Card.filled( - onTap: onTap, - color: selected - ? color.withValues(alpha: 0.2) - : Theme.of(context).colorScheme.surfaceContainerHighest, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - count.toString(), - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - color: color, - ), - ), - const SizedBox(width: 8), - Text(label, style: Theme.of(context).textTheme.bodySmall), - ], + return AppStatusSummaryRow( + padding: const EdgeInsets.symmetric(vertical: 8), + items: [ + AppStatusSummaryItem( + label: 'Pending', + count: pending, + kind: StatusKind.warning, + selected: selectedStatus == ItServiceRequestStatus.pendingApproval, + onTap: () => onStatusTap(ItServiceRequestStatus.pendingApproval), ), - ), + AppStatusSummaryItem( + label: 'Scheduled', + count: scheduled, + kind: StatusKind.neutral, + selected: selectedStatus == ItServiceRequestStatus.scheduled, + onTap: () => onStatusTap(ItServiceRequestStatus.scheduled), + ), + AppStatusSummaryItem( + label: 'In Progress', + count: inProgress, + kind: StatusKind.info, + selected: selectedStatus == ItServiceRequestStatus.inProgress, + onTap: () => onStatusTap(ItServiceRequestStatus.inProgress), + ), + AppStatusSummaryItem( + label: 'Completed', + count: completed, + kind: StatusKind.success, + selected: selectedStatus == ItServiceRequestStatus.completed, + onTap: () => onStatusTap(ItServiceRequestStatus.completed), + ), + ], ); } } diff --git a/lib/screens/network_map/network_map_overview_screen.dart b/lib/screens/network_map/network_map_overview_screen.dart index e8174df9..b99dc646 100644 --- a/lib/screens/network_map/network_map_overview_screen.dart +++ b/lib/screens/network_map/network_map_overview_screen.dart @@ -7,6 +7,7 @@ import '../../providers/network_map/network_devices_provider.dart'; import '../../providers/network_map/network_sites_provider.dart'; import '../../providers/profile_provider.dart'; import '../../utils/snackbar.dart'; +import '../../widgets/app_metric_card.dart'; import '../../widgets/app_page_header.dart'; import '../../widgets/app_state_view.dart'; import '../../widgets/responsive_body.dart'; @@ -167,41 +168,24 @@ class _SummaryRow extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - Expanded(child: _SummaryCard(label: 'Sites', value: siteCount.toString(), icon: Icons.location_city_outlined)), - const SizedBox(width: 12), - Expanded(child: _SummaryCard(label: 'Devices', value: deviceCount.toString(), icon: Icons.devices_outlined)), - ], - ); - } -} - -class _SummaryCard extends StatelessWidget { - const _SummaryCard({required this.label, required this.value, required this.icon}); - final String label; - final String value; - final IconData icon; - - @override - Widget build(BuildContext context) { - final tt = Theme.of(context).textTheme; - final cs = Theme.of(context).colorScheme; - return Card( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Row( - children: [ - Icon(icon, color: cs.primary), - const SizedBox(width: 12), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: tt.labelSmall?.copyWith(color: cs.onSurfaceVariant)), - Text(value, style: tt.headlineSmall?.copyWith(fontWeight: FontWeight.w700)), - ], - ), - ], + Expanded( + child: AppMetricCard( + title: 'Sites', + value: siteCount.toString(), + icon: Icons.location_city_outlined, + animateValue: false, + ), ), - ), + const SizedBox(width: 12), + Expanded( + child: AppMetricCard( + title: 'Devices', + value: deviceCount.toString(), + icon: Icons.devices_outlined, + animateValue: false, + ), + ), + ], ); } } diff --git a/lib/screens/tasks/tasks_list_screen.dart b/lib/screens/tasks/tasks_list_screen.dart index 4a9222ba..2973f04b 100644 --- a/lib/screens/tasks/tasks_list_screen.dart +++ b/lib/screens/tasks/tasks_list_screen.dart @@ -30,6 +30,7 @@ import '../../widgets/tasq_adaptive_list.dart'; import '../../widgets/typing_dots.dart'; import '../../theme/app_surfaces.dart'; import '../../theme/app_status_colors.dart'; +import '../../widgets/app_status_summary.dart'; import '../../utils/snackbar.dart'; import '../../widgets/app_breakpoints.dart'; import '../../widgets/app_page_header.dart'; @@ -1329,63 +1330,15 @@ class _StatusSummaryRow extends StatelessWidget { final entries = counts.entries.toList() ..sort((a, b) => a.key.compareTo(b.key)); - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (int i = 0; i < entries.length; i++) ...[ - if (i > 0) const SizedBox(width: 8), - _StatusSummaryCard( - status: entries[i].key, - count: entries[i].value, - ), - ], - ], - ), - ); - } -} - -class _StatusSummaryCard extends StatelessWidget { - const _StatusSummaryCard({required this.status, required this.count}); - - final String status; - final int count; - - @override - Widget build(BuildContext context) { - 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(AppSurfaces.of(context).chipRadius), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - label, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: foreground, - fontWeight: FontWeight.w600, - letterSpacing: 0.4, - ), - ), - const SizedBox(width: 6), - Text( - count.toString(), - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: foreground, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), + return AppStatusSummaryRow( + items: [ + for (final e in entries) + AppStatusSummaryItem( + label: e.key.replaceAll('_', ' '), + count: e.value, + kind: _taskStatusKind(e.key), + ), + ], ); } } diff --git a/lib/screens/tickets/tickets_list_screen.dart b/lib/screens/tickets/tickets_list_screen.dart index 76bbb81e..c3e82b4c 100644 --- a/lib/screens/tickets/tickets_list_screen.dart +++ b/lib/screens/tickets/tickets_list_screen.dart @@ -28,6 +28,7 @@ import '../../utils/snackbar.dart'; import '../../widgets/app_breakpoints.dart'; import '../../widgets/app_page_header.dart'; import '../../widgets/app_state_view.dart'; +import '../../widgets/app_status_summary.dart'; import '../../widgets/sync_pending_badge.dart'; class TicketsListScreen extends ConsumerStatefulWidget { @@ -715,63 +716,15 @@ class _StatusSummaryRow extends StatelessWidget { final entries = counts.entries.toList() ..sort((a, b) => a.key.compareTo(b.key)); - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (int i = 0; i < entries.length; i++) ...[ - if (i > 0) const SizedBox(width: 8), - _StatusSummaryCard( - status: entries[i].key, - count: entries[i].value, - ), - ], - ], - ), - ); - } -} - -class _StatusSummaryCard extends StatelessWidget { - const _StatusSummaryCard({required this.status, required this.count}); - - final String status; - final int count; - - @override - Widget build(BuildContext context) { - 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(AppSurfaces.of(context).chipRadius), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - label, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: foreground, - fontWeight: FontWeight.w600, - letterSpacing: 0.4, - ), - ), - const SizedBox(width: 6), - Text( - count.toString(), - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: foreground, - fontWeight: FontWeight.w700, - ), - ), - ], - ), - ), + return AppStatusSummaryRow( + items: [ + for (final e in entries) + AppStatusSummaryItem( + label: e.key.replaceAll('_', ' '), + count: e.value, + kind: _ticketStatusKind(e.key), + ), + ], ); } } diff --git a/lib/screens/workforce/workforce_screen.dart b/lib/screens/workforce/workforce_screen.dart index a5180bbe..d63bfa87 100644 --- a/lib/screens/workforce/workforce_screen.dart +++ b/lib/screens/workforce/workforce_screen.dart @@ -16,6 +16,7 @@ import '../../providers/chat_provider.dart'; import '../../providers/ramadan_provider.dart'; import '../../providers/notifications_provider.dart'; import '../../widgets/app_page_header.dart'; +import '../../widgets/app_section_header.dart'; import '../../widgets/app_state_view.dart'; import '../../widgets/responsive_body.dart'; import '../../theme/app_surfaces.dart'; @@ -126,22 +127,15 @@ class _SchedulePanel extends ConsumerWidget { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: Row( - children: [ - Text( - 'Duty Schedules', - style: Theme.of( - context, - ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), - ), - const Spacer(), - FilterChip( - label: const Text('Show past'), - selected: showPast, - onSelected: (v) => - ref.read(showPastSchedulesProvider.notifier).state = v, - ), - ], + child: AppSectionHeader( + 'Duty Schedules', + padding: EdgeInsets.zero, + trailing: FilterChip( + label: const Text('Show past'), + selected: showPast, + onSelected: (v) => + ref.read(showPastSchedulesProvider.notifier).state = v, + ), ), ), const SizedBox(height: 4), diff --git a/lib/widgets/app_filter_bar.dart b/lib/widgets/app_filter_bar.dart new file mode 100644 index 00000000..44239131 --- /dev/null +++ b/lib/widgets/app_filter_bar.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +/// Standardized filter-bar chrome for list screens. +/// +/// Wraps a screen's filter controls (search field, dropdowns, date-range +/// button, etc.) in a consistent, responsive [Wrap] so every list screen's +/// filter row shares the same spacing and wrap behavior. Callers keep building +/// their own controls; sizing individual controls with a [SizedBox] width is +/// still fine — this only standardizes the layout around them. +class AppFilterBar extends StatelessWidget { + const AppFilterBar({ + super.key, + required this.children, + this.padding = const EdgeInsets.symmetric(vertical: 4), + this.spacing = 12, + this.runSpacing = 12, + }); + + final List children; + final EdgeInsetsGeometry padding; + final double spacing; + final double runSpacing; + + @override + Widget build(BuildContext context) { + if (children.isEmpty) return const SizedBox.shrink(); + return Padding( + padding: padding, + child: Wrap( + spacing: spacing, + runSpacing: runSpacing, + crossAxisAlignment: WrapCrossAlignment.center, + children: children, + ), + ); + } +} diff --git a/lib/widgets/app_metric_card.dart b/lib/widgets/app_metric_card.dart new file mode 100644 index 00000000..ec2d7091 --- /dev/null +++ b/lib/widgets/app_metric_card.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; + +import '../theme/app_surfaces.dart'; +import 'mono_text.dart'; + +/// Cyber/IT metric / stat card — an uppercase telemetry label above a large +/// monospace value, on a flat tonal surface with a hairline outline. +/// +/// The single shared style for KPI/stat tiles (unifies the Dashboard KPI card, +/// the Network Map "Sites/Devices" cards, and the Reports conversion tile). +/// Purely presentational: callers pass already-resolved [title]/[value] strings. +class AppMetricCard extends StatelessWidget { + const AppMetricCard({ + super.key, + required this.title, + required this.value, + this.icon, + this.onTap, + this.trailing, + this.animateValue = true, + }); + + final String title; + final String value; + + /// Optional leading accent glyph. + final IconData? icon; + + final VoidCallback? onTap; + + /// Optional trailing widget (e.g. a chevron for drill-down cards). + final Widget? trailing; + + /// Cross-fade the value when it changes (nice for live-updating metrics). + final bool animateValue; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final tt = Theme.of(context).textTheme; + final isDark = cs.brightness == Brightness.dark; + final radius = AppSurfaces.of(context).cardRadius; + + final valueWidget = MonoText( + value, + key: ValueKey(value), + style: tt.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + color: cs.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + + final content = Padding( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + if (icon != null) ...[ + Icon(icon, size: 22, color: cs.primary), + const SizedBox(width: 14), + ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + title.toUpperCase(), + style: tt.labelMedium?.copyWith( + fontWeight: FontWeight.w600, + letterSpacing: 0.6, + color: cs.onSurfaceVariant, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 10), + if (animateValue) + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + switchInCurve: Curves.easeOutCubic, + switchOutCurve: Curves.easeInCubic, + transitionBuilder: (child, anim) => + FadeTransition(opacity: anim, child: child), + child: valueWidget, + ) + else + valueWidget, + ], + ), + ), + if (trailing != null) ...[const SizedBox(width: 8), trailing!], + ], + ), + ); + + final decorated = AnimatedContainer( + duration: const Duration(milliseconds: 400), + curve: Curves.easeOutCubic, + decoration: BoxDecoration( + color: isDark ? cs.surfaceContainer : cs.surfaceContainerLowest, + borderRadius: BorderRadius.circular(radius), + border: Border.all(color: cs.outlineVariant), + ), + child: content, + ); + + if (onTap == null) return decorated; + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(radius), + child: decorated, + ), + ); + } +} diff --git a/lib/widgets/app_section_header.dart b/lib/widgets/app_section_header.dart new file mode 100644 index 00000000..7388a468 --- /dev/null +++ b/lib/widgets/app_section_header.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +/// Cyber/IT section header — a short accent bar + an uppercase, letter-spaced +/// "telemetry" label, with optional trailing actions. +/// +/// This is the single shared treatment for in-page section titles (promoted +/// from the Dashboard's original `_sectionTitle`). Use it instead of ad-hoc +/// bold `Text` sub-headers so every screen's sections read the same way. +class AppSectionHeader extends StatelessWidget { + const AppSectionHeader( + this.title, { + super.key, + this.trailing, + this.padding = const EdgeInsets.only(bottom: 12), + }); + + final String title; + + /// Optional trailing widget (e.g. a "Show past" button or filter chip), + /// pinned to the end of the header row. + final Widget? trailing; + + final EdgeInsetsGeometry padding; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Padding( + padding: padding, + child: Row( + children: [ + Container( + width: 3, + height: 16, + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + title.toUpperCase(), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.8, + color: cs.onSurface, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ?trailing, + ], + ), + ); + } +} diff --git a/lib/widgets/app_status_summary.dart b/lib/widgets/app_status_summary.dart new file mode 100644 index 00000000..d2ae73be --- /dev/null +++ b/lib/widgets/app_status_summary.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; + +import '../theme/app_status_colors.dart'; +import '../theme/app_surfaces.dart'; + +/// One entry in an [AppStatusSummaryRow]. +class AppStatusSummaryItem { + const AppStatusSummaryItem({ + required this.label, + required this.count, + required this.kind, + this.selected = false, + this.onTap, + }); + + final String label; + final int count; + final StatusKind kind; + + /// Emphasized (filled) when true — used for the active filter. + final bool selected; + + /// When set, the chip is tappable (e.g. to filter the list by this status). + final VoidCallback? onTap; +} + +/// A horizontally-scrollable row of status-count chips — the single shared +/// "count-by-status" summary used above entity lists (unifies the separate +/// tickets / tasks / ISR summary-chip implementations). +class AppStatusSummaryRow extends StatelessWidget { + const AppStatusSummaryRow({ + super.key, + required this.items, + this.padding = EdgeInsets.zero, + }); + + final List items; + final EdgeInsetsGeometry padding; + + @override + Widget build(BuildContext context) { + if (items.isEmpty) return const SizedBox.shrink(); + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: padding, + child: Row( + children: [ + for (var i = 0; i < items.length; i++) ...[ + if (i > 0) const SizedBox(width: 8), + AppStatusSummaryChip(item: items[i]), + ], + ], + ), + ); + } +} + +/// A single status-count chip: a status dot + bold count + label. Filled with +/// the status tone when [AppStatusSummaryItem.selected], subtle otherwise. +class AppStatusSummaryChip extends StatelessWidget { + const AppStatusSummaryChip({super.key, required this.item}); + + final AppStatusSummaryItem item; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final tt = Theme.of(context).textTheme; + final swatch = AppStatusColors.of(context).byKind(item.kind); + final radius = AppSurfaces.of(context).chipRadius; + + final bg = item.selected ? swatch.container : cs.surfaceContainerHighest; + final labelColor = + item.selected ? swatch.onContainer : cs.onSurfaceVariant; + final countColor = item.selected ? swatch.onContainer : cs.onSurface; + + final chip = AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(radius), + border: Border.all( + color: item.selected ? Colors.transparent : cs.outlineVariant, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: swatch.color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Text( + item.count.toString(), + style: tt.labelLarge?.copyWith( + color: countColor, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(width: 6), + Text( + item.label.toUpperCase(), + style: tt.labelSmall?.copyWith( + color: labelColor, + fontWeight: FontWeight.w600, + letterSpacing: 0.4, + ), + ), + ], + ), + ); + + if (item.onTap == null) return chip; + return InkWell( + onTap: item.onTap, + borderRadius: BorderRadius.circular(radius), + child: chip, + ); + } +}