// ignore_for_file: use_build_context_synchronously import 'package:flutter/material.dart'; import '../models/schedule.dart' as sched; import '../models/navigation.dart'; import '../services/auth_service.dart'; import '../services/schedule_service.dart'; import '../screens/navigation_screen.dart'; import '../screens/recurring_schedules_screen.dart'; class ScheduleOverviewScreen extends StatefulWidget { const ScheduleOverviewScreen({super.key}); @override State createState() => _ScheduleOverviewScreenState(); } class _ScheduleOverviewScreenState extends State with TickerProviderStateMixin { final _scheduleService = ScheduleService(); final _authService = AuthService(); late Future> _scheduleFuture; DateTime _selectedDate = DateTime.now(); late TabController _tabController; @override void initState() { super.initState(); _scheduleFuture = _scheduleService.getSchedules(_selectedDate); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } Future _handleLogout() async { await _authService.logout(); if (mounted) { Navigator.of(context).pushReplacementNamed('/login'); } } Future _refreshSchedules() async { setState(() { _scheduleFuture = _scheduleService.getSchedules(_selectedDate); }); } Future _selectDate() async { final colorScheme = Theme.of(context).colorScheme; final picked = await showDatePicker( context: context, initialDate: _selectedDate, firstDate: DateTime.now().subtract(const Duration(days: 7)), lastDate: DateTime.now().add(const Duration(days: 30)), builder: (context, child) { return Theme( data: Theme.of(context).copyWith( dialogBackgroundColor: colorScheme.surface, ), child: child!, ); }, ); if (picked != null && picked != _selectedDate) { setState(() { _selectedDate = picked; }); _refreshSchedules(); } } Future _showScheduleDetails(sched.ServiceSchedule svc) async { try { final stops = await _scheduleService.getScheduleStops(svc.id); if (!mounted) return; await showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, builder: (sheetContext) { return DraggableScrollableSheet( expand: false, initialChildSize: 0.75, minChildSize: 0.45, maxChildSize: 0.95, builder: (context, scrollController) { return ListView( controller: scrollController, padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), children: [ Text( 'Lijn ${svc.lineNumber}', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w800, ), ), const SizedBox(height: 8), Text( svc.busName, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 4), Text( '${_formatTime(svc.startTime)} - ${_formatTime(svc.endTime)}', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ _InfoChip(label: svc.status), _InfoChip(label: svc.isRecurring ? _weekdayLabel(svc.weekdays) : 'Eenmalig'), ], ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: FilledButton.icon( onPressed: () async { try { await _scheduleService.updateScheduleStatus( svc.id, 'in_progress', date: svc.startTime, ); if (!mounted) return; Navigator.of(sheetContext).pop(); final routeObj = sched.Route( id: svc.id, busLine: sched.BusLine( id: svc.busLineId, lineNumber: svc.lineNumber, startStop: '', endStop: '', estimatedDuration: 0, ), busType: sched.BusType( id: svc.busId, name: svc.busName, seatCapacity: 0, licensePlate: '', ), startTime: svc.startTime, endTime: svc.endTime, status: 'in_progress', ); final navStops = stops .map((s) => NavigationPoint( latitude: s.latitude, longitude: s.longitude, name: s.name, )) .toList(); try { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => NavigationScreen(route: routeObj, initialStops: navStops), ), ); } catch (navErr) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Navigatiefout: ${navErr.toString()}')), ); } } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Status bijwerken mislukt: $e')), ); } }, icon: const Icon(Icons.navigation), label: const Text('Start route'), ), ), Text( 'Haltes', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 12), if (stops.isEmpty) Text( 'Geen haltes gekoppeld aan deze dienst.', style: Theme.of(context).textTheme.bodyMedium, ) else ...stops.map((stop) { final departureTime = svc.startTime.add( Duration(minutes: stop.estimatedArrivalMinutes), ); return Card( child: ListTile( dense: true, title: Text('${stop.order}. ${stop.name}'), subtitle: Text('${stop.estimatedArrivalMinutes} min na vertrek'), trailing: Text(_formatTime(departureTime)), ), ); }), ], ); }, ); }, ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Kon dienstdetails niet laden: $e')), ); } } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Scaffold( appBar: AppBar( title: const Text('Diensten'), centerTitle: true, elevation: 0, bottom: TabBar( controller: _tabController, tabs: const [ Tab(icon: Icon(Icons.calendar_today), text: 'Dagschema'), Tab(icon: Icon(Icons.repeat), text: 'Vaste diensten'), Tab(icon: Icon(Icons.timeline), text: 'Voortgang'), ], ), actions: [ IconButton( icon: const Icon(Icons.logout), tooltip: 'Logout', onPressed: _handleLogout, ), ], ), body: TabBarView( controller: _tabController, children: [ _DailyScheduleTab( scheduleService: _scheduleService, selectedDate: _selectedDate, onSelectDate: _selectDate, onRefresh: _refreshSchedules, onShowDetails: _showScheduleDetails, scheduleFuture: _scheduleFuture, ), const RecurringSchedulesScreen(), _ProgressTab( scheduleService: _scheduleService, selectedDate: _selectedDate, scheduleFuture: _scheduleFuture, ), ], ), ); } } class _ProgressTab extends StatelessWidget { final ScheduleService scheduleService; final DateTime selectedDate; final Future> scheduleFuture; const _ProgressTab({ required this.scheduleService, required this.selectedDate, required this.scheduleFuture, }); @override Widget build(BuildContext context) { final dateStr = '${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}'; return Column( children: [ Container( padding: const EdgeInsets.all(16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Voortgang voor $dateStr', style: Theme.of(context).textTheme.titleLarge), IconButton( onPressed: () async { // refresh handled by parent via reselecting date; show a simple feedback ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Ververs de datum in Dagschema-tab om andere datum te laden'))); }, icon: const Icon(Icons.info_outline), ), ], ), ), Expanded( child: FutureBuilder>( future: scheduleFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) return const Center(child: CircularProgressIndicator()); if (snapshot.hasError) return Center(child: Text('Fout bij laden: ${snapshot.error}')); final schedules = snapshot.data ?? []; if (schedules.isEmpty) return Center(child: Text('Geen diensten op $dateStr')); return ListView.builder( padding: const EdgeInsets.all(12), itemCount: schedules.length, itemBuilder: (context, index) { final svc = schedules[index]; return Card( child: ListTile( title: Text('Lijn ${svc.lineNumber} — ${svc.busName}'), subtitle: Text('${_formatTime(svc.startTime)}'), trailing: TextButton( onPressed: () async { try { final progress = await scheduleService.getScheduleProgress(svc.id, selectedDate); if (!context.mounted) return; await showModalBottomSheet( context: context, isScrollControlled: true, builder: (ctx) { final stops = (progress['stops'] as List?) ?? []; return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Voortgang — Lijn ${svc.lineNumber}', style: Theme.of(ctx).textTheme.headlineSmall), const SizedBox(height: 8), Text('Status: ${progress['schedule']?['status'] ?? 'onbekend'}'), Text('Driver state: ${progress['driverState'] ?? 'onbekend'}'), Text('Passed: ${progress['passedStopCount'] ?? 0} / ${progress['totalStopCount'] ?? 0}'), const SizedBox(height: 12), const Text('Haltes:'), const SizedBox(height: 8), Expanded( child: ListView.builder( shrinkWrap: true, itemCount: stops.length, itemBuilder: (c, i) { final s = stops[i] as Map; return ListTile( dense: true, title: Text('${s['stopOrder']}. ${s['stopName']}'), subtitle: Text('Scheduled: ${s['scheduledPassedAt'] ?? '-'}\nActual: ${s['actualPassedAt'] ?? '-'}'), ); }, ), ), ], ), ); }, ); } catch (e) { if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Kon voortgang niet laden: $e'))); } }, child: const Text('Bekijk voortgang'), ), ), ); }, ); }, ), ), ], ); } } class _DailyScheduleTab extends StatelessWidget { final ScheduleService scheduleService; final DateTime selectedDate; final VoidCallback onSelectDate; final VoidCallback onRefresh; final Function(sched.ServiceSchedule) onShowDetails; final Future> scheduleFuture; const _DailyScheduleTab({ required this.scheduleService, required this.selectedDate, required this.onSelectDate, required this.onRefresh, required this.onShowDetails, required this.scheduleFuture, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final dateStr = '${selectedDate.year}-${selectedDate.month.toString().padLeft(2, '0')}-${selectedDate.day.toString().padLeft(2, '0')}'; return Container( decoration: BoxDecoration( gradient: LinearGradient( colors: Theme.of(context).brightness == Brightness.dark ? const [Color(0xFF0B1220), Color(0xFF111827)] : const [Color(0xFFF1F5F9), Color(0xFFE2E8F0)], begin: Alignment.topCenter, end: Alignment.bottomCenter, ), ), child: Column( children: [ Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 12), padding: const EdgeInsets.all(18), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF0F172A), Color(0xFF1D4ED8)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.16), blurRadius: 22, offset: const Offset(0, 10), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Geplande diensten', style: Theme.of(context).textTheme.labelSmall?.copyWith( color: Colors.white.withOpacity(0.78), ), ), const SizedBox(height: 6), Text( dateStr, style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w800, ), ), const SizedBox(height: 6), Text( 'Automatisch berekende eindtijden, haltes en herhalende weekdagen', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Colors.white.withOpacity(0.84), ), ), const SizedBox(height: 14), Align( alignment: Alignment.centerRight, child: SizedBox( height: 48, child: OutlinedButton.icon( onPressed: onSelectDate, icon: const Icon(Icons.calendar_today, size: 18), label: const Text('Wijzig datum'), style: OutlinedButton.styleFrom( foregroundColor: Colors.white, side: BorderSide(color: Colors.white.withOpacity(0.28)), padding: const EdgeInsets.symmetric(horizontal: 16), ), ), ), ), ], ), ), Expanded( child: FutureBuilder>( future: scheduleFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: colorScheme.error), const SizedBox(height: 16), Text('Fout bij laden: ${snapshot.error}', textAlign: TextAlign.center), const SizedBox(height: 16), FilledButton( onPressed: onRefresh, child: const Text('Opnieuw proberen'), ), ], ), ); } final schedules = snapshot.data ?? []; if (schedules.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.event_available_outlined, size: 48, color: colorScheme.outline), const SizedBox(height: 16), Text('Geen diensten op $dateStr', style: Theme.of(context).textTheme.titleMedium), ], ), ); } return RefreshIndicator( onRefresh: () async => onRefresh(), child: ListView.builder( padding: const EdgeInsets.fromLTRB(0, 12, 0, 24), itemCount: schedules.length, itemBuilder: (context, index) { final schedule = schedules[index]; return _ScheduleSummaryCard( schedule: schedule, onTap: () => onShowDetails(schedule), ); }, ), ); }, ), ), ], ), ); } } class _ScheduleSummaryCard extends StatelessWidget { final sched.ServiceSchedule schedule; final VoidCallback onTap; const _ScheduleSummaryCard({ required this.schedule, required this.onTap, }); @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Card( margin: const EdgeInsets.fromLTRB(16, 8, 16, 8), elevation: 0, color: colorScheme.surface, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: InkWell( borderRadius: BorderRadius.circular(20), onTap: onTap, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( gradient: LinearGradient( colors: [colorScheme.primary, colorScheme.tertiary], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(8), ), child: Text( 'Line ${schedule.lineNumber}', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 18, ), ), ), // Only show status for one-time schedules; recurring schedules have no fixed status if (!schedule.isRecurring) _InfoChip(label: schedule.status), ], ), const SizedBox(height: 16), Text( schedule.busName, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 4), Text( '${_formatTime(schedule.startTime)} - ${_formatTime(schedule.endTime)}', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 12), Text( schedule.isRecurring ? _weekdayLabel(schedule.weekdays) : 'Eenmalige dienst', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), ), const SizedBox(height: 10), Align( alignment: Alignment.centerRight, child: TextButton.icon( onPressed: onTap, icon: const Icon(Icons.list_alt, size: 18), label: const Text('Haltes'), ), ), ], ), ), ), ); } } class _InfoChip extends StatelessWidget { final String label; const _InfoChip({required this.label}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(20), ), child: Text( label, style: Theme.of(context).textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w600, ), ), ); } } String _formatTime(DateTime time) { return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; } String _weekdayLabel(List weekdays) { const labels = ['Zo', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Za']; if (weekdays.isEmpty) { return 'Eenmalige dienst'; } if (weekdays.length == 7) { return 'Dagelijks'; } return weekdays.where((day) => day >= 0 && day < labels.length).map((day) => labels[day]).join(', '); }