diff --git a/lib/components/pages/loco_timeline.dart b/lib/components/pages/loco_timeline.dart index 1562235..3dabbd7 100644 --- a/lib/components/pages/loco_timeline.dart +++ b/lib/components/pages/loco_timeline.dart @@ -18,10 +18,12 @@ class LocoTimelinePage extends StatefulWidget { super.key, required this.locoId, required this.locoLabel, + this.forceShowPending = false, }); final int locoId; final String locoLabel; + final bool forceShowPending; @override State createState() => _LocoTimelinePageState(); @@ -41,7 +43,13 @@ class _LocoTimelinePageState extends State { void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { - await _restorePendingVisibility(); + if (widget.forceShowPending) { + setState(() { + _showPending = true; + }); + } else { + await _restorePendingVisibility(); + } if (!mounted) return; await _load(); }); diff --git a/lib/components/pages/traction/traction_page.dart b/lib/components/pages/traction/traction_page.dart index dec21fe..daa1737 100644 --- a/lib/components/pages/traction/traction_page.dart +++ b/lib/components/pages/traction/traction_page.dart @@ -4,6 +4,7 @@ enum _TractionMoreAction { classStats, classLeaderboard, adminPending, + adminPendingChanges, } class TractionPage extends StatefulWidget { @@ -708,6 +709,19 @@ class _TractionPageState extends State { ); } break; + case _TractionMoreAction.adminPendingChanges: + final messenger = ScaffoldMessenger.of(context); + try { + await context.push('/traction/changes'); + } catch (_) { + if (!mounted) return; + messenger.showSnackBar( + const SnackBar( + content: Text('Unable to open pending changes'), + ), + ); + } + break; } }, itemBuilder: (context) { @@ -765,6 +779,12 @@ class _TractionPageState extends State { ), ), ); + items.add( + const PopupMenuItem( + value: _TractionMoreAction.adminPendingChanges, + child: Text('Pending changes'), + ), + ); } return items; }, diff --git a/lib/components/pages/traction/traction_pending_changes_page.dart b/lib/components/pages/traction/traction_pending_changes_page.dart new file mode 100644 index 0000000..afde8e3 --- /dev/null +++ b/lib/components/pages/traction/traction_pending_changes_page.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mileograph_flutter/components/traction/traction_card.dart'; +import 'package:mileograph_flutter/objects/objects.dart'; +import 'package:mileograph_flutter/services/api_service.dart'; +import 'package:mileograph_flutter/services/authservice.dart'; +import 'package:provider/provider.dart'; + +class TractionPendingChangesPage extends StatefulWidget { + const TractionPendingChangesPage({super.key}); + + @override + State createState() => + _TractionPendingChangesPageState(); +} + +class _TractionPendingChangesPageState extends State { + bool _isLoading = false; + String? _error; + List _locos = const []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _load()); + } + + Future _load() async { + setState(() { + _isLoading = true; + _error = null; + }); + try { + final api = context.read(); + final json = await api.get('/event/pending/locos'); + if (json is List) { + setState(() { + _locos = json + .whereType() + .map((e) => LocoSummary.fromJson( + e.map((k, v) => MapEntry(k.toString(), v)), + )) + .toList(); + }); + } else { + setState(() { + _error = 'Unexpected response'; + _locos = const []; + }); + } + } catch (e) { + setState(() { + _error = e.toString(); + _locos = const []; + }); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final isElevated = context.select((auth) => auth.isElevated); + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.of(context).maybePop(), + ), + title: const Text('Pending changes'), + ), + body: isElevated + ? RefreshIndicator( + onRefresh: _load, + child: _buildBody(context), + ) + : ListView( + padding: const EdgeInsets.all(16), + children: const [ + Text('Admin access required.'), + ], + ), + ); + } + + Widget _buildBody(BuildContext context) { + if (_isLoading && _locos.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + if (_error != null) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + Text( + 'Failed to load pending changes: $_error', + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).colorScheme.error), + ), + const SizedBox(height: 12), + FilledButton.icon( + onPressed: _load, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + ), + ], + ); + } + if (_locos.isEmpty) { + return ListView( + padding: const EdgeInsets.all(16), + children: const [ + Text('No pending changes found.'), + ], + ); + } + return ListView.builder( + padding: const EdgeInsets.all(12), + itemCount: _locos.length, + itemBuilder: (context, index) { + final loco = _locos[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6.0), + child: TractionCard( + loco: loco, + selectionMode: false, + isSelected: false, + onShowInfo: () => showTractionDetails( + context, + loco, + onActionComplete: _load, + ), + onOpenTimeline: () => context.push( + '/traction/${loco.id}/timeline', + extra: { + 'label': '${loco.locoClass} ${loco.number}'.trim(), + 'showPending': true, + }, + ), + onOpenLegs: () => context.push('/traction/${loco.id}/legs'), + onActionComplete: _load, + ), + ); + }, + ); + } +} diff --git a/lib/ui/app_shell.dart b/lib/ui/app_shell.dart index bbfcb95..d5ef0ec 100644 --- a/lib/ui/app_shell.dart +++ b/lib/ui/app_shell.dart @@ -21,6 +21,7 @@ import 'package:mileograph_flutter/components/pages/settings.dart'; import 'package:mileograph_flutter/components/pages/stats.dart'; import 'package:mileograph_flutter/components/pages/traction.dart'; import 'package:mileograph_flutter/components/pages/traction/traction_pending_page.dart'; +import 'package:mileograph_flutter/components/pages/traction/traction_pending_changes_page.dart'; import 'package:mileograph_flutter/components/pages/more/user_profile_page.dart'; import 'package:mileograph_flutter/components/widgets/friend_request_notification_card.dart'; import 'package:mileograph_flutter/components/widgets/leg_share_edit_notification_card.dart'; @@ -221,6 +222,11 @@ class _MyAppState extends State { path: '/traction/pending', builder: (context, state) => const TractionPendingPage(), ), + GoRoute( + path: '/traction/changes', + builder: (context, state) => + const TractionPendingChangesPage(), + ), GoRoute( path: '/profile', builder: (context, state) => const ProfilePage(), @@ -238,7 +244,15 @@ class _MyAppState extends State { label = extra; } if (label.trim().isEmpty) label = 'Loco $locoId'; - return LocoTimelinePage(locoId: locoId, locoLabel: label); + bool showPending = false; + if (extra is Map && extra['showPending'] is bool) { + showPending = extra['showPending'] as bool; + } + return LocoTimelinePage( + locoId: locoId, + locoLabel: label, + forceShowPending: showPending, + ); }, ), GoRoute(