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 AdminUsersPanel extends ConsumerStatefulWidget { final bool embedded; const AdminUsersPanel({super.key, this.embedded = false}); @override ConsumerState createState() => _AdminUsersPanelState(); } class _AdminUsersPanelState 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 _togglePremium(UserAdmin user) async { final newValue = !user.isPremium; final confirmed = await _confirm( context, newValue ? 'Ge Premium' : 'Ta bort Premium', '${newValue ? 'Ge' : 'Ta bort'} Premium för ${user.username}?', ); if (!confirmed || !mounted) return; try { await ref .read(adminRepositoryProvider) .setPremium(user.id, isPremium: newValue); if (!mounted) return; _load(); } catch (e) { if (!mounted) return; _showError(e); } } Future _toggleRecipeSharing(UserAdmin user) async { final newValue = !user.canShareRecipes; final confirmed = await _confirm( context, newValue ? 'Tillåt receptdelning' : 'Blockera receptdelning', '${newValue ? 'Tillåt' : 'Blockera'} receptdelning för ${user.username}?', ); if (!confirmed || !mounted) return; try { await ref .read(adminRepositoryProvider) .setRecipeSharing(user.id, canShareRecipes: newValue); 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 _editEmail(UserAdmin user) async { final controller = TextEditingController(text: user.email); try { final newEmail = await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: Text('Ändra e-post för ${user.username}'), content: TextField( controller: controller, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration( labelText: 'E-post', border: OutlineInputBorder(), ), ), actions: [ TextButton( onPressed: () => Navigator.pop(dialogContext), child: const Text('Avbryt'), ), FilledButton( onPressed: () => Navigator.pop(dialogContext, controller.text.trim()), child: const Text('Spara'), ), ], ), ); if (newEmail == null || newEmail.isEmpty || !mounted) return; if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(newEmail)) { _showError('Ogiltig e-postadress.'); return; } await ref.read(adminRepositoryProvider).updateEmail(user.id, newEmail); if (!mounted) return; _load(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('E-post uppdaterad.')), ); } catch (e) { if (!mounted) return; _showError(e); } finally { controller.dispose(); } } 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); if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error != null) { return 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')), ], ), ); } if (_users.isEmpty) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.embedded) ...[ FilledButton.icon( onPressed: _createUser, icon: const Icon(Icons.person_add_outlined), label: const Text('Ny användare'), ), const SizedBox(height: 16), ], const Text('Inga användare hittades.'), ], ); } final list = ListView.builder( shrinkWrap: widget.embedded, physics: widget.embedded ? const NeverScrollableScrollPhysics() : null, padding: widget.embedded ? EdgeInsets.zero : const EdgeInsets.fromLTRB(16, 8, 16, 80), itemCount: _users.length, itemBuilder: (ctx, i) => _UserTile( user: _users[i], onChangeRole: () => _changeRole(_users[i]), onTogglePremium: () => _togglePremium(_users[i]), onToggleRecipeSharing: () => _toggleRecipeSharing(_users[i]), onEditEmail: () => _editEmail(_users[i]), onResetPassword: () => _resetPassword(_users[i]), onDelete: () => _deleteUser(_users[i]), ), ); if (!widget.embedded) { return list; } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Text( 'Hantera användare direkt från profilsidan.', style: theme.textTheme.bodyMedium, ), ), IconButton( icon: const Icon(Icons.refresh), tooltip: 'Uppdatera', onPressed: _load, ), ], ), const SizedBox(height: 8), FilledButton.icon( onPressed: _createUser, icon: const Icon(Icons.person_add_outlined), label: const Text('Ny användare'), ), const SizedBox(height: 16), list, ], ); } } class _UserTile extends StatelessWidget { final UserAdmin user; final VoidCallback onChangeRole; final VoidCallback onTogglePremium; final VoidCallback onToggleRecipeSharing; final VoidCallback onEditEmail; final VoidCallback onResetPassword; final VoidCallback onDelete; const _UserTile({ required this.user, required this.onChangeRole, required this.onTogglePremium, required this.onToggleRecipeSharing, required this.onEditEmail, 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, ), ], const SizedBox(width: 4), Chip( label: Text(user.canShareRecipes ? 'Delning: På' : 'Delning: Av'), padding: EdgeInsets.zero, visualDensity: VisualDensity.compact, labelStyle: theme.textTheme.labelSmall, backgroundColor: user.canShareRecipes ? theme.colorScheme.secondaryContainer : theme.colorScheme.errorContainer, ), ], ), ], ), isThreeLine: true, trailing: PopupMenuButton( onSelected: (action) { switch (action) { case 'role': onChangeRole(); break; case 'premium': onTogglePremium(); break; case 'sharing': onToggleRecipeSharing(); break; case 'email': onEditEmail(); break; case 'reset': onResetPassword(); break; case 'delete': onDelete(); break; } }, itemBuilder: (_) => [ PopupMenuItem( value: 'role', child: Text( user.isAdmin ? 'Nedgradera till user' : 'Uppgradera till admin', ), ), PopupMenuItem( value: 'premium', child: Text(user.isPremium ? 'Ta bort Premium' : 'Ge Premium'), ), PopupMenuItem( value: 'sharing', child: Text( user.canShareRecipes ? 'Blockera receptdelning' : 'Tillåt receptdelning', ), ), const PopupMenuItem( value: 'email', child: Text('Ändra e-post'), ), 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'), ), ], ); } }