import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/api/api_error_mapper.dart'; import '../data/admin_repository.dart'; import '../domain/user_admin.dart'; class AdminScreen extends ConsumerStatefulWidget { const AdminScreen({super.key}); @override ConsumerState createState() => _AdminScreenState(); } class _AdminScreenState extends ConsumerState { bool _isLoading = true; String? _error; List _users = []; @override void initState() { super.initState(); _load(); } Future _load() async { setState(() { _isLoading = true; _error = null; }); try { final users = await ref.read(adminRepositoryProvider).listUsers(); if (!mounted) return; setState(() => _users = users); } catch (e) { if (!mounted) return; setState(() => _error = mapErrorToUserMessage(e, context)); } finally { if (mounted) setState(() => _isLoading = false); } } Future _changeRole(UserAdmin user) async { final newRole = user.isAdmin ? 'user' : 'admin'; final confirmed = await _confirm( context, 'Ändra roll', 'Ändra ${user.username} till $newRole?', ); if (!confirmed || !mounted) return; try { await ref.read(adminRepositoryProvider).setRole(user.id, newRole); if (!mounted) return; _load(); } catch (e) { if (!mounted) return; _showError(e); } } Future _resetPassword(UserAdmin user) async { final confirmed = await _confirm( context, 'Återställ lösenord', 'Generera ett tillfälligt lösenord för ${user.username}?', ); if (!confirmed || !mounted) return; try { final result = await ref.read(adminRepositoryProvider).resetPassword(user.id); if (!mounted) return; final tempPw = result['temporaryPassword'] as String? ?? ''; await showDialog( context: context, builder: (_) => AlertDialog( title: const Text('Tillfälligt lösenord'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Lösenord för ${user.username}:'), const SizedBox(height: 8), Row( children: [ Expanded( child: SelectableText( tempPw, style: const TextStyle(fontFamily: 'monospace', fontWeight: FontWeight.bold), ), ), IconButton( icon: const Icon(Icons.copy), tooltip: 'Kopiera', onPressed: () => Clipboard.setData(ClipboardData(text: tempPw)), ), ], ), const SizedBox(height: 8), const Text('Användaren måste byta lösenord vid nästa inloggning.', style: TextStyle(fontSize: 12)), ], ), actions: [TextButton(onPressed: () => Navigator.pop(context), child: const Text('Stäng'))], ), ); } catch (e) { if (!mounted) return; _showError(e); } } Future _deleteUser(UserAdmin user) async { final confirmed = await _confirm( context, 'Ta bort användare', 'Ta bort ${user.username} permanent? Detta går inte att ångra.', destructive: true, ); if (!confirmed || !mounted) return; try { await ref.read(adminRepositoryProvider).deleteUser(user.id); if (!mounted) return; _load(); } catch (e) { if (!mounted) return; _showError(e); } } Future _createUser() async { final result = await showDialog>( context: context, builder: (_) => const _CreateUserDialog(), ); if (result == null || !mounted) return; try { await ref.read(adminRepositoryProvider).createUser( username: result['username']!, email: result['email']!, password: result['password']!, role: result['role'] ?? 'user', ); if (!mounted) return; _load(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Användare ${result['username']} skapad.')), ); } catch (e) { if (!mounted) return; _showError(e); } } void _showError(Object e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(mapErrorToUserMessage(e, context)), backgroundColor: Theme.of(context).colorScheme.error, ), ); } Future _confirm(BuildContext ctx, String title, String body, {bool destructive = false}) async { final result = await showDialog( context: ctx, builder: (_) => AlertDialog( title: Text(title), content: Text(body), actions: [ TextButton(onPressed: () => Navigator.pop(_, false), child: const Text('Avbryt')), TextButton( onPressed: () => Navigator.pop(_, true), style: destructive ? TextButton.styleFrom(foregroundColor: Theme.of(ctx).colorScheme.error) : null, child: const Text('Bekräfta'), ), ], ), ); return result ?? false; } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('Admin – Användare'), actions: [ IconButton( icon: const Icon(Icons.refresh), tooltip: 'Uppdatera', onPressed: _isLoading ? null : _load, ), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: _createUser, icon: const Icon(Icons.person_add_outlined), label: const Text('Ny användare'), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text(_error!, style: TextStyle(color: theme.colorScheme.error)), const SizedBox(height: 16), FilledButton(onPressed: _load, child: const Text('Försök igen')), ], ), ) : _users.isEmpty ? const Center(child: Text('Inga användare hittades.')) : ListView.builder( padding: const EdgeInsets.fromLTRB(16, 8, 16, 80), itemCount: _users.length, itemBuilder: (ctx, i) => _UserTile( user: _users[i], onChangeRole: () => _changeRole(_users[i]), onResetPassword: () => _resetPassword(_users[i]), onDelete: () => _deleteUser(_users[i]), ), ), ); } } class _UserTile extends StatelessWidget { final UserAdmin user; final VoidCallback onChangeRole; final VoidCallback onResetPassword; final VoidCallback onDelete; const _UserTile({ required this.user, required this.onChangeRole, required this.onResetPassword, required this.onDelete, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( margin: const EdgeInsets.symmetric(vertical: 4), child: ListTile( leading: CircleAvatar( backgroundColor: user.isAdmin ? theme.colorScheme.primaryContainer : theme.colorScheme.secondaryContainer, child: Icon( user.isAdmin ? Icons.shield_outlined : Icons.person_outline, color: user.isAdmin ? theme.colorScheme.onPrimaryContainer : theme.colorScheme.onSecondaryContainer, size: 20, ), ), title: Text(user.displayName), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(user.email, style: theme.textTheme.bodySmall), Row( children: [ Chip( label: Text(user.role), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, labelStyle: theme.textTheme.labelSmall, ), if (user.isPremium) ...[ const SizedBox(width: 4), Chip( label: const Text('Premium'), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, labelStyle: theme.textTheme.labelSmall, backgroundColor: theme.colorScheme.tertiaryContainer, ), ], ], ), ], ), isThreeLine: true, trailing: PopupMenuButton( onSelected: (action) { switch (action) { case 'role': onChangeRole(); case 'reset': onResetPassword(); case 'delete': onDelete(); } }, itemBuilder: (_) => [ PopupMenuItem( value: 'role', child: Text(user.isAdmin ? 'Nedgradera till user' : 'Uppgradera till admin'), ), const PopupMenuItem(value: 'reset', child: Text('Återställ lösenord')), const PopupMenuDivider(), PopupMenuItem( value: 'delete', child: Text('Ta bort', style: TextStyle(color: Theme.of(context).colorScheme.error)), ), ], ), ), ); } } class _CreateUserDialog extends StatefulWidget { const _CreateUserDialog(); @override State<_CreateUserDialog> createState() => _CreateUserDialogState(); } class _CreateUserDialogState extends State<_CreateUserDialog> { final _formKey = GlobalKey(); final _usernameCtrl = TextEditingController(); final _emailCtrl = TextEditingController(); final _passwordCtrl = TextEditingController(); String _role = 'user'; bool _obscure = true; @override void dispose() { _usernameCtrl.dispose(); _emailCtrl.dispose(); _passwordCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Skapa användare'), content: Form( key: _formKey, child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: _usernameCtrl, decoration: const InputDecoration(labelText: 'Användarnamn'), validator: (v) => (v == null || v.length < 2) ? 'Minst 2 tecken' : null, ), const SizedBox(height: 12), TextFormField( controller: _emailCtrl, decoration: const InputDecoration(labelText: 'E-post'), keyboardType: TextInputType.emailAddress, validator: (v) { if (v == null || v.isEmpty) return 'Obligatoriskt'; if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(v)) return 'Ogiltig e-post'; return null; }, ), const SizedBox(height: 12), TextFormField( controller: _passwordCtrl, decoration: InputDecoration( labelText: 'Lösenord', suffixIcon: IconButton( icon: Icon(_obscure ? Icons.visibility_off_outlined : Icons.visibility_outlined), onPressed: () => setState(() => _obscure = !_obscure), ), ), obscureText: _obscure, validator: (v) => (v == null || v.length < 8) ? 'Minst 8 tecken' : null, ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: _role, decoration: const InputDecoration(labelText: 'Roll'), items: const [ DropdownMenuItem(value: 'user', child: Text('Användare')), DropdownMenuItem(value: 'admin', child: Text('Admin')), ], onChanged: (v) => setState(() => _role = v ?? 'user'), ), ], ), ), ), actions: [ TextButton(onPressed: () => Navigator.pop(context), child: const Text('Avbryt')), FilledButton( onPressed: () { if (_formKey.currentState!.validate()) { Navigator.pop(context, { 'username': _usernameCtrl.text.trim(), 'email': _emailCtrl.text.trim(), 'password': _passwordCtrl.text, 'role': _role, }); } }, child: const Text('Skapa'), ), ], ); } }