Files
SIB/old/lib/presentation/schedules_screen/schedules_screen.dart

633 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../core/app_export.dart';
import '../../models/route_model.dart';
import '../../services/app_state_service.dart';
import '../../services/transportation_service.dart';
import '../../widgets/custom_app_bar.dart';
import '../../widgets/custom_bottom_bar.dart';
import '../../widgets/route_selection_bottom_sheet.dart';
import './widgets/empty_state_widget.dart';
import './widgets/notification_badge_widget.dart';
import './widgets/route_selection_card.dart';
import './widgets/schedule_card.dart';
import './widgets/search_bar_widget.dart';
class SchedulesScreen extends StatefulWidget {
const SchedulesScreen({super.key});
@override
State<SchedulesScreen> createState() => _SchedulesScreenState();
}
class _SchedulesScreenState extends State<SchedulesScreen>
with TickerProviderStateMixin {
int _currentBottomIndex = 1; // Schedules tab
String _searchQuery = '';
final ScrollController _scrollController = ScrollController();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
GlobalKey<RefreshIndicatorState>();
// Animation controllers
late AnimationController _fadeAnimationController;
late Animation<double> _fadeAnimation;
// Service and data
final TransportationService _transportationService = TransportationService();
final AppStateService _appStateService = AppStateService();
List<RouteModel> _routes = [];
List<Map<String, dynamic>> _schedules = [];
bool _isLoadingSchedules = false;
String _currentScheduleType = 'weekday';
// Notification state
final Map<String, bool> _notificationStates = {};
int _activeNotifications = 0;
@override
void initState() {
super.initState();
_initializeAnimations();
_appStateService.addListener(_onGlobalStateChanged);
_loadInitialData();
_determineCurrentScheduleType();
}
@override
void dispose() {
_appStateService.removeListener(_onGlobalStateChanged);
_fadeAnimationController.dispose();
_scrollController.dispose();
super.dispose();
}
void _initializeAnimations() {
_fadeAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _fadeAnimationController,
curve: Curves.easeInOut,
));
}
void _onGlobalStateChanged() {
// React to global route selection changes
if (mounted) {
_loadSchedulesForSelectedRoute();
}
}
void _determineCurrentScheduleType() {
// Use Panama timezone as specified in requirements
final panamaNow =
DateTime.now().toUtc().add(Duration(hours: -5)); // Panama is UTC-5
final dayOfWeek = panamaNow.weekday;
// Monday = 1, Sunday = 7
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
_currentScheduleType = 'weekday';
} else if (dayOfWeek == 6) {
_currentScheduleType = 'saturday';
} else {
_currentScheduleType = 'sunday';
}
}
Future<void> _loadInitialData() async {
// Check if we have routes loaded globally
if (_appStateService.allRoutes.isEmpty &&
!_appStateService.isLoadingRoutes) {
await _appStateService.loadRoutes();
}
// Show toast if no routes found
if (_appStateService.allRoutes.isEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_appStateService.showNoRoutesToast(context);
});
}
// Update local routes reference
setState(() {
_routes = _appStateService.allRoutes;
});
// Load schedules for currently selected route
await _loadSchedulesForSelectedRoute();
}
Future<void> _loadSchedulesForSelectedRoute() async {
final selectedRouteId = _appStateService.selectedRouteId;
if (selectedRouteId == null) {
setState(() {
_schedules = [];
_isLoadingSchedules = false;
});
return;
}
setState(() {
_isLoadingSchedules = true;
});
try {
final schedules =
await _transportationService.getRouteTimetablesByScheduleType(
selectedRouteId,
_currentScheduleType,
);
setState(() {
_schedules = schedules;
_isLoadingSchedules = false;
});
_fadeAnimationController.forward();
} catch (e) {
setState(() {
_isLoadingSchedules = false;
_schedules = [];
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error loading schedules: ${e.toString()}'),
backgroundColor: Colors.red,
duration: const Duration(seconds: 3),
),
);
}
}
}
List<RouteModel> get _filteredRoutes {
if (_searchQuery.isEmpty) return _routes;
return _routes.where((route) {
final name = route.name.toLowerCase();
final query = _searchQuery.toLowerCase();
return name.contains(query);
}).toList();
}
List<Map<String, dynamic>> get _filteredSchedules {
if (_searchQuery.isEmpty) return _schedules;
return _schedules.where((schedule) {
final departureTime =
(schedule['departure_time'] as String).toLowerCase();
final query = _searchQuery.toLowerCase();
return departureTime.contains(query);
}).toList();
}
void _onBottomNavTap(int index) {
setState(() {
_currentBottomIndex = index;
});
}
Future<void> _onRouteSelected(String routeId) async {
await _appStateService.selectRoute(routeId);
setState(() {
_searchQuery = '';
});
}
void _onSearchChanged(String query) {
setState(() {
_searchQuery = query;
});
}
void _onSearchClear() {
setState(() {
_searchQuery = '';
});
}
void _toggleNotification(String scheduleId) {
setState(() {
final currentState = _notificationStates[scheduleId] ?? false;
_notificationStates[scheduleId] = !currentState;
if (!currentState) {
_activeNotifications++;
} else {
_activeNotifications--;
}
});
}
Future<void> _onRefresh() async {
await _appStateService.refreshRoutes();
setState(() {
_routes = _appStateService.allRoutes;
});
await _loadSchedulesForSelectedRoute();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Horarios actualizados'),
backgroundColor: AppTheme.lightTheme.colorScheme.tertiary,
duration: const Duration(seconds: 2),
),
);
}
}
void _showRouteSelector() {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => RouteSelectionBottomSheet(
title: 'Seleccionar Ruta',
onRouteChanged: () {
// The global state listener will handle the reload
},
),
);
}
void _showScheduleContextMenu(String scheduleId, String departureTime) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 12.w,
height: 0.5.h,
margin: EdgeInsets.symmetric(vertical: 2.h),
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.outline,
borderRadius: BorderRadius.circular(2),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: 4.w),
child: Text(
'Opciones para $departureTime',
style: AppTheme.lightTheme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
SizedBox(height: 2.h),
_buildContextMenuItem(
icon: 'alarm_add',
title: 'Configurar Recordatorio',
onTap: () {
Navigator.pop(context);
_showReminderDialog(scheduleId, departureTime);
},
),
_buildContextMenuItem(
icon: 'share',
title: 'Compartir Horario',
onTap: () {
Navigator.pop(context);
_shareSchedule(departureTime);
},
),
_buildContextMenuItem(
icon: 'report_problem',
title: 'Reportar Problema',
onTap: () {
Navigator.pop(context);
_reportIssue(scheduleId);
},
),
SizedBox(height: 2.h),
],
),
),
),
);
}
Widget _buildContextMenuItem({
required String icon,
required String title,
required VoidCallback onTap,
}) {
return ListTile(
leading: CustomIconWidget(
iconName: icon,
color: AppTheme.lightTheme.colorScheme.onSurface,
size: 24,
),
title: Text(
title,
style: AppTheme.lightTheme.textTheme.bodyLarge,
),
onTap: onTap,
);
}
void _showReminderDialog(String scheduleId, String departureTime) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Configurar Recordatorio'),
content: Text(
'¿Deseas recibir una notificación antes de la salida de las $departureTime?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancelar'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_toggleNotification(scheduleId);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Recordatorio configurado'),
duration: Duration(seconds: 2),
),
);
},
child: const Text('Confirmar'),
),
],
),
);
}
void _shareSchedule(String departureTime) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Compartiendo horario de $departureTime'),
duration: const Duration(seconds: 2),
),
);
}
void _reportIssue(String scheduleId) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Reporte enviado. Gracias por tu feedback.'),
duration: Duration(seconds: 2),
),
);
}
String _getScheduleTypeDisplayName() {
switch (_currentScheduleType) {
case 'weekday':
return 'Lunes a Viernes';
case 'saturday':
return 'Sábado';
case 'sunday':
return 'Domingo';
default:
return 'Horario';
}
}
Widget _buildRouteSelectionView() {
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomIconWidget(
iconName: 'schedule',
color: AppTheme.lightTheme.colorScheme.secondary,
size: 20,
),
SizedBox(width: 2.w),
Text(
'Horarios para ${_getScheduleTypeDisplayName()}',
style: AppTheme.lightTheme.textTheme.bodyMedium?.copyWith(
color: AppTheme.lightTheme.colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w500,
),
),
],
),
),
SearchBarWidget(
hintText: 'Buscar rutas...',
onChanged: _onSearchChanged,
onClear: _onSearchClear,
),
Expanded(
child: _appStateService.isLoadingRoutes
? const Center(child: CircularProgressIndicator())
: _filteredRoutes.isEmpty
? const EmptyStateWidget(
title: 'No se encontraron rutas',
subtitle: 'Intenta con otros términos de búsqueda',
)
: ListView.builder(
controller: _scrollController,
itemCount: _filteredRoutes.length,
itemBuilder: (context, index) {
final route = _filteredRoutes[index];
return RouteSelectionCard(
routeName: route.displayName,
duration: route.direction,
onTap: () => _onRouteSelected(route.id),
);
},
),
),
],
);
}
Widget _buildScheduleView() {
final selectedRoute = _appStateService.getSelectedRoute();
final selectedRouteName = _appStateService.selectedRouteName;
if (selectedRoute == null || selectedRouteName == null) {
return const Center(
child: Text('No route selected'),
);
}
return Column(
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.surface,
border: Border(
bottom: BorderSide(
color: AppTheme.lightTheme.colorScheme.outline,
width: 1,
),
),
),
child: Row(
children: [
GestureDetector(
onTap: () {
_appStateService.clearSelectedRoute();
setState(() {
_searchQuery = '';
_schedules.clear();
});
},
child: Container(
padding: EdgeInsets.all(2.w),
decoration: BoxDecoration(
color: AppTheme.lightTheme.colorScheme.secondary
.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: CustomIconWidget(
iconName: 'arrow_back',
color: AppTheme.lightTheme.colorScheme.primary,
size: 20,
),
),
),
SizedBox(width: 3.w),
Expanded(
child: GestureDetector(
onTap: _showRouteSelector,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
selectedRouteName,
style:
AppTheme.lightTheme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
Text(
'${_getScheduleTypeDisplayName()}${selectedRoute.direction}',
style:
AppTheme.lightTheme.textTheme.bodySmall?.copyWith(
color:
AppTheme.lightTheme.colorScheme.onSurfaceVariant,
),
),
],
),
),
),
],
),
),
SearchBarWidget(
hintText: 'Buscar horarios...',
onChanged: _onSearchChanged,
onClear: _onSearchClear,
),
Expanded(
child: RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: _onRefresh,
color: AppTheme.lightTheme.colorScheme.secondary,
child: _isLoadingSchedules
? const Center(child: CircularProgressIndicator())
: _filteredSchedules.isEmpty
? const EmptyStateWidget(
title: 'No hay horarios disponibles',
subtitle: 'Intenta actualizar o selecciona otra ruta',
)
: FadeTransition(
opacity: _fadeAnimation,
child: ListView.builder(
controller: _scrollController,
itemCount: _filteredSchedules.length,
itemBuilder: (context, index) {
final schedule = _filteredSchedules[index];
final scheduleId = schedule['id'].toString();
final departureTime =
schedule['departure_time'] as String;
final timeParts = departureTime.split(':');
final hour = timeParts[0];
final minute = timeParts[1];
final formattedTime = '$hour:$minute';
return ScheduleCard(
departureTime: formattedTime,
duration: selectedRoute.direction,
arrivalTime: '',
isNotificationEnabled:
_notificationStates[scheduleId] ?? false,
onNotificationToggle: () =>
_toggleNotification(scheduleId),
onLongPress: () => _showScheduleContextMenu(
scheduleId,
formattedTime,
),
);
},
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final hasSelectedRoute = _appStateService.hasSelectedRoute;
return Scaffold(
backgroundColor: AppTheme.lightTheme.scaffoldBackgroundColor,
appBar: CustomAppBar(
title: 'Horarios',
actions: [
NotificationBadgeWidget(
count: _activeNotifications,
child: IconButton(
icon: CustomIconWidget(
iconName: 'notifications',
color: AppTheme.lightTheme.colorScheme.onSurface,
size: 24,
),
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Tienes $_activeNotifications notificaciones activas'),
duration: const Duration(seconds: 2),
),
);
},
),
),
],
),
body: SafeArea(
child: !hasSelectedRoute
? _buildRouteSelectionView()
: _buildScheduleView(),
),
bottomNavigationBar: CustomBottomBar(
currentIndex: _currentBottomIndex,
onTap: _onBottomNavTap,
),
);
}
}