afcaf56b9c
AdminUserRow shows username + admin/auto-approve badges; tap opens
AdminUserEditSheet bottom sheet.
Edit sheet: SwitchListTile pair for is_admin and auto_approve_requests
(both surface server errors via SnackBar, including the last-admin
guard). Reset password takes a typed-input dialog (>=8 chars enforced
client-side; admin supplies the new password). Delete routes through
TypedConfirmSheet ("DELETE") and pops the sheet on success.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
166 lines
5.1 KiB
Dart
166 lines
5.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../models/admin_user.dart';
|
|
import '../../theme/theme_extension.dart';
|
|
import '../admin_providers.dart';
|
|
import 'typed_confirm_sheet.dart';
|
|
|
|
class AdminUserEditSheet extends ConsumerWidget {
|
|
const AdminUserEditSheet({super.key, required this.user});
|
|
|
|
final AdminUser user;
|
|
|
|
static Future<void> show(BuildContext context, AdminUser user) =>
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (_) => AdminUserEditSheet(user: user),
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
|
final users = ref.read(adminUsersProvider.notifier);
|
|
return Padding(
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
),
|
|
child: Container(
|
|
color: fs.iron,
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
user.username,
|
|
style: TextStyle(
|
|
color: fs.parchment,
|
|
fontFamily: 'Fraunces',
|
|
fontSize: 20,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SwitchListTile(
|
|
key: const Key('user_edit_is_admin'),
|
|
title: Text('Admin', style: TextStyle(color: fs.parchment)),
|
|
value: user.isAdmin,
|
|
onChanged: (v) async {
|
|
try {
|
|
await users.setAdmin(user.id, v);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
},
|
|
),
|
|
SwitchListTile(
|
|
key: const Key('user_edit_auto_approve'),
|
|
title: Text('Auto-approve requests',
|
|
style: TextStyle(color: fs.parchment)),
|
|
value: user.autoApproveRequests,
|
|
onChanged: (v) async {
|
|
try {
|
|
await users.setAutoApprove(user.id, v);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
key: const Key('user_edit_reset_password'),
|
|
onPressed: () => _resetPassword(context, users),
|
|
child: const Text('Reset password'),
|
|
),
|
|
const Spacer(),
|
|
TextButton(
|
|
key: const Key('user_edit_delete'),
|
|
style: TextButton.styleFrom(foregroundColor: fs.oxblood),
|
|
onPressed: () => _deleteUser(context, users),
|
|
child: const Text('Delete'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _resetPassword(
|
|
BuildContext context, AdminUsersController users) async {
|
|
final controller = TextEditingController();
|
|
final newPw = await showDialog<String>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Reset password'),
|
|
content: TextField(
|
|
controller: controller,
|
|
obscureText: true,
|
|
decoration: const InputDecoration(
|
|
labelText: 'New password',
|
|
helperText: 'Minimum 8 characters',
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Cancel'),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
if (controller.text.length >= 8) {
|
|
Navigator.pop(ctx, controller.text);
|
|
}
|
|
},
|
|
child: const Text('Reset'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (newPw == null || !context.mounted) return;
|
|
try {
|
|
await users.resetPassword(user.id, newPw);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Password reset.')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteUser(
|
|
BuildContext context, AdminUsersController users) async {
|
|
if (!await TypedConfirmSheet.show(
|
|
context,
|
|
title: 'Delete user?',
|
|
message:
|
|
'Permanently delete user "${user.username}" and all their data. '
|
|
'Cannot be undone.',
|
|
)) return;
|
|
try {
|
|
await users.delete(user.id);
|
|
if (context.mounted) Navigator.pop(context);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
}
|
|
}
|