import 'package:flutter/material.dart'; import 'package:mileograph_flutter/objects/objects.dart'; class TractionCard extends StatelessWidget { const TractionCard({ super.key, required this.loco, required this.selectionMode, required this.isSelected, required this.onShowInfo, required this.onOpenTimeline, this.onOpenLegs, this.onToggleSelect, }); final LocoSummary loco; final bool selectionMode; final bool isSelected; final VoidCallback onShowInfo; final VoidCallback onOpenTimeline; final VoidCallback? onOpenLegs; final VoidCallback? onToggleSelect; @override Widget build(BuildContext context) { final status = loco.status ?? 'Unknown'; final operatorName = loco.operator ?? ''; final domain = loco.domain ?? ''; final hasMileageOrTrips = _hasMileageOrTrips(loco); final statusColors = _statusChipColors(context, status); return Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( loco.number, style: Theme.of(context).textTheme.headlineSmall ?.copyWith(fontWeight: FontWeight.w800), ), if (hasMileageOrTrips) Padding( padding: const EdgeInsets.only(left: 6.0), child: Icon( Icons.check_circle, size: 18, color: Colors.green.shade600, ), ), ], ), Text( loco.locoClass, style: Theme.of(context).textTheme.labelMedium, ), if ((loco.name ?? '').isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2.0), child: Text( loco.name ?? '', style: Theme.of(context).textTheme.bodyMedium ?.copyWith(fontStyle: FontStyle.italic), ), ), ], ), Chip( label: Text(status), backgroundColor: statusColors.$1, labelStyle: TextStyle(color: statusColors.$2), ), ], ), const SizedBox(height: 8), LayoutBuilder( builder: (context, constraints) { final isNarrow = constraints.maxWidth < 520; final buttons = [ TextButton.icon( onPressed: onShowInfo, icon: const Icon(Icons.info_outline), label: const Text('Details'), ), TextButton.icon( onPressed: onOpenTimeline, icon: const Icon(Icons.timeline), label: const Text('Timeline'), ), if (hasMileageOrTrips && onOpenLegs != null) TextButton.icon( onPressed: onOpenLegs, icon: const Icon(Icons.view_list), label: const Text('Legs'), ), ]; final addButton = selectionMode && onToggleSelect != null ? TextButton.icon( onPressed: onToggleSelect, icon: Icon( isSelected ? Icons.remove_circle_outline : Icons.add_circle_outline, ), label: Text(isSelected ? 'Remove' : 'Add to entry'), ) : null; if (isNarrow) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 4, children: buttons, ), if (addButton != null) ...[ const SizedBox(height: 6), addButton, ], ], ); } return Row( children: [ ...buttons.expand((btn) sync* { yield btn; yield const SizedBox(width: 8); }).take(buttons.length * 2 - 1), const Spacer(), if (addButton != null) addButton, ], ); }, ), Wrap( spacing: 8, runSpacing: 4, children: [ _statPill( context, label: 'Miles', value: _formatNumber(loco.mileage), ), _statPill( context, label: 'Trips', value: (loco.trips ?? loco.journeys ?? 0).toString(), ), if (operatorName.isNotEmpty) _statPill(context, label: 'Operator', value: operatorName), if (domain.isNotEmpty) _statPill(context, label: 'Domain', value: domain), ], ), ], ), ), ); } Widget _statPill( BuildContext context, { required String label, required String value, }) { return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(10), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text('$label: ', style: Theme.of(context).textTheme.labelSmall), Text( value, style: Theme.of( context, ).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700), ), ], ), ); } } Future showTractionDetails( BuildContext context, LocoSummary loco, ) async { final hasMileageOrTrips = _hasMileageOrTrips(loco); await showModalBottomSheet( context: context, isScrollControlled: true, builder: (ctx) { return DraggableScrollableSheet( expand: false, maxChildSize: 0.9, initialChildSize: 0.65, builder: (_, controller) { return Padding( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(ctx).pop(), ), const SizedBox(width: 8), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( loco.number, style: Theme.of(context).textTheme.titleLarge ?.copyWith(fontWeight: FontWeight.w800), ), if (hasMileageOrTrips) Padding( padding: const EdgeInsets.only(left: 6.0), child: Icon( Icons.check_circle, size: 18, color: Colors.green.shade600, ), ), ], ), Text( loco.locoClass, style: Theme.of(context).textTheme.labelMedium, ), ], ), ], ), if ((loco.name ?? '').isNotEmpty) Padding( padding: const EdgeInsets.only(left: 52.0, bottom: 12), child: Text( loco.name ?? '', style: Theme.of(context).textTheme.bodyMedium, ), ), const SizedBox(height: 4), Expanded( child: ListView( controller: controller, children: [ _detailRow(context, 'Status', loco.status ?? 'Unknown'), _detailRow(context, 'Operator', loco.operator ?? ''), _detailRow(context, 'Domain', loco.domain ?? ''), _detailRow(context, 'Owner', loco.owner ?? ''), _detailRow(context, 'Livery', loco.livery ?? ''), _detailRow(context, 'Location', loco.location ?? ''), _detailRow( context, 'Mileage', _formatNumber(loco.mileage ?? 0), ), _detailRow( context, 'Trips', (loco.trips ?? loco.journeys ?? 0).toString(), ), _detailRow(context, 'EVN', loco.evn ?? ''), if (loco.notes != null && loco.notes!.isNotEmpty) _detailRow(context, 'Notes', loco.notes!), ], ), ), ], ), ); }, ); }, ); } Widget _detailRow(BuildContext context, String label, String value) { if (value.isEmpty) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.symmetric(vertical: 6.0), child: Row( children: [ SizedBox( width: 110, child: Text( label, style: Theme.of(context) .textTheme .labelMedium ?.copyWith(fontWeight: FontWeight.w600), ), ), Expanded( child: Text(value, style: Theme.of(context).textTheme.bodyMedium), ), ], ), ); } (Color, Color) _statusChipColors(BuildContext context, String status) { final scheme = Theme.of(context).colorScheme; final isDark = scheme.brightness == Brightness.dark; Color blend( Color base, { double bgOpacity = 0.18, double fgOpacity = 0.82, }) { final bg = Color.alphaBlend( base.withValues(alpha: isDark ? bgOpacity + 0.07 : bgOpacity), scheme.surface, ); final fg = Color.alphaBlend( base.withValues(alpha: isDark ? fgOpacity : fgOpacity * 0.8), scheme.onSurface, ); return Color.lerp(bg, fg, 0.0) ?? bg; } Color background; Color foreground; final key = status.toLowerCase(); if (key.contains('scrap')) { background = blend(Colors.red); foreground = Colors.red.shade200.withValues(alpha: isDark ? 0.85 : 0.9); } else if (key.contains('active')) { background = blend(scheme.primary); foreground = scheme.primary.withValues(alpha: isDark ? 0.9 : 0.8); } else if (key.contains('withdrawn')) { background = blend(Colors.amber); foreground = Colors.amber.shade800.withValues(alpha: isDark ? 0.9 : 0.8); } else if (key.contains('stored') || key.contains('unknown')) { background = blend(Colors.grey); foreground = Colors.grey.shade700.withValues(alpha: isDark ? 0.85 : 0.75); } else { background = scheme.surfaceContainerHighest; foreground = scheme.onSurface; } return (background, foreground); } bool _hasMileageOrTrips(LocoSummary loco) { final mileage = loco.mileage ?? 0; final trips = loco.trips ?? loco.journeys ?? 0; return mileage > 0 || trips > 0; } String _formatNumber(double? value) { if (value == null) return '0'; return value.toStringAsFixed(1); }