import 'package:flutter/material.dart'; import 'package:flutter_lucide/flutter_lucide.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../shared/widgets/main_app_bar_actions.dart'; import '../theme/theme_extension.dart'; import 'admin_providers.dart'; import 'widgets/admin_user_edit_sheet.dart'; import 'widgets/admin_user_row.dart'; import 'widgets/invite_row.dart'; class AdminUsersScreen extends ConsumerWidget { const AdminUsersScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; final users = ref.watch(adminUsersProvider); final invites = ref.watch(adminInvitesProvider); return Scaffold( backgroundColor: fs.obsidian, appBar: AppBar( backgroundColor: fs.obsidian, elevation: 0, title: Text('Users', style: TextStyle(color: fs.parchment)), actions: const [MainAppBarActions(currentRoute: '/admin/users')], ), body: SafeArea( child: RefreshIndicator( onRefresh: () async { ref.invalidate(adminUsersProvider); ref.invalidate(adminInvitesProvider); }, child: ListView( children: [ const _SectionHeader(text: 'Users'), users.when( loading: () => const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ), error: (e, _) => Padding( padding: const EdgeInsets.all(16), child: Text('$e', style: TextStyle(color: fs.error)), ), data: (rows) => Column( children: rows .map((u) => AdminUserRow( key: Key('admin_user_row_${u.id}'), user: u, onTap: () => AdminUserEditSheet.show(context, u), )) .toList(), ), ), const Divider(), _SectionHeader( text: 'Invites', trailing: TextButton.icon( key: const Key('invite_generate_button'), onPressed: () => _showGenerateInvite(context, ref), icon: Icon(LucideIcons.plus, color: fs.parchment), label: Text('Generate', style: TextStyle(color: fs.parchment)), ), ), invites.when( loading: () => const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ), error: (e, _) => Padding( padding: const EdgeInsets.all(16), child: Text('$e', style: TextStyle(color: fs.error)), ), data: (rows) => Column( children: rows .map((i) => InviteRow( key: Key('invite_row_${i.token}'), invite: i, onRevoke: () => ref .read(adminInvitesProvider.notifier) .revoke(i.token), )) .toList(), ), ), ], ), ), ), ); } Future _showGenerateInvite(BuildContext context, WidgetRef ref) async { final controller = TextEditingController(); final note = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Generate invite'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Token expires in 24 hours.'), const SizedBox(height: 12), TextField( controller: controller, decoration: const InputDecoration( labelText: 'Note (optional)', helperText: 'e.g. "for alice"', ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Cancel'), ), TextButton( onPressed: () => Navigator.pop(ctx, controller.text), child: const Text('Generate'), ), ], ), ); if (note == null || !context.mounted) return; try { final invite = await ref .read(adminInvitesProvider.notifier) .create(note: note.isEmpty ? null : note); if (!context.mounted) return; await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Invite created'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Share this token with the new user:'), const SizedBox(height: 8), SelectableText( invite.token, style: const TextStyle(fontFamily: 'JetBrainsMono'), ), ], ), actions: [ TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: invite.token)); Navigator.pop(ctx); }, child: const Text('Copy'), ), TextButton( onPressed: () => Navigator.pop(ctx), child: const Text('Close'), ), ], ), ); } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('$e'))); } } } } class _SectionHeader extends StatelessWidget { const _SectionHeader({required this.text, this.trailing}); final String text; final Widget? trailing; @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; return Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), child: Row( children: [ Text( text, style: TextStyle( color: fs.parchment, fontFamily: 'Fraunces', fontSize: 18, ), ), const Spacer(), if (trailing != null) trailing!, ], ), ); } }