MIF_E31222656/lib/screens/admin/group_detail_dialog.dart

587 lines
18 KiB
Dart

import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:tugas_akhir_supabase/core/theme/app_colors.dart';
import 'package:tugas_akhir_supabase/screens/community/models/group.dart';
class GroupMember {
final String userId;
final String username;
final String? avatarUrl;
final String role;
final DateTime joinedAt;
GroupMember({
required this.userId,
required this.username,
this.avatarUrl,
required this.role,
required this.joinedAt,
});
factory GroupMember.fromMap(Map<String, dynamic> map) {
return GroupMember(
userId: map['user_id'],
username: map['username'] ?? 'Unknown User',
avatarUrl: map['avatar_url'],
role: map['role'] ?? 'member',
joinedAt:
map['joined_at'] != null
? DateTime.parse(map['joined_at'])
: DateTime.now(),
);
}
}
class NonMember {
final String userId;
final String username;
final String? avatarUrl;
final String email;
NonMember({
required this.userId,
required this.username,
this.avatarUrl,
required this.email,
});
factory NonMember.fromMap(Map<String, dynamic> map) {
return NonMember(
userId: map['user_id'],
username: map['username'] ?? 'Unknown User',
avatarUrl: map['avatar_url'],
email: map['email'] ?? '',
);
}
}
class GroupDetailDialog extends StatefulWidget {
final Group group;
final Function() onGroupUpdated;
const GroupDetailDialog({
super.key,
required this.group,
required this.onGroupUpdated,
});
@override
State<GroupDetailDialog> createState() => _GroupDetailDialogState();
}
class _GroupDetailDialogState extends State<GroupDetailDialog>
with SingleTickerProviderStateMixin {
late TabController _tabController;
bool _isLoading = true;
List<GroupMember> _members = [];
List<NonMember> _nonMembers = [];
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_loadGroupMembers();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _loadGroupMembers() async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
final supabase = Supabase.instance.client;
// Load group members
final membersResponse = await supabase.rpc(
'get_group_members',
params: {'group_id_param': widget.group.id},
);
if (!mounted) return;
List<GroupMember> members = [];
if (membersResponse != null) {
for (final item in membersResponse) {
members.add(GroupMember.fromMap(item));
}
}
// Load non-members
final nonMembersResponse = await supabase.rpc(
'get_users_not_in_group',
params: {'group_id_param': widget.group.id},
);
if (!mounted) return;
List<NonMember> nonMembers = [];
if (nonMembersResponse != null) {
for (final item in nonMembersResponse) {
nonMembers.add(NonMember.fromMap(item));
}
}
if (mounted) {
setState(() {
_members = members;
_nonMembers = nonMembers;
_isLoading = false;
});
}
} catch (e) {
print('[ERROR] Failed to load group members: $e');
if (mounted) {
setState(() => _isLoading = false);
_showErrorSnackBar(
'Failed to load group members: ${e.toString().split('Exception:').last.trim()}',
);
}
}
}
Future<void> _removeUserFromGroup(String userId) async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
final supabase = Supabase.instance.client;
final result = await supabase.rpc(
'remove_user_from_group',
params: {'user_id_param': userId, 'group_id_param': widget.group.id},
);
if (!mounted) return;
if (result == true) {
_showSuccessSnackBar('User removed from group successfully');
await _loadGroupMembers(); // Reload members
widget.onGroupUpdated(); // Refresh parent
} else {
_showErrorSnackBar('Failed to remove user from group');
setState(() => _isLoading = false);
}
} catch (e) {
print('[ERROR] Failed to remove user from group: $e');
if (mounted) {
_showErrorSnackBar(
'Error: ${e.toString().split('Exception:').last.trim()}',
);
setState(() => _isLoading = false);
}
}
}
Future<void> _addUserToGroup(String userId) async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
final supabase = Supabase.instance.client;
final result = await supabase.rpc(
'add_user_to_group',
params: {
'user_id_param': userId,
'group_id_param': widget.group.id,
'role_param': 'member',
},
);
if (!mounted) return;
if (result == true) {
_showSuccessSnackBar('User added to group successfully');
await _loadGroupMembers(); // Reload members
widget.onGroupUpdated(); // Refresh parent
} else {
_showErrorSnackBar('Failed to add user to group');
setState(() => _isLoading = false);
}
} catch (e) {
print('[ERROR] Failed to add user to group: $e');
if (mounted) {
_showErrorSnackBar(
'Error: ${e.toString().split('Exception:').last.trim()}',
);
setState(() => _isLoading = false);
}
}
}
void _showErrorSnackBar(String message) {
if (!mounted) return;
final scaffoldMessenger = ScaffoldMessenger.of(context);
scaffoldMessenger.clearSnackBars();
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
);
}
void _showSuccessSnackBar(String message) {
if (!mounted) return;
final scaffoldMessenger = ScaffoldMessenger.of(context);
scaffoldMessenger.clearSnackBars();
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.green),
);
}
void _showRemoveConfirmation(GroupMember member) {
if (widget.group.isDefault) {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Remove from Default Group?'),
content: Text(
'This is a default group. "${member.username}" will be temporarily removed but will rejoin if default group settings change.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_removeUserFromGroup(member.userId);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Remove'),
),
],
),
);
} else {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Remove Member?'),
content: Text(
'Are you sure you want to remove "${member.username}" from this group?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_removeUserFromGroup(member.userId);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Remove'),
),
],
),
);
}
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
width: double.maxFinite,
constraints: BoxConstraints(
maxWidth: 600,
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header with group info
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.3),
child: Text(
widget.group.name.substring(0, 1).toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.group.name,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
if (widget.group.isDefault)
Container(
margin: const EdgeInsets.only(
top: 4,
right: 4,
),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Default Group',
style: TextStyle(
fontSize: 12,
color: Colors.white,
),
),
),
Text(
'${_members.length} members',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
],
),
],
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
],
),
if (widget.group.description.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
widget.group.description,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: 14,
),
),
],
],
),
),
// Tabs
TabBar(
controller: _tabController,
labelColor: AppColors.primary,
unselectedLabelColor: Colors.grey,
tabs: const [Tab(text: 'Members'), Tab(text: 'Add Members')],
),
// Tab content
Expanded(
child:
_isLoading
? const Center(child: CircularProgressIndicator())
: TabBarView(
controller: _tabController,
children: [
// Members tab
_buildMembersTab(),
// Add members tab
_buildAddMembersTab(),
],
),
),
],
),
),
);
}
Widget _buildMembersTab() {
if (_members.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'No members in this group',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
Text(
'Add members from the "Add Members" tab',
style: TextStyle(color: Colors.grey[600]),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _members.length,
itemBuilder: (context, index) {
final member = _members[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: AppColors.primary.withOpacity(0.2),
backgroundImage:
member.avatarUrl != null && member.avatarUrl!.isNotEmpty
? NetworkImage(member.avatarUrl!)
: null,
child:
member.avatarUrl == null || member.avatarUrl!.isEmpty
? Text(
member.username.substring(0, 1).toUpperCase(),
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
)
: null,
),
title: Text(
member.username,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
'Joined: ${_formatDate(member.joinedAt)}${member.role}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
trailing: IconButton(
icon: const Icon(Icons.remove_circle_outline, color: Colors.red),
onPressed: () => _showRemoveConfirmation(member),
),
);
},
);
}
Widget _buildAddMembersTab() {
if (_nonMembers.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'All users are already members',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
onPressed: _loadGroupMembers,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
),
],
),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _nonMembers.length,
itemBuilder: (context, index) {
final nonMember = _nonMembers[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: AppColors.primary.withOpacity(0.2),
backgroundImage:
nonMember.avatarUrl != null && nonMember.avatarUrl!.isNotEmpty
? NetworkImage(nonMember.avatarUrl!)
: null,
child:
nonMember.avatarUrl == null || nonMember.avatarUrl!.isEmpty
? Text(
nonMember.username.substring(0, 1).toUpperCase(),
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
)
: null,
),
title: Text(
nonMember.username,
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
nonMember.email,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
trailing: IconButton(
icon: const Icon(Icons.add_circle_outline, color: Colors.green),
onPressed: () => _addUserToGroup(nonMember.userId),
),
);
},
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays < 1) {
return 'Today';
} else if (difference.inDays < 2) {
return 'Yesterday';
} else if (difference.inDays < 7) {
return '${difference.inDays} days ago';
} else {
return '${date.day}/${date.month}/${date.year}';
}
}
}