Neda/admin_panel/lib/screens/groups_screen.dart

436 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../models/group_model.dart';
import '../providers/group_provider.dart';
import '../theme/app_theme.dart';
import '../widgets/app_sidebar.dart';
import '../widgets/responsive_layout.dart';
class GroupsScreen extends StatefulWidget {
const GroupsScreen({super.key});
@override
State<GroupsScreen> createState() => _GroupsScreenState();
}
class _GroupsScreenState extends State<GroupsScreen> {
final _searchCtrl = TextEditingController();
String _search = '';
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<GroupProvider>().loadGroups();
});
}
@override
void dispose() {
_searchCtrl.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ResponsiveLayout(
title: 'گروه‌ها',
sidebar: const AppSidebar(),
body: _GroupsBody(
search: _search,
searchCtrl: _searchCtrl,
onSearch: (v) => setState(() => _search = v.toLowerCase()),
),
);
}
}
class _GroupsBody extends StatelessWidget {
final String search;
final TextEditingController searchCtrl;
final ValueChanged<String> onSearch;
const _GroupsBody({
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.group_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<GroupProvider>(
builder: (_, provider, __) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.status == LoadStatus.error) {
return _ErrorView(
message: provider.error ?? 'خطا',
onRetry: () => provider.loadGroups(),
);
}
final filtered = provider.groups
.where((g) =>
search.isEmpty || g.name.toLowerCase().contains(search))
.toList();
if (filtered.isEmpty) {
return const _EmptyView(
icon: Icons.groups_outlined,
message: 'گروهی یافت نشد',
);
}
return _GroupsTable(groups: filtered);
},
),
),
],
),
);
}
void _showCreateDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => const _CreateGroupDialog(),
);
}
}
// ── Create Group Dialog ──────────────────────────────────────────────────────
class _CreateGroupDialog extends StatefulWidget {
const _CreateGroupDialog();
@override
State<_CreateGroupDialog> createState() => _CreateGroupDialogState();
}
class _CreateGroupDialogState extends State<_CreateGroupDialog> {
final _formKey = GlobalKey<FormState>();
final _nameCtrl = TextEditingController();
bool _loading = false;
String? _error;
@override
void dispose() {
_nameCtrl.dispose();
super.dispose();
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_loading = true;
_error = null;
});
final provider = context.read<GroupProvider>();
final group = await provider.createGroup(
_nameCtrl.text.trim(),
);
if (!mounted) return;
setState(() => _loading = false);
if (group != null) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('گروه «${group.name}» با موفقیت ایجاد شد'),
backgroundColor: AppTheme.success,
),
);
} else {
setState(() => _error = provider.error);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.group_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: _nameCtrl,
decoration: const InputDecoration(
labelText: 'نام گروه',
prefixIcon: Icon(Icons.groups_rounded),
),
validator: (v) => (v == null || v.trim().isEmpty)
? 'نام گروه الزامی است'
: null,
),
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('ایجاد'),
),
],
);
}
}
// ── Groups Table ─────────────────────────────────────────────────────────────
class _GroupsTable extends StatelessWidget {
final List<GroupModel> groups;
const _GroupsTable({required this.groups});
@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('عملیات')),
],
rows: groups.map((g) => _buildRow(context, g)).toList(),
),
),
),
),
);
}
DataRow _buildRow(BuildContext context, GroupModel group) {
return DataRow(
cells: [
DataCell(
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: const Color(0xFF7C3AED).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.groups_rounded,
color: Color(0xFF7C3AED),
size: 18,
),
),
const SizedBox(width: 10),
Text(
group.name,
style: const TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
DataCell(
Text(
group.type.label,
style: const TextStyle(color: AppTheme.textSecondary),
),
),
DataCell(_StatusBadge(isActive: group.isActive)),
DataCell(
TextButton.icon(
onPressed: () => context.go('/groups/${group.id}'),
icon: const Icon(Icons.arrow_forward_rounded, size: 16),
label: const Text('جزئیات'),
),
),
],
);
}
}
// ── Shared small widgets ─────────────────────────────────────────────────────
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('تلاش مجدد'),
),
],
),
);
}
}