9f764150a2
Extract four shared layout widgets from the best existing implementations and adopt them across list/overview screens, deleting duplicated inline code. Layout/consistency only — no behavior change. New primitives (lib/widgets/): - app_section_header.dart — AppSectionHeader (azure accent bar + uppercase telemetry label; promoted from the dashboard's _sectionTitle). - app_metric_card.dart — AppMetricCard (uppercase label + mono value on a flat hairline surface; unifies KPI/stat tiles). - app_status_summary.dart — AppStatusSummaryRow/Item/Chip (dot + count + label, selectable; semantic AppStatusColors). - app_filter_bar.dart — AppFilterBar (responsive filter-row chrome). Adopted / de-duplicated: - Dashboard: _MetricCard now wraps AppMetricCard; section titles use AppSectionHeader (removed local _sectionTitle). - Tickets / Tasks / ISR: three separate summary-chip implementations (_StatusSummaryCard x2, _SummaryChip) collapsed into AppStatusSummaryRow. - Network Map overview: Sites/Devices tiles -> AppMetricCard (removed local _SummaryCard). - Workforce: "Duty Schedules" header -> AppSectionHeader with trailing chip. Verified: flutter analyze lib clean; flutter build web green; visual QA (tickets + ISR summary chips) in dark. Co-Authored-By: claude-flow <ruv@ruv.net>
127 lines
3.6 KiB
Dart
127 lines
3.6 KiB
Dart
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<AppStatusSummaryItem> 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,
|
|
);
|
|
}
|
|
}
|