import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/data_service.dart'; import 'package:provider/provider.dart'; class LegCard extends StatefulWidget { const LegCard({ super.key, required this.leg, this.showEditButton = true, this.showDate = true, }); final Leg leg; final bool showEditButton; final bool showDate; @override State createState() => _LegCardState(); } class _LegCardState extends State { bool _expanded = false; @override Widget build(BuildContext context) { final leg = widget.leg; final routeSegments = _parseRouteSegments(leg.route); final textTheme = Theme.of(context).textTheme; return Card( child: ExpansionTile( onExpansionChanged: (v) => setState(() => _expanded = v), tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), leading: const Icon(Icons.train), title: LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth > 520; final routeText = Text('${leg.start} → ${leg.end}'); final timeText = Text(_formatDateTime(leg.beginTime, includeDate: widget.showDate)); if (!isWide) { return routeText; } return Row( children: [ timeText, const SizedBox(width: 6), const Text('·'), const SizedBox(width: 6), Expanded(child: routeText), ], ); }, ), subtitle: LayoutBuilder( builder: (context, constraints) { final isWide = constraints.maxWidth > 520; final timeWidget = Text(_formatDateTime(leg.beginTime, includeDate: widget.showDate)); final tractionWrap = !_expanded && leg.locos.isNotEmpty ? Wrap( spacing: 8, runSpacing: 4, children: leg.locos.map((loco) { final iconColor = loco.powering ? Theme.of(context).colorScheme.primary : Theme.of(context).hintColor; final label = '${loco.locoClass} ${loco.number}'.trim(); return Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.train, size: 14, color: iconColor), const SizedBox(width: 4), Text( label.isEmpty ? 'Loco ${loco.id}' : label, style: textTheme.labelSmall, ), ], ); }).toList(), ) : null; final children = []; if (isWide) { if (tractionWrap != null) { children.add(tractionWrap); } } else { children.add(timeWidget); if (tractionWrap != null) { children.add(const SizedBox(height: 4)); children.add(tractionWrap); } } if (leg.headcode.isNotEmpty) { children.add( Text( 'Headcode: ${leg.headcode}', style: textTheme.labelSmall, ), ); } if (leg.network.isNotEmpty) { children.add( Text( leg.network, style: textTheme.labelSmall, ), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: children, ); }, ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '${leg.mileage.toStringAsFixed(1)} mi', style: textTheme.labelLarge?.copyWith( fontWeight: FontWeight.w700, ), ), if (leg.tripId != 0) ...[ const SizedBox(height: 2), Text( 'Trip #${leg.tripId}', style: textTheme.labelSmall, maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ], ), if (widget.showEditButton) ...[ const SizedBox(width: 8), IconButton( tooltip: 'Edit entry', icon: const Icon(Icons.edit), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), onPressed: () => context.push('/legs/edit/${leg.id}'), ), if (_expanded) ...[ const SizedBox(width: 4), IconButton( tooltip: 'Delete entry', icon: const Icon(Icons.delete_outline), color: Theme.of(context).colorScheme.error, visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, constraints: const BoxConstraints(minWidth: 32, minHeight: 32), onPressed: () => _confirmDelete(context, leg.id), ), ], ], ], ), children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (leg.notes.isNotEmpty) ...[ Text('Notes', style: textTheme.titleSmall), const SizedBox(height: 4), Text(leg.notes), const SizedBox(height: 12), ], if (leg.locos.isNotEmpty) ...[ Text('Locos', style: textTheme.titleSmall), const SizedBox(height: 6), Wrap( spacing: 8, runSpacing: 8, children: _buildLocoChips(context, leg), ), const SizedBox(height: 12), ], if (routeSegments.isNotEmpty) ...[ Text('Route', style: textTheme.titleSmall), const SizedBox(height: 6), _buildRouteList(routeSegments), ], ], ), ), ], ), ); } Future _confirmDelete(BuildContext context, int legId) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Delete entry?'), content: const Text('Are you sure you want to delete this entry?'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Cancel'), ), FilledButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Delete'), ), ], ), ); if (confirmed != true) return; final data = context.read(); final messenger = ScaffoldMessenger.of(context); try { await data.api.delete('/legs/delete?leg_id=$legId'); await data.refreshLegs(); messenger.showSnackBar(const SnackBar(content: Text('Entry deleted'))); } catch (e) { messenger.showSnackBar( SnackBar(content: Text('Failed to delete entry: $e')), ); } } String _formatDate(DateTime? date) { if (date == null) return ''; return '${date.year.toString().padLeft(4, '0')}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; } String _formatTime(DateTime date) { return '${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; } String _formatDateTime(DateTime date, {bool includeDate = true}) { final timeStr = _formatTime(date); if (!includeDate) return timeStr; final dateStr = _formatDate(date); return '$dateStr · $timeStr'; } List _buildLocoChips(BuildContext context, Leg leg) { final theme = Theme.of(context); final textTheme = theme.textTheme; return leg.locos .map( (loco) { final powering = loco.powering == true; final iconColor = powering ? theme.colorScheme.primary : theme.disabledColor; final labelStyle = powering ? null : textTheme.bodyMedium?.copyWith(color: theme.disabledColor); final background = powering ? theme.colorScheme.surfaceContainerHighest : theme.colorScheme.surfaceContainerLow; return Chip( label: Text( '${loco.locoClass} ${loco.number}', style: labelStyle, ), avatar: Icon( Icons.directions_railway, size: 16, color: iconColor, ), backgroundColor: background, ); }, ) .toList(); } Widget _buildRouteList(List segments) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: segments .map( (segment) => Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Row( children: [ const Icon(Icons.circle, size: 10), const SizedBox(width: 8), Expanded(child: Text(segment)), ], ), ), ) .toList(), ); } List _parseRouteSegments(String route) { final trimmed = route.trim(); if (trimmed.isEmpty) return []; try { final decoded = jsonDecode(trimmed); if (decoded is List) { return decoded.map((e) => e.toString()).toList(); } } catch (_) {} if (trimmed.startsWith('[') && trimmed.endsWith(']')) { try { final replaced = trimmed.replaceAll("'", '"'); final decoded = jsonDecode(replaced); if (decoded is List) { return decoded.map((e) => e.toString()).toList(); } } catch (_) {} } if (trimmed.contains('->')) { return trimmed .split('->') .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); } if (trimmed.contains(',')) { return trimmed .split(',') .map((e) => e.trim()) .where((e) => e.isNotEmpty) .toList(); } return [trimmed]; } }