502 lines
16 KiB
Dart
502 lines
16 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:intl/intl.dart' hide TextDirection;
|
|
import 'package:provider/provider.dart';
|
|
import '../models/group_member_model.dart';
|
|
import '../models/user_model.dart';
|
|
import '../providers/group_provider.dart';
|
|
import '../providers/user_provider.dart';
|
|
import '../theme/app_theme.dart';
|
|
import '../widgets/app_sidebar.dart';
|
|
import '../widgets/responsive_layout.dart';
|
|
|
|
class GroupDetailScreen extends StatefulWidget {
|
|
final String groupId;
|
|
|
|
const GroupDetailScreen({super.key, required this.groupId});
|
|
|
|
@override
|
|
State<GroupDetailScreen> createState() => _GroupDetailScreenState();
|
|
}
|
|
|
|
class _GroupDetailScreenState extends State<GroupDetailScreen> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
final groups = context.read<GroupProvider>();
|
|
groups.loadGroupMembers(widget.groupId);
|
|
if (groups.groups.isEmpty) groups.loadGroups();
|
|
context.read<UserProvider>().loadUsers();
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<GroupProvider>(builder: (_, groupProvider, __) {
|
|
final group = groupProvider.groups
|
|
.where((g) => g.id == widget.groupId)
|
|
.firstOrNull;
|
|
|
|
return ResponsiveLayout(
|
|
title: group?.name ?? 'جزئیات گروه',
|
|
sidebar: const AppSidebar(),
|
|
body: _GroupDetailBody(
|
|
groupId: widget.groupId,
|
|
groupName: group?.name ?? '...',
|
|
groupDescription: group?.description,
|
|
isActive: group?.isActive ?? true,
|
|
createdAt: group?.createdAt,
|
|
),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class _GroupDetailBody extends StatelessWidget {
|
|
final String groupId;
|
|
final String groupName;
|
|
final String? groupDescription;
|
|
final bool isActive;
|
|
final DateTime? createdAt;
|
|
|
|
const _GroupDetailBody({
|
|
required this.groupId,
|
|
required this.groupName,
|
|
required this.groupDescription,
|
|
required this.isActive,
|
|
required this.createdAt,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// ── Back button + Header ──────────────────────────────────
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
onPressed: () => context.go('/groups'),
|
|
icon: const Icon(Icons.arrow_back_rounded),
|
|
tooltip: 'بازگشت',
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
groupName,
|
|
style: const TextStyle(
|
|
fontSize: 24,
|
|
fontWeight: FontWeight.w800,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
),
|
|
ElevatedButton.icon(
|
|
onPressed: () => _showAddMemberDialog(context),
|
|
icon: const Icon(Icons.person_add_rounded, size: 18),
|
|
label: const Text('افزودن عضو'),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
|
|
// ── Group info card ───────────────────────────────────────
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 56,
|
|
height: 56,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF7C3AED).withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(14),
|
|
),
|
|
child: const Icon(
|
|
Icons.groups_rounded,
|
|
color: Color(0xFF7C3AED),
|
|
size: 28,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Text(
|
|
groupName,
|
|
style: const TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
_StatusBadge(isActive: isActive),
|
|
],
|
|
),
|
|
if (groupDescription != null) ...[
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
groupDescription!,
|
|
style: const TextStyle(
|
|
color: AppTheme.textSecondary,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
createdAt != null
|
|
? 'ایجاد شده در ${DateFormat('yyyy/MM/dd').format(createdAt!)}'
|
|
: '',
|
|
style: const TextStyle(
|
|
color: AppTheme.textSecondary,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: 24),
|
|
|
|
// ── Members section ───────────────────────────────────────
|
|
const Text(
|
|
'اعضای گروه',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.w700,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
_MembersTable(groupId: groupId),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showAddMemberDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => _AddMemberDialog(groupId: groupId),
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Add Member Dialog ────────────────────────────────────────────────────────
|
|
|
|
class _AddMemberDialog extends StatefulWidget {
|
|
final String groupId;
|
|
const _AddMemberDialog({required this.groupId});
|
|
|
|
@override
|
|
State<_AddMemberDialog> createState() => _AddMemberDialogState();
|
|
}
|
|
|
|
class _AddMemberDialogState extends State<_AddMemberDialog> {
|
|
UserModel? _selectedUser;
|
|
GroupRole _role = GroupRole.member;
|
|
bool _loading = false;
|
|
String? _error;
|
|
|
|
Future<void> _submit() async {
|
|
if (_selectedUser == null) {
|
|
setState(() => _error = 'لطفاً یک کاربر انتخاب کنید');
|
|
return;
|
|
}
|
|
setState(() {
|
|
_loading = true;
|
|
_error = null;
|
|
});
|
|
|
|
final provider = context.read<GroupProvider>();
|
|
final currentMembers = provider.membersOf(widget.groupId);
|
|
if (currentMembers.any((m) => m.userId == _selectedUser!.id)) {
|
|
setState(() {
|
|
_loading = false;
|
|
_error = 'این کاربر قبلاً عضو این گروه است';
|
|
});
|
|
return;
|
|
}
|
|
|
|
final result = await provider.addMember(
|
|
widget.groupId,
|
|
_selectedUser!.id,
|
|
_role,
|
|
_selectedUser!.username,
|
|
);
|
|
|
|
if (!mounted) return;
|
|
setState(() => _loading = false);
|
|
|
|
if (result != null) {
|
|
Navigator.of(context).pop();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content:
|
|
Text('«${_selectedUser!.username}» به گروه اضافه شد'),
|
|
backgroundColor: AppTheme.success,
|
|
),
|
|
);
|
|
} else {
|
|
setState(() => _error = provider.error);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<UserProvider>(builder: (_, userProvider, __) {
|
|
final users = userProvider.users;
|
|
|
|
return AlertDialog(
|
|
title: const Row(
|
|
children: [
|
|
Icon(Icons.person_add_rounded, color: AppTheme.primary),
|
|
SizedBox(width: 10),
|
|
Text('افزودن عضو به گروه'),
|
|
],
|
|
),
|
|
content: SizedBox(
|
|
width: 400,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
// User selector
|
|
DropdownButtonFormField<UserModel>(
|
|
initialValue: _selectedUser,
|
|
decoration: const InputDecoration(
|
|
labelText: 'انتخاب کاربر',
|
|
prefixIcon: Icon(Icons.person_outline_rounded),
|
|
),
|
|
hint: const Text('کاربر را انتخاب کنید'),
|
|
items: users
|
|
.map((u) => DropdownMenuItem(
|
|
value: u,
|
|
child: Text(
|
|
'${u.username} (${u.role.label})',
|
|
),
|
|
))
|
|
.toList(),
|
|
onChanged: (v) => setState(() => _selectedUser = v),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Role selector
|
|
DropdownButtonFormField<GroupRole>(
|
|
initialValue: _role,
|
|
decoration: const InputDecoration(
|
|
labelText: 'نقش در گروه',
|
|
prefixIcon: Icon(Icons.badge_outlined),
|
|
),
|
|
items: GroupRole.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('افزودن'),
|
|
),
|
|
],
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Members Table ────────────────────────────────────────────────────────────
|
|
|
|
class _MembersTable extends StatelessWidget {
|
|
final String groupId;
|
|
const _MembersTable({required this.groupId});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Consumer<GroupProvider>(builder: (_, provider, __) {
|
|
final members = provider.membersOf(groupId);
|
|
|
|
if (members.isEmpty) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(40),
|
|
child: Center(
|
|
child: Column(
|
|
children: [
|
|
const Icon(Icons.people_outline_rounded,
|
|
size: 56, color: AppTheme.border),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'هنوز عضوی به این گروه اضافه نشده است',
|
|
style: TextStyle(color: AppTheme.textSecondary),
|
|
),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton.icon(
|
|
onPressed: () => showDialog(
|
|
context: context,
|
|
builder: (_) => _AddMemberDialog(groupId: groupId),
|
|
),
|
|
icon: const Icon(Icons.person_add_rounded, size: 16),
|
|
label: const Text('افزودن اولین عضو'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
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('تاریخ عضویت')),
|
|
],
|
|
rows: members.map((m) => _buildRow(m)).toList(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
DataRow _buildRow(GroupMemberModel member) {
|
|
final color = member.role == GroupRole.manager
|
|
? const Color(0xFF0891B2)
|
|
: AppTheme.textSecondary;
|
|
|
|
return DataRow(
|
|
cells: [
|
|
DataCell(
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
CircleAvatar(
|
|
radius: 16,
|
|
backgroundColor: AppTheme.primary.withValues(alpha: 0.12),
|
|
child: Text(
|
|
(member.username ?? member.userId)[0].toUpperCase(),
|
|
style: const TextStyle(
|
|
color: AppTheme.primary,
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
member.username ?? member.userId,
|
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
DataCell(
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: color.withValues(alpha: 0.12),
|
|
borderRadius: BorderRadius.circular(20),
|
|
),
|
|
child: Text(
|
|
member.role.label,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
member.joinedAt != null
|
|
? DateFormat('yyyy/MM/dd').format(member.joinedAt!)
|
|
: '—',
|
|
style: const TextStyle(color: AppTheme.textSecondary),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// ── Status Badge ─────────────────────────────────────────────────────────────
|
|
|
|
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: Text(
|
|
isActive ? 'فعال' : 'غیرفعال',
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|