Neda/admin_panel/lib/screens/users_screen.dart
2026-03-07 19:18:52 +03:30

552 lines
16 KiB
Dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart' hide TextDirection;
import 'package:provider/provider.dart';
import '../models/user_model.dart';
import '../providers/user_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/app_sidebar.dart';
import '../widgets/responsive_layout.dart';
import '../widgets/secret_dialog.dart';
class UsersScreen extends StatefulWidget {
const UsersScreen({super.key});
@override
State<UsersScreen> createState() => _UsersScreenState();
}
class _UsersScreenState extends State<UsersScreen> {
final _searchCtrl = TextEditingController();
String _search = '';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<UserProvider>().loadUsers();
});
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
title: 'کاربران',
sidebar: const AppSidebar(),
body: _UsersBody(
search: _search,
searchCtrl: _searchCtrl,
onSearch: (v) => setState(() => _search = v.toLowerCase()),
),
);
}
}
class _UsersBody extends StatelessWidget {
final String search;
final TextEditingController searchCtrl;
final ValueChanged<String> onSearch;
const _UsersBody({
required this.search,
required this.searchCtrl,
required this.onSearch,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Header ───────────────────────────────────────────────
Row(
children: [
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'مدیریت کاربران',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppTheme.textPrimary,
),
),
SizedBox(height: 4),
Text(
'ایجاد و مدیریت کاربران سیستم',
style: TextStyle(
fontSize: 14, color: AppTheme.textSecondary),
),
],
),
),
ElevatedButton.icon(
onPressed: () => _showCreateDialog(context),
icon: const Icon(Icons.person_add_rounded, size: 18),
label: const Text('کاربر جدید'),
),
],
),
const SizedBox(height: 20),
// ── Search ───────────────────────────────────────────────
TextField(
controller: searchCtrl,
onChanged: onSearch,
decoration: const InputDecoration(
hintText: 'جستجوی کاربر...',
prefixIcon: Icon(Icons.search_rounded),
),
),
const SizedBox(height: 16),
// ── Table ────────────────────────────────────────────────
Expanded(
child: Consumer<UserProvider>(
builder: (_, provider, __) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.status.name == 'error') {
return _ErrorView(
message: provider.error ?? 'خطا',
onRetry: () => provider.loadUsers());
}
final filtered = provider.users
.where((u) =>
search.isEmpty ||
u.username.toLowerCase().contains(search) ||
u.role.label.toLowerCase().contains(search))
.toList();
if (filtered.isEmpty) {
return const _EmptyView(
icon: Icons.people_outline_rounded,
message: 'کاربری یافت نشد',
);
}
return _UsersTable(users: filtered);
},
),
),
],
),
);
}
void _showCreateDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => const _CreateUserDialog(),
);
}
}
// ── Create User Dialog ──────────────────────────────────────────────────────
class _CreateUserDialog extends StatefulWidget {
const _CreateUserDialog();
@override
State<_CreateUserDialog> createState() => _CreateUserDialogState();
}
class _CreateUserDialogState extends State<_CreateUserDialog> {
final _formKey = GlobalKey<FormState>();
final _usernameCtrl = TextEditingController();
UserRole _role = UserRole.member;
bool _loading = false;
String? _error;
@override
void dispose() {
_usernameCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
final provider = context.read<UserProvider>();
final result = await provider.createUser(
_usernameCtrl.text.trim(),
_role,
);
if (!mounted) return;
setState(() => _loading = false);
if (result != null) {
Navigator.of(context).pop();
await SecretDialog.show(
context,
username: result.user.username,
secret: result.secret,
);
} else {
setState(() => _error = provider.error);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.person_add_rounded, color: AppTheme.primary),
SizedBox(width: 10),
Text('ایجاد کاربر جدید'),
],
),
content: SizedBox(
width: 400,
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _usernameCtrl,
textDirection: TextDirection.ltr,
decoration: const InputDecoration(
labelText: 'نام کاربری',
prefixIcon: Icon(Icons.person_outline_rounded),
),
validator: (v) =>
(v == null || v.trim().isEmpty) ? 'نام کاربری الزامی است' : null,
),
const SizedBox(height: 16),
DropdownButtonFormField<UserRole>(
initialValue: _role,
decoration: const InputDecoration(
labelText: 'نقش',
prefixIcon: Icon(Icons.badge_outlined),
),
items: UserRole.values
.map((r) => DropdownMenuItem(
value: r,
child: Text(r.label),
))
.toList(),
onChanged: (v) => setState(() => _role = v!),
),
if (_error != null) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppTheme.danger.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.danger.withValues(alpha: 0.3)),
),
child: Text(
_error!,
style: const TextStyle(color: AppTheme.danger, fontSize: 13),
),
),
],
],
),
),
),
actions: [
TextButton(
onPressed: _loading ? null : () => Navigator.of(context).pop(),
child: const Text('انصراف'),
),
ElevatedButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
color: Colors.white, strokeWidth: 2))
: const Text('ایجاد'),
),
],
);
}
}
// ── Users Table ──────────────────────────────────────────────────────────────
class _UsersTable extends StatelessWidget {
final List<UserModel> users;
const _UsersTable({required this.users});
@override
Widget build(BuildContext context) {
return Card(
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: const [
DataColumn(label: Text('نام کاربری')),
DataColumn(label: Text('نقش')),
DataColumn(label: Text('وضعیت')),
DataColumn(label: Text('تاریخ ایجاد')),
DataColumn(label: Text('عملیات')),
],
rows: users.map((user) => _buildRow(context, user)).toList(),
),
),
),
),
);
}
DataRow _buildRow(BuildContext context, UserModel user) {
return DataRow(
cells: [
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
CircleAvatar(
radius: 16,
backgroundColor: AppTheme.primary.withValues(alpha: 0.12),
child: Text(
user.username[0].toUpperCase(),
style: const TextStyle(
color: AppTheme.primary,
fontSize: 13,
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 10),
Text(
user.username,
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
DataCell(_RoleBadge(role: user.role)),
DataCell(_StatusBadge(isActive: user.isActive)),
DataCell(
Text(
user.createdAt != null
? DateFormat('yyyy/MM/dd').format(user.createdAt!)
: '',
style: const TextStyle(color: AppTheme.textSecondary),
),
),
DataCell(
_ResetSecretButton(user: user),
),
],
);
}
}
class _ResetSecretButton extends StatefulWidget {
final UserModel user;
const _ResetSecretButton({required this.user});
@override
State<_ResetSecretButton> createState() => _ResetSecretButtonState();
}
class _ResetSecretButtonState extends State<_ResetSecretButton> {
bool _loading = false;
Future<void> _reset() async {
final confirmed = await showDialog<bool>(
context: context,
builder: (_) => AlertDialog(
title: const Text('تأیید ریست رمز'),
content: Text(
'آیا مطمئن هستید که می‌خواهید رمز «${widget.user.username}» را ریست کنید؟'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('انصراف'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(backgroundColor: AppTheme.warning),
child: const Text('ریست'),
),
],
),
);
if (confirmed != true || !mounted) return;
setState(() => _loading = true);
final provider = context.read<UserProvider>();
final secret = await provider.resetSecret(widget.user.id);
if (!mounted) return;
setState(() => _loading = false);
if (secret != null) {
await SecretDialog.show(
context,
username: widget.user.username,
secret: secret,
isReset: true,
);
} else if (provider.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(provider.error!),
backgroundColor: AppTheme.danger,
),
);
}
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2));
}
return TextButton.icon(
onPressed: _reset,
icon: const Icon(Icons.lock_reset_rounded, size: 16),
label: const Text('ریست رمز'),
style: TextButton.styleFrom(foregroundColor: AppTheme.warning),
);
}
}
// ── Shared small widgets ─────────────────────────────────────────────────────
class _RoleBadge extends StatelessWidget {
final UserRole role;
const _RoleBadge({required this.role});
@override
Widget build(BuildContext context) {
final color = AppTheme.roleColor(role.name);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Text(
role.label,
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
);
}
}
class _StatusBadge extends StatelessWidget {
final bool isActive;
const _StatusBadge({required this.isActive});
@override
Widget build(BuildContext context) {
final color = isActive ? AppTheme.success : AppTheme.danger;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 5),
Text(
isActive ? 'فعال' : 'غیرفعال',
style: TextStyle(
color: color,
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
class _EmptyView extends StatelessWidget {
final IconData icon;
final String message;
const _EmptyView({required this.icon, required this.message});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 64, color: AppTheme.border),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
color: AppTheme.textSecondary, fontSize: 16),
),
],
),
);
}
}
class _ErrorView extends StatelessWidget {
final String message;
final VoidCallback onRetry;
const _ErrorView({required this.message, required this.onRetry});
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline_rounded,
size: 64, color: AppTheme.danger),
const SizedBox(height: 16),
Text(message,
style: const TextStyle(color: AppTheme.textSecondary)),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh_rounded),
label: const Text('تلاش مجدد'),
),
],
),
);
}
}