Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

View File

@ -0,0 +1,210 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Custom app bar implementing clean, minimal header design
/// with contextual actions for the transit app
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final String title;
final List<Widget>? actions;
final Widget? leading;
final bool showBackButton;
final VoidCallback? onBackPressed;
final Color? backgroundColor;
final Color? foregroundColor;
final double elevation;
final bool centerTitle;
const CustomAppBar({
super.key,
required this.title,
this.actions,
this.leading,
this.showBackButton = true,
this.onBackPressed,
this.backgroundColor,
this.foregroundColor,
this.elevation = 0,
this.centerTitle = true,
});
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
void _handleBackPress(BuildContext context) {
// Haptic feedback for back navigation
HapticFeedback.lightImpact();
if (onBackPressed != null) {
onBackPressed!();
} else {
Navigator.of(context).pop();
}
}
Widget _buildLeading(BuildContext context) {
if (leading != null) {
return leading!;
}
if (showBackButton && Navigator.of(context).canPop()) {
return IconButton(
icon: const Icon(Icons.arrow_back_ios_new),
onPressed: () => _handleBackPress(context),
tooltip: 'Atrás',
);
}
return const SizedBox.shrink();
}
List<Widget> _buildActions(BuildContext context) {
final List<Widget> actionWidgets = [];
// Add custom actions if provided
if (actions != null) {
actionWidgets.addAll(actions!);
}
// Add contextual actions based on current route
final currentRoute = ModalRoute.of(context)?.settings.name;
switch (currentRoute) {
case '/map-screen':
actionWidgets.add(
IconButton(
icon: const Icon(Icons.my_location),
onPressed: () {
HapticFeedback.lightImpact();
// Handle location centering
},
tooltip: 'Mi ubicación',
),
);
actionWidgets.add(
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () {
HapticFeedback.lightImpact();
// Handle filter options
},
tooltip: 'Filtros',
),
);
break;
case '/schedules-screen':
actionWidgets.add(
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
HapticFeedback.lightImpact();
// Handle schedule refresh
},
tooltip: 'Actualizar',
),
);
break;
case '/coupons-screen':
actionWidgets.add(
IconButton(
icon: const Icon(Icons.qr_code_scanner),
onPressed: () {
HapticFeedback.lightImpact();
// Handle QR code scanning
},
tooltip: 'Escanear',
),
);
break;
}
return actionWidgets;
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AppBar(
title: Text(
title,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w500,
color: foregroundColor ?? theme.colorScheme.onSurface,
),
),
leading: _buildLeading(context),
actions: _buildActions(context),
backgroundColor: backgroundColor ?? theme.colorScheme.surface,
foregroundColor: foregroundColor ?? theme.colorScheme.onSurface,
elevation: elevation,
shadowColor: theme.shadowColor.withValues(alpha: 0.1),
surfaceTintColor: Colors.transparent,
centerTitle: centerTitle,
systemOverlayStyle: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: theme.brightness == Brightness.light
? Brightness.dark
: Brightness.light,
statusBarBrightness: theme.brightness,
),
);
}
}
/// Specialized app bar for map screen with location and filter controls
class CustomMapAppBar extends CustomAppBar {
const CustomMapAppBar({
super.key,
super.title = 'Mapa de Rutas',
super.showBackButton = false,
});
}
/// Specialized app bar for schedules screen with refresh functionality
class CustomSchedulesAppBar extends CustomAppBar {
const CustomSchedulesAppBar({
super.key,
super.title = 'Horarios',
});
}
/// Specialized app bar for coupons screen with QR scanner
class CustomCouponsAppBar extends CustomAppBar {
const CustomCouponsAppBar({
super.key,
super.title = 'Cupones',
});
}
/// Specialized app bar for bus stop details with contextual actions
class CustomBusStopAppBar extends CustomAppBar {
final String stopName;
const CustomBusStopAppBar({
super.key,
required this.stopName,
}) : super(title: stopName);
@override
List<Widget> _buildActions(BuildContext context) {
return [
IconButton(
icon: const Icon(Icons.favorite_border),
onPressed: () {
HapticFeedback.lightImpact();
// Handle adding to favorites
},
tooltip: 'Agregar a favoritos',
),
IconButton(
icon: const Icon(Icons.share),
onPressed: () {
HapticFeedback.lightImpact();
// Handle sharing bus stop
},
tooltip: 'Compartir',
),
...super._buildActions(context),
];
}
}

