Files
The-transit-Project/deprecated/lib/screens/schedule_overview_screen.dart
Sven laptop 9d1a37e248
Build and Push Docker Images / detect-changes (push) Successful in 10s
Build and Push Docker Images / build-auth-api (push) Successful in 58s
Build and Push Docker Images / build-employee-api (push) Successful in 58s
Build and Push Docker Images / build-transit-api (push) Successful in 59s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m8s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m14s
Build and Push Docker Images / deploy-test-node (push) Successful in 5s
Update readme's, branches and actions
2026-09-17 12:04:44 +02:00

675 lines
25 KiB
Dart

// 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<ScheduleOverviewScreen> createState() => _ScheduleOverviewScreenState();
}
class _ScheduleOverviewScreenState extends State<ScheduleOverviewScreen> with TickerProviderStateMixin {
final _scheduleService = ScheduleService();
final _authService = AuthService();
late Future<List<sched.ServiceSchedule>> _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<void> _handleLogout() async {
await _authService.logout();
if (mounted) {
Navigator.of(context).pushReplacementNamed('/login');
}
}
Future<void> _refreshSchedules() async {
setState(() {
_scheduleFuture = _scheduleService.getSchedules(_selectedDate);
});
}
Future<void> _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<void> _showScheduleDetails(sched.ServiceSchedule svc) async {
try {
final stops = await _scheduleService.getScheduleStops(svc.id);
if (!mounted) return;
await showModalBottomSheet<void>(
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) {
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<List<sched.ServiceSchedule>> 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<List<sched.ServiceSchedule>>(
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<void>(
context: context,
isScrollControlled: true,
builder: (ctx) {
final stops = (progress['stops'] as List<dynamic>?) ?? [];
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<String, dynamic>;
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<List<sched.ServiceSchedule>> 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<List<sched.ServiceSchedule>>(
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<int> 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(', ');
}