import "package:flutter/material.dart"; import "../main.dart"; import "../models.dart"; import "../format.dart"; import "../theme.dart"; /// Admin-only screen to manage user accounts: list, create, change role, /// reset password, delete. The API enforces the real access control and the /// last-admin / self-delete guards; the UI mirrors them to avoid dead actions. class AdminUsersScreen extends StatefulWidget { const AdminUsersScreen({super.key}); @override State createState() => _AdminUsersScreenState(); } class _AdminUsersScreenState extends State { late Future> _future; @override void initState() { super.initState(); _future = apiClient.listUsers(); } void _reload() => setState(() => _future = apiClient.listUsers()); String? get _myId => authService.user?.id; void _snack(String msg) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg))); Future _changeRole(AdminUser u, String role) async { try { await apiClient.updateUser(u.id, role: role); _reload(); } catch (e) { _snack("$e"); } } Future _resetPassword(AdminUser u) async { final controller = TextEditingController(); final saved = await showDialog( context: context, builder: (ctx) => AlertDialog( title: Text("Reset password — ${u.email}"), content: TextField( controller: controller, autofocus: true, decoration: const InputDecoration( labelText: "New password (min 8)", border: OutlineInputBorder(), ), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text("Cancel")), FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text("Set password")), ], ), ); if (saved != true) return; try { await apiClient.setUserPassword(u.id, controller.text); _snack("Password updated."); } catch (e) { _snack("$e"); } } Future _delete(AdminUser u) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text("Delete user?"), content: Text("Delete ${u.name.isEmpty ? u.email : u.name}? This cannot be undone."), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text("Cancel")), FilledButton( style: FilledButton.styleFrom(backgroundColor: DriverVault.danger), onPressed: () => Navigator.pop(ctx, true), child: const Text("Delete"), ), ], ), ); if (confirmed != true) return; try { await apiClient.deleteUser(u.id); _reload(); } catch (e) { _snack("$e"); } } Future _createUser() async { final created = await showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => const _CreateUserSheet(), ); if (created == true) _reload(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Users")), floatingActionButton: FloatingActionButton.extended( onPressed: _createUser, icon: const Icon(Icons.person_add_alt_1), label: const Text("Add user"), ), body: FutureBuilder>( future: _future, builder: (context, snap) { if (snap.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snap.hasError) { return Center(child: Text("${snap.error}")); } final users = snap.data ?? []; final adminCount = users.where((u) => u.role == "admin").length; return ListView.separated( padding: const EdgeInsets.all(12), itemCount: users.length, separatorBuilder: (_, __) => const Divider(height: 1), itemBuilder: (context, i) { final u = users[i]; final isSelf = u.id == _myId; final lastAdmin = u.role == "admin" && adminCount <= 1; return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 4), title: Row( children: [ Flexible(child: Text(u.email, overflow: TextOverflow.ellipsis)), if (isSelf) const Padding( padding: EdgeInsets.only(left: 6), child: Text("(you)", style: TextStyle(color: Colors.grey, fontSize: 12)), ), ], ), subtitle: Text( "${u.name.isEmpty ? '—' : u.name} · ${formatDate(DateTime.tryParse(u.created))}", style: const TextStyle(fontSize: 12), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ DropdownButton( value: u.role == "admin" ? "admin" : "user", underline: const SizedBox.shrink(), // Disable demoting the last admin. onChanged: lastAdmin ? null : (v) => v == null ? null : _changeRole(u, v), items: const [ DropdownMenuItem(value: "user", child: Text("user")), DropdownMenuItem(value: "admin", child: Text("admin")), ], ), PopupMenuButton( onSelected: (choice) { if (choice == "password") _resetPassword(u); if (choice == "delete") _delete(u); }, itemBuilder: (_) => [ const PopupMenuItem(value: "password", child: Text("Reset password")), PopupMenuItem( value: "delete", // Can't delete yourself or the last admin. enabled: !isSelf && !lastAdmin, child: const Text("Delete", style: TextStyle(color: DriverVault.danger)), ), ], ), ], ), ); }, ); }, ), ); } } class _CreateUserSheet extends StatefulWidget { const _CreateUserSheet(); @override State<_CreateUserSheet> createState() => _CreateUserSheetState(); } class _CreateUserSheetState extends State<_CreateUserSheet> { final _email = TextEditingController(); final _name = TextEditingController(); final _password = TextEditingController(); String _role = "user"; bool _saving = false; String? _error; @override void dispose() { _email.dispose(); _name.dispose(); _password.dispose(); super.dispose(); } Future _save() async { setState(() { _saving = true; _error = null; }); try { await apiClient.createUser( email: _email.text.trim(), password: _password.text, name: _name.text.trim(), role: _role, ); if (mounted) Navigator.pop(context, true); } catch (e) { setState(() => _error = e.toString()); } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 16, bottom: MediaQuery.of(context).viewInsets.bottom + 16, ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("Add a user", style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), const SizedBox(height: 12), if (_error != null) Padding( padding: const EdgeInsets.only(bottom: 8), child: Text(_error!, style: const TextStyle(color: DriverVault.danger)), ), TextField( controller: _email, keyboardType: TextInputType.emailAddress, decoration: const InputDecoration(labelText: "Email", border: OutlineInputBorder()), ), const SizedBox(height: 8), TextField( controller: _name, decoration: const InputDecoration(labelText: "Name", border: OutlineInputBorder()), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextField( controller: _password, decoration: const InputDecoration( labelText: "Password (min 8)", border: OutlineInputBorder(), ), ), ), const SizedBox(width: 8), DropdownButton( value: _role, onChanged: (v) => setState(() => _role = v ?? "user"), items: const [ DropdownMenuItem(value: "user", child: Text("user")), DropdownMenuItem(value: "admin", child: Text("admin")), ], ), ], ), const SizedBox(height: 12), SizedBox( width: double.infinity, child: FilledButton( onPressed: _saving ? null : _save, child: Text(_saving ? "Creating…" : "Create user"), ), ), ], ), ); } }