View File

@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Custom bottom navigation bar implementing adaptive tab persistence
/// with contextual badge indicators for the transit app
class CustomBottomBar extends StatefulWidget {
final int currentIndex;
final Function(int) onTap;
const CustomBottomBar({
super.key,
required this.currentIndex,
required this.onTap,
});
@override
State<CustomBottomBar> createState() => _CustomBottomBarState();
}
class _CustomBottomBarState extends State<CustomBottomBar> {
// Navigation items with routes and icons
final List<BottomNavigationBarItem> _navigationItems = [
const BottomNavigationBarItem(
icon: Icon(Icons.map_outlined),
activeIcon: Icon(Icons.map),
label: 'Mapa',
),
const BottomNavigationBarItem(
icon: Icon(Icons.schedule_outlined),
activeIcon: Icon(Icons.schedule),
label: 'Horarios',
),
const BottomNavigationBarItem(
icon: Icon(Icons.local_offer_outlined),
activeIcon: Icon(Icons.local_offer),
label: 'Cupones',
),
const BottomNavigationBarItem(
icon: Icon(Icons.local_taxi_outlined),
activeIcon: Icon(Icons.local_taxi),
label: 'Taxi',
),
];
final List<String> _routes = [
'/map-screen',
'/schedules-screen',
'/coupons-screen',
'/taxi-screen',
];
void _handleTap(int index) {
if (index != widget.currentIndex) {
// Haptic feedback for tab selection
HapticFeedback.lightImpact();
// Navigate to selected route
Navigator.pushNamed(context, _routes[index]);
// Call the onTap callback
widget.onTap(index);
}
}
Widget _buildNavigationItem(BottomNavigationBarItem item, int index) {
final bool isSelected = index == widget.currentIndex;
final theme = Theme.of(context);
return Expanded(
child: InkWell(
onTap: () => _handleTap(index),
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Icon with micro-interaction confirmation
AnimatedScale(
scale: isSelected ? 1.1 : 1.0,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: isSelected
? theme.colorScheme.secondary.withValues(alpha: 0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: _buildIconWithBadge(
isSelected ? item.activeIcon : item.icon,
index,
isSelected,
),
),
),
const SizedBox(height: 4),
// Label with smooth transition
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 200),
style: theme.textTheme.labelSmall!.copyWith(
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w400,
),
child: Text(item.label!),
),
],
),
),
),
);
}
Widget _buildIconWithBadge(Widget icon, int index, bool isSelected) {
final theme = Theme.of(context);
// Smart notification badges - contextual indicators
bool showBadge = false;
String? badgeText;
switch (index) {
case 0: // Map
// Show badge when there are nearby bus updates
showBadge = false; // Would be dynamic based on app state
break;
case 1: // Schedules
// Show badge when there are schedule changes
showBadge = false; // Would be dynamic based on app state
break;
case 2: // Coupons
// Show badge when there are new coupons available
showBadge = true; // Example: new coupons available
badgeText = '3';
break;
case 3: // Taxi
// Show badge for new taxi services or favorites
showBadge = false; // Would be dynamic based on app state
break;
}
if (!showBadge) {
return IconTheme(
data: IconThemeData(
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
size: 24,
),
child: icon,
);
}
return Badge(
label: badgeText != null ? Text(badgeText) : null,
backgroundColor: theme.colorScheme.error,
textColor: theme.colorScheme.onError,
smallSize: badgeText == null ? 8 : null,
child: IconTheme(
data: IconThemeData(
color: isSelected
? theme.colorScheme.primary
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
size: 24,
),
child: icon,
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
child: Container(
height: 72,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: List.generate(
_navigationItems.length,
(index) => _buildNavigationItem(_navigationItems[index], index),
),
),
),
),
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../core/app_export.dart';
// custom_error_widget.dart
class CustomErrorWidget extends StatelessWidget {
final FlutterErrorDetails? errorDetails;
final String? errorMessage;
const CustomErrorWidget({
Key? key,
this.errorDetails,
this.errorMessage,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFFAFAFA),
body: SafeArea(
child: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SvgPicture.asset(
'assets/images/sad_face.svg',
height: 42,
width: 42,
),
const SizedBox(height: 8),
Text(
"Something went wrong",
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w500,
color: Color(0xFF262626),
),
),
const SizedBox(height: 4),
SizedBox(
child: const Text(
'We encountered an unexpected error while processing your request.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
color: Color(0xFF525252), // neutral-600
),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
bool canBeBack = Navigator.canPop(context);
if (canBeBack) {
Navigator.of(context).pop();
} else {
Navigator.pushNamed(context, AppRoutes.initial);
}
},
icon:
const Icon(Icons.arrow_back, size: 18, color: Colors.white),
label: const Text('Back'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.lightTheme.primaryColor,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
],
),
),
)),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,182 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../core/app_export.dart';
extension ImageTypeExtension on String {
ImageType get imageType {
if (this.startsWith('http') || this.startsWith('https')) {
return ImageType.network;
} else if (this.endsWith('.svg')) {
return ImageType.svg;
} else if (this.startsWith('file: //')) {
return ImageType.file;
} else {
return ImageType.png;
}
}
}
enum ImageType { svg, png, network, file, unknown }
// ignore_for_file: must_be_immutable
class CustomImageWidget extends StatelessWidget {
CustomImageWidget({
this.imageUrl,
this.height,
this.width,
this.color,
this.fit,
this.alignment,
this.onTap,
this.radius,
this.margin,
this.border,
this.placeHolder = 'assets/images/no-image.jpg',
this.errorWidget,
this.semanticLabel,
});
///[imageUrl] is required parameter for showing image
final String? imageUrl;
final double? height;
final double? width;
final BoxFit? fit;
final String placeHolder;
final Color? color;
final Alignment? alignment;
final VoidCallback? onTap;
final BorderRadius? radius;
final EdgeInsetsGeometry? margin;
final BoxBorder? border;
/// Optional widget to show when the image fails to load.
/// If null, a default asset image is shown.
final Widget? errorWidget;
/// Semantic label for the image to improve accessibility
final String? semanticLabel;
@override
Widget build(BuildContext context) {
return alignment != null
? Align(alignment: alignment!, child: _buildWidget())
: _buildWidget();
}
Widget _buildWidget() {
return Padding(
padding: margin ?? EdgeInsets.zero,
child: InkWell(
onTap: onTap,
child: _buildCircleImage(),
),
);
}
///build the image with border radius
_buildCircleImage() {
if (radius != null) {
return ClipRRect(
borderRadius: radius ?? BorderRadius.zero,
child: _buildImageWithBorder(),
);
} else {
return _buildImageWithBorder();
}
}
///build the image with border and border radius style
_buildImageWithBorder() {
if (border != null) {
return Container(
decoration: BoxDecoration(
border: border,
borderRadius: radius,
),
child: _buildImageView(),
);
} else {
return _buildImageView();
}
}
Widget _buildImageView() {
if (imageUrl != null) {
switch (imageUrl!.imageType) {
case ImageType.svg:
return Container(
height: height,
width: width,
child: SvgPicture.asset(
imageUrl!,
height: height,
width: width,
fit: fit ?? BoxFit.contain,
colorFilter: this.color != null
? ColorFilter.mode(
this.color ?? Colors.transparent, BlendMode.srcIn)
: null,
semanticsLabel: semanticLabel,
),
);
case ImageType.file:
return Image.file(
File(imageUrl!),
height: height,
width: width,
fit: fit ?? BoxFit.cover,
color: color,
semanticLabel: semanticLabel,
);
case ImageType.network:
return CachedNetworkImage(
height: height,
width: width,
fit: fit,
imageUrl: imageUrl!,
color: color,
placeholder: (context, url) => Container(
height: 30,
width: 30,
child: LinearProgressIndicator(
color: Colors.grey.shade200,
backgroundColor: Colors.grey.shade100,
),
),
errorWidget: (context, url, error) =>
errorWidget ??
Image.asset(
placeHolder,
height: height,
width: width,
fit: fit ?? BoxFit.cover,
semanticLabel: semanticLabel,
),
);
case ImageType.png:
default:
return Image.asset(
imageUrl!,
height: height,
width: width,
fit: fit ?? BoxFit.cover,
color: color,
semanticLabel: semanticLabel,
);
}
}
return SizedBox();
}
}

View File

@ -0,0 +1,188 @@
import 'package:flutter/material.dart';
import '../services/supabase_service.dart';
class DebugBannerWidget extends StatefulWidget {
final String? lastError;
final int routeCount;
final bool isConnected;
final String connectionStatus;
const DebugBannerWidget({
super.key,
this.lastError,
this.routeCount = 0,
this.isConnected = false,
this.connectionStatus = 'Not checked',
});
@override
State<DebugBannerWidget> createState() => _DebugBannerWidgetState();
}
class _DebugBannerWidgetState extends State<DebugBannerWidget> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Positioned(
top: 16,
left: 16,
child: GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Container(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.8,
),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFFEE715), width: 2),
),
child: _isExpanded ? _buildExpandedView() : _buildCollapsedView(),
),
),
);
}
Widget _buildCollapsedView() {
return Container(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.bug_report,
color: widget.isConnected ? Colors.green : Colors.red,
size: 16,
),
const SizedBox(width: 4),
Text(
'DEBUG',
style: TextStyle(
color: widget.isConnected ? Colors.green : Colors.red,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 4),
const Icon(
Icons.keyboard_arrow_down,
color: Color(0xFFFEE715),
size: 16,
),
],
),
);
}
Widget _buildExpandedView() {
final truncatedUrl = SupabaseService.getTruncatedUrl();
final maskedKey = SupabaseService.getMaskedAnonKey();
return Container(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// Header
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.bug_report,
color: widget.isConnected ? Colors.green : Colors.red,
size: 16,
),
const SizedBox(width: 4),
const Text(
'DEBUG INFO',
style: TextStyle(
color: Color(0xFFFEE715),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
const Icon(
Icons.keyboard_arrow_up,
color: Color(0xFFFEE715),
size: 16,
),
],
),
const Divider(color: Color(0xFFFEE715), height: 16, thickness: 1),
// Connection Status
_buildDebugRow(
'Status',
widget.isConnected ? 'Connected ✓' : 'Connection Failed',
isError: !widget.isConnected,
),
// Supabase URL (first/last 8 chars)
_buildDebugRow(
'SUPABASE_URL',
truncatedUrl,
),
// Supabase Anon Key (first/last 6 chars, masked)
_buildDebugRow(
'SUPABASE_ANON_KEY',
maskedKey,
),
// Route Count
_buildDebugRow(
'Routes Count',
widget.isConnected ? '${widget.routeCount} routes' : 'N/A',
),
// Last Error
if (widget.lastError != null) ...[
const SizedBox(height: 8),
_buildDebugRow(
'Last Error',
widget.lastError!,
isError: true,
),
],
],
),
);
}
Widget _buildDebugRow(String label, String value, {bool isError = false}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label:',
style: const TextStyle(
color: Color(0xFFFEE715),
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 2),
Text(
value,
style: TextStyle(
color: isError ? Colors.red : Colors.white,
fontSize: 9,
fontFamily: 'monospace',
),
maxLines: isError ? 3 : 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}

View File

@ -0,0 +1,284 @@
import 'package:flutter/material.dart';
import '../services/app_state_service.dart';
class RouteSelectionBottomSheet extends StatefulWidget {
final String title;
final VoidCallback? onRouteChanged;
const RouteSelectionBottomSheet({
super.key,
this.title = 'Seleccionar Ruta',
this.onRouteChanged,
});
@override
State<RouteSelectionBottomSheet> createState() =>
_RouteSelectionBottomSheetState();
}
class _RouteSelectionBottomSheetState extends State<RouteSelectionBottomSheet> {
final AppStateService _appStateService = AppStateService();
bool _isRefreshing = false;
@override
void initState() {
super.initState();
_appStateService.addListener(_onStateChanged);
}
@override
void dispose() {
_appStateService.removeListener(_onStateChanged);
super.dispose();
}
void _onStateChanged() {
if (mounted) {
setState(() {});
}
}
Future<void> _onRouteSelected(String routeId) async {
try {
await _appStateService.selectRoute(routeId);
// Notify parent widget that route changed
widget.onRouteChanged?.call();
if (mounted) {
Navigator.pop(context);
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('Ruta cambiada: ${_appStateService.selectedRouteName}'),
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error seleccionando ruta: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
}
Future<void> _refreshRoutes() async {
setState(() {
_isRefreshing = true;
});
try {
await _appStateService.refreshRoutes();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Rutas actualizadas'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error actualizando rutas: $e'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
} finally {
if (mounted) {
setState(() {
_isRefreshing = false;
});
}
}
}
@override
Widget build(BuildContext context) {
final routes = _appStateService.allRoutes;
final selectedRouteId = _appStateService.selectedRouteId;
final isLoading = _appStateService.isLoadingRoutes || _isRefreshing;
final error = _appStateService.error;
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
// Header
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
Expanded(
child: Text(
widget.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF101820),
),
),
),
IconButton(
onPressed: _refreshRoutes,
icon: Icon(
Icons.refresh,
color: isLoading ? Colors.grey : const Color(0xFF101820),
),
),
],
),
),
const SizedBox(height: 16),
// Content
Flexible(
child: isLoading
? const Center(
child: Padding(
padding: EdgeInsets.all(40),
child: CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation<Color>(Color(0xFFFEE715)),
),
),
)
: error != null
? Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.error_outline,
color: Colors.red[600],
size: 48,
),
const SizedBox(height: 16),
Text(
error,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.red[600],
fontSize: 16,
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _refreshRoutes,
icon: const Icon(Icons.refresh),
label: const Text('Reintentar'),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFEE715),
foregroundColor: const Color(0xFF101820),
),
),
],
),
)
: routes.isEmpty
? const Padding(
padding: EdgeInsets.all(40),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.route_outlined,
color: Colors.grey,
size: 48,
),
SizedBox(height: 16),
Text(
'No hay rutas disponibles',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
],
),
)
: ListView.builder(
shrinkWrap: true,
itemCount: routes.length,
itemBuilder: (context, index) {
final route = routes[index];
final isSelected = selectedRouteId == route.id;
return ListTile(
leading: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: Color(int.parse(route.color
.replaceFirst('#', '0xFF'))),
shape: BoxShape.circle,
),
),
title: Text(
route.displayName,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
subtitle: route.direction.isNotEmpty
? Text(
route.direction,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
)
: null,
trailing: isSelected
? const Icon(
Icons.check_circle,
color: Color(0xFFFEE715),
)
: null,
onTap: () => _onRouteSelected(route.id),
);
},
),
),
const SizedBox(height: 16),
],
),
),
);
}
}