import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:mileograph_flutter/objects/objects.dart'; import 'package:mileograph_flutter/services/api_service.dart'; import 'package:mileograph_flutter/services/authservice.dart'; class AdminPage extends StatefulWidget { const AdminPage({super.key}); @override State createState() => _AdminPageState(); } class _AdminPageState extends State { final TextEditingController _titleController = TextEditingController(); final TextEditingController _bodyController = TextEditingController(); final List _selectedUsers = []; List _userOptions = []; List _channels = []; String? _selectedChannel; String? _channelError; bool _loadingChannels = false; String? _userError; bool _sending = false; @override void initState() { super.initState(); _loadChannels(); } @override void dispose() { _titleController.dispose(); _bodyController.dispose(); super.dispose(); } Future _loadChannels() async { setState(() { _loadingChannels = true; _channelError = null; }); try { final api = context.read(); final json = await api.get('/notifications/channels'); List? list; if (json is List) { list = json; } else if (json is Map) { for (final key in ['channels', 'data']) { final value = json[key]; if (value is List) { list = value; break; } } } final parsed = list?.map((e) => e.toString()).where((e) => e.isNotEmpty).toList() ?? const []; setState(() { _channels = parsed; _selectedChannel = parsed.isNotEmpty ? parsed.first : null; }); } catch (e) { setState(() { _channelError = 'Failed to load channels'; }); } finally { if (mounted) setState(() => _loadingChannels = false); } } Future> _fetchUserSuggestions( ApiService api, String query, ) async { final encoded = Uri.encodeComponent(query); final candidates = [ '/users/search?q=$encoded', '/users/search?query=$encoded', '/users?search=$encoded', ]; for (final path in candidates) { try { final json = await api.get(path); List? list; if (json is List) { list = json; } else if (json is Map) { for (final key in ['users', 'data', 'results', 'items']) { final value = json[key]; if (value is List) { list = value; break; } } } if (list != null) { return list .whereType() .map((e) => UserSummary.fromJson( e.map((k, v) => MapEntry(k.toString(), v)), )) .toList(); } } catch (_) { // Try next endpoint } } return const []; } void _removeUser(UserSummary user) { setState(() { _selectedUsers.removeWhere((u) => u.userId == user.userId); }); } Future _openUserPicker() async { final api = context.read(); var tempSelected = List.from(_selectedUsers); var options = List.from(_userOptions); String query = ''; bool loading = false; String? error = _userError; Future runSearch(String q, void Function(void Function()) setModalState) async { setModalState(() { query = q; loading = true; error = null; }); try { final results = await _fetchUserSuggestions(api, q); setModalState(() { options = results; loading = false; error = null; }); if (mounted) { setState(() { _userOptions = results; _userError = null; }); } } catch (e) { setModalState(() { loading = false; error = 'Failed to search users'; }); if (mounted) { setState(() { _userError = 'Failed to search users'; }); } } } var initialFetchTriggered = false; await showModalBottomSheet( context: context, isScrollControlled: true, builder: (ctx) { return StatefulBuilder( builder: (ctx, setModalState) { if (!initialFetchTriggered && !loading && options.isEmpty) { initialFetchTriggered = true; WidgetsBinding.instance.addPostFrameCallback( (_) => runSearch('', setModalState), ); } final lowerQuery = query.toLowerCase(); final filtered = lowerQuery.isEmpty ? options : options.where((u) { return u.displayName.toLowerCase().contains(lowerQuery) || u.username.toLowerCase().contains(lowerQuery) || u.email.toLowerCase().contains(lowerQuery); }).toList(); return SafeArea( child: Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 16, bottom: MediaQuery.of(ctx).viewInsets.bottom + 16, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Select recipients', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ), const Spacer(), TextButton( onPressed: () { setModalState(() { tempSelected.clear(); }); setState(() => _selectedUsers.clear()); }, child: const Text('Clear'), ), ], ), const SizedBox(height: 8), TextField( decoration: const InputDecoration( labelText: 'Search users', border: OutlineInputBorder(), ), onChanged: (val) => runSearch(val, setModalState), ), if (loading) const Padding( padding: EdgeInsets.only(top: 8.0), child: LinearProgressIndicator(minHeight: 2), ), if (error != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( error!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), const SizedBox(height: 12), SizedBox( height: 340, child: filtered.isEmpty ? const Center(child: Text('No users yet.')) : ListView.builder( itemCount: filtered.length, itemBuilder: (_, index) { final user = filtered[index]; final selected = tempSelected.any((u) => u.userId == user.userId); return CheckboxListTile( value: selected, title: Text(user.displayName), subtitle: user.email.isNotEmpty ? Text(user.email) : (user.username.isNotEmpty ? Text(user.username) : null), onChanged: (val) { setModalState(() { if (val == true) { if (!tempSelected .any((u) => u.userId == user.userId)) { tempSelected.add(user); } } else { tempSelected.removeWhere( (u) => u.userId == user.userId); } }); if (mounted) { setState(() { _selectedUsers ..clear() ..addAll(tempSelected); }); } }, ); }, ), ), const SizedBox(height: 8), Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () => Navigator.of(ctx).pop(), child: const Text('Done'), ), ), ], ), ), ); }, ); }, ); } Future _sendNotification() async { final channel = _selectedChannel; if (channel == null || channel.isEmpty) { _showSnack('Select a channel first.'); return; } if (_selectedUsers.isEmpty) { _showSnack('Select at least one user.'); return; } final title = _titleController.text.trim(); final body = _bodyController.text.trim(); if (title.isEmpty || body.isEmpty) { _showSnack('Title and body are required.'); return; } setState(() => _sending = true); try { final api = context.read(); await api.post('/notifications/new', { 'user_ids': _selectedUsers.map((e) => e.userId).toList(), 'channel': channel, 'title': title, 'body': body, }); if (!mounted) return; _showSnack('Notification sent'); setState(() { _selectedUsers.clear(); _titleController.clear(); _bodyController.clear(); _userOptions.clear(); }); } catch (e) { _showSnack('Failed to send: $e'); } finally { if (mounted) setState(() => _sending = false); } } void _showSnack(String message) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); } @override Widget build(BuildContext context) { final isAdmin = context.select((auth) => auth.isElevated); if (!isAdmin) { return const Scaffold( body: Center(child: Text('You do not have access to this page.')), ); } return Scaffold( appBar: AppBar( title: const Text('Admin'), ), body: ListView( padding: const EdgeInsets.all(16), children: [ Text( 'Send notification', style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), const SizedBox(height: 12), _buildUserPicker(), const SizedBox(height: 12), DropdownButtonFormField( value: _selectedChannel, decoration: const InputDecoration( labelText: 'Channel', border: OutlineInputBorder(), ), items: _channels .map( (c) => DropdownMenuItem( value: c, child: Text(c), ), ) .toList(), onChanged: _loadingChannels ? null : (val) => setState(() => _selectedChannel = val), ), if (_loadingChannels) const Padding( padding: EdgeInsets.only(top: 8.0), child: LinearProgressIndicator(minHeight: 2), ), if (_channelError != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( _channelError!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), const SizedBox(height: 12), TextField( controller: _titleController, decoration: const InputDecoration( labelText: 'Title', border: OutlineInputBorder(), ), ), const SizedBox(height: 12), TextField( controller: _bodyController, minLines: 3, maxLines: 6, decoration: const InputDecoration( labelText: 'Body', border: OutlineInputBorder(), ), ), const SizedBox(height: 16), SizedBox( width: double.infinity, child: ElevatedButton.icon( onPressed: _sending ? null : _sendNotification, icon: _sending ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.send), label: Text(_sending ? 'Sending...' : 'Send notification'), ), ), ], ), ); } Widget _buildUserPicker() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Recipients', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: _selectedUsers .map( (u) => InputChip( label: Text(u.displayName), onDeleted: () => _removeUser(u), ), ) .toList(), ), const SizedBox(height: 8), OutlinedButton.icon( onPressed: _openUserPicker, icon: const Icon(Icons.person_search), label: const Text('Select users'), ), if (_userError != null) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( _userError!, style: TextStyle(color: Theme.of(context).colorScheme.error), ), ), ], ); } }