MIF_E31222656/lib/screens/admin/community_management.dart

1421 lines
50 KiB
Dart

import 'package:flutter/material.dart';
import 'package:tugas_akhir_supabase/core/theme/app_colors.dart';
import 'package:tugas_akhir_supabase/screens/community/models/group.dart';
import 'package:tugas_akhir_supabase/screens/community/services/group_management_service.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:convert';
import 'package:tugas_akhir_supabase/screens/admin/group_detail_dialog.dart';
import 'package:flutter/rendering.dart';
// Model untuk menampilkan pengguna dan grup yang mereka ikuti
class UserWithGroups {
final String userId;
final String username;
final String email;
final String? avatarUrl;
final bool isActive;
final DateTime? lastSignIn;
final List<UserGroup> groups;
UserWithGroups({
required this.userId,
required this.username,
required this.email,
this.avatarUrl,
required this.isActive,
this.lastSignIn,
required this.groups,
});
}
class UserGroup {
final String groupId;
final String groupName;
final String role;
UserGroup({
required this.groupId,
required this.groupName,
required this.role,
});
}
class CommunityManagement extends StatefulWidget {
const CommunityManagement({super.key});
@override
_CommunityManagementState createState() => _CommunityManagementState();
}
class _CommunityManagementState extends State<CommunityManagement>
with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
final _groupManagementService = GroupManagementService();
late TabController _tabController;
// State variables
bool _isLoading = true;
List<Group> _groups = [];
List<UserWithGroups> _users = [];
bool _isLoadingUsers = true;
// Text controllers for group creation/editing
final _groupNameController = TextEditingController();
final _groupDescriptionController = TextEditingController();
bool _isPublicGroup = true;
bool _isDefaultGroup = true;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_loadGroups();
_loadUsers(); // Load users data
}
@override
void dispose() {
_tabController.dispose();
_groupNameController.dispose();
_groupDescriptionController.dispose();
super.dispose();
}
@override
bool get wantKeepAlive => true;
Future<void> _loadGroups() async {
print('[DEBUG] _loadGroups called');
if (!mounted) return;
setState(() => _isLoading = true);
try {
print('[DEBUG] Admin: Loading all groups directly from database...');
// Gunakan pendekatan langsung tanpa mengandalkan service yang mungkin terpengaruh RLS
final supabase = Supabase.instance.client;
final response = await supabase.rpc('get_all_groups_for_admin');
if (!mounted) return;
print('[DEBUG] Admin: Raw response: $response');
List<Group> groups = [];
if (response != null) {
for (final item in response) {
try {
// The RPC now returns member_count directly
final group = Group.fromMap(item);
groups.add(group);
print(
'[DEBUG] Admin: Added group: ${group.name} (${group.id}) with ${group.memberCount} members',
);
} catch (e) {
print('[ERROR] Admin: Failed to parse group: $e');
}
}
}
print(
'[DEBUG] Admin: Loaded ${groups.length} groups directly from database',
);
if (mounted) {
setState(() {
_groups = groups;
_isLoading = false;
});
print('[DEBUG] Admin: State updated with ${_groups.length} groups');
}
} catch (e) {
print('[ERROR] Admin: Failed to load groups: $e');
if (mounted) {
setState(() => _isLoading = false);
_showErrorSnackBar('Failed to load groups. Please try again.');
}
}
// Safety timer untuk mengatasi kemungkinan loading yang tidak berhenti
Future.delayed(Duration(seconds: 5), () {
if (mounted && _isLoading) {
print('[WARNING] Admin: Forcing load completion after timeout');
setState(() => _isLoading = false);
}
});
}
Future<void> _loadUsers() async {
if (!mounted) return;
setState(() => _isLoadingUsers = true);
try {
print('[DEBUG] Admin: Loading all users with their groups...');
// Menggunakan fungsi yang mengembalikan pengguna beserta grup
final supabase = Supabase.instance.client;
// Coba menggunakan fungsi get_users_with_groups
dynamic response;
try {
// Sinkronkan pengguna ke grup default terlebih dahulu
final syncResult = await supabase.rpc('sync_users_to_default_groups');
if (!mounted) return;
int added = 0, already = 0, reactivated = 0;
if (syncResult != null && syncResult is List) {
for (final item in syncResult) {
if (item['status'] == 'added_new') added++;
if (item['status'] == 'already_member') already++;
if (item['status'] == 'reactivated') reactivated++;
}
}
print('[DEBUG] Admin: Synchronized users to default groups:');
print(' - Added: $added users');
print(' - Already members: $already users');
print(' - Reactivated: $reactivated users');
// Sekarang ambil data pengguna
response = await supabase.rpc('get_users_with_groups');
if (!mounted) return;
print(
'[DEBUG] Admin: Successfully used get_users_with_groups function',
);
} catch (e) {
if (!mounted) return;
print('[WARNING] Admin: get_users_with_groups function failed: $e');
// Fallback ke metode lama jika fungsi tidak tersedia
response = await supabase.rpc('get_users_for_admin');
if (!mounted) return;
print('[DEBUG] Admin: Fallback to get_users_for_admin function');
}
print('[DEBUG] Admin: Raw users response: $response');
List<UserWithGroups> users = [];
if (response != null) {
for (final dynamic item in response) {
try {
// Konversi item ke UserWithGroups
Map<String, dynamic> userData;
if (item is Map) {
userData = Map<String, dynamic>.from(item);
} else if (item is String) {
// Jika dikembalikan sebagai JSON string
userData = jsonDecode(item) as Map<String, dynamic>;
} else {
continue; // Skip item yang tidak valid
}
final userId = userData['user_id'] as String? ?? '';
if (userId.isEmpty) continue; // Skip jika user_id kosong
// Mendapatkan grup yang diikuti pengguna
List<UserGroup> groups = [];
if (userData.containsKey('groups') && userData['groups'] != null) {
final groupsData = userData['groups'];
dynamic groupsList;
if (groupsData is String) {
groupsList = jsonDecode(groupsData);
} else {
groupsList = groupsData;
}
if (groupsList is List) {
for (final groupData in groupsList) {
Map<String, dynamic> groupMap;
if (groupData is Map) {
groupMap = Map<String, dynamic>.from(groupData);
} else if (groupData is String) {
groupMap = jsonDecode(groupData) as Map<String, dynamic>;
} else {
continue;
}
groups.add(
UserGroup(
groupId: groupMap['group_id'] as String? ?? '',
groupName:
groupMap['group_name'] as String? ?? 'Unknown Group',
role:
groupMap['is_default'] == true
? 'default member'
: 'member',
),
);
}
}
}
users.add(
UserWithGroups(
userId: userId,
username: userData['username'] as String? ?? 'Unknown User',
email: userData['email'] as String? ?? 'No Email',
avatarUrl: userData['avatar_url'] as String?,
isActive: userData['is_active'] as bool? ?? true,
lastSignIn:
null, // Mengabaikan last_sign_in untuk menghindari error
groups: groups,
),
);
} catch (e) {
print('[ERROR] Admin: Failed to parse user data: $e');
}
}
}
print('[DEBUG] Admin: Loaded ${users.length} users');
if (mounted) {
setState(() {
_users = users;
_isLoadingUsers = false;
});
print('[DEBUG] Admin: State updated with ${_users.length} users');
}
} catch (e) {
print('[ERROR] Admin: Failed to load users: $e');
if (mounted) {
setState(() => _isLoadingUsers = false);
_showErrorSnackBar(
'Failed to load users: ${e.toString().split('Exception:').last.trim()}',
);
}
}
}
Future<void> _toggleUserStatus(String userId, bool newStatus) async {
if (!mounted) return;
setState(() => _isLoadingUsers = true);
try {
print('[DEBUG] Admin: Toggling user status: $userId to $newStatus');
// Panggil RPC untuk mengubah status pengguna
final supabase = Supabase.instance.client;
final result = await supabase.rpc(
'toggle_user_status',
params: {'user_id_param': userId, 'is_active_param': newStatus},
);
if (!mounted) return;
if (result == true) {
_showSuccessSnackBar('User status updated successfully');
_loadUsers(); // Reload user list
} else {
_showErrorSnackBar('Failed to update user status');
setState(() => _isLoadingUsers = false);
}
} catch (e) {
print('[ERROR] Admin: Failed to toggle user status: $e');
if (mounted) {
_showErrorSnackBar(
'Error: ${e.toString().split('Exception:').last.trim()}',
);
setState(() => _isLoadingUsers = false);
}
}
}
Future<void> _removeUserFromGroup(String userId, String groupId) async {
if (!mounted) return;
setState(() => _isLoadingUsers = true);
try {
print('[DEBUG] Admin: Removing user $userId from group $groupId');
// Panggil RPC untuk menghapus pengguna dari grup
final supabase = Supabase.instance.client;
final result = await supabase.rpc(
'remove_user_from_group',
params: {'user_id_param': userId, 'group_id_param': groupId},
);
if (!mounted) return;
if (result == true) {
_showSuccessSnackBar('User removed from group successfully');
_loadUsers(); // Reload user list
} else {
_showErrorSnackBar('Failed to remove user from group');
setState(() => _isLoadingUsers = false);
}
} catch (e) {
print('[ERROR] Admin: Failed to remove user from group: $e');
if (mounted) {
_showErrorSnackBar(
'Error: ${e.toString().split('Exception:').last.trim()}',
);
setState(() => _isLoadingUsers = false);
}
}
}
void _showCreateGroupDialog() {
// Reset form
_groupNameController.text = '';
_groupDescriptionController.text = '';
_isPublicGroup = true; // Tetap true karena kita menghilangkan opsi ini
_isDefaultGroup = false; // Default ke false untuk grup baru
// Cek apakah sudah ada grup default
bool hasDefaultGroup = _groups.any((group) => group.isDefault);
showDialog(
context: context,
builder:
(context) => StatefulBuilder(
builder: (BuildContext context, StateSetter setDialogState) {
return AlertDialog(
title: const Text('Create New Group'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _groupNameController,
decoration: const InputDecoration(
labelText: 'Group Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _groupDescriptionController,
decoration: const InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Default Group',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'All new users join automatically',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
if (!hasDefaultGroup)
Switch(
value: _isDefaultGroup,
activeColor: Colors.green,
onChanged: (value) {
setDialogState(() {
_isDefaultGroup = value;
});
},
)
else
Tooltip(
message: 'Default group already exists',
child: Switch(
value: false,
activeColor: Colors.grey,
onChanged: null,
),
),
],
),
),
if (hasDefaultGroup)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Icon(
Icons.info_outline,
color: Colors.orange,
size: 16,
),
SizedBox(width: 8),
Expanded(
child: Text(
'Default group already exists',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
fontSize: 12,
),
),
),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// Jika sudah ada grup default, pastikan grup baru tidak menjadi default
if (hasDefaultGroup) {
_isDefaultGroup = false;
}
_createGroup();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
child: const Text('Create'),
),
],
);
},
),
);
}
Future<void> _createGroup() async {
final name = _groupNameController.text.trim();
final description = _groupDescriptionController.text.trim();
if (name.isEmpty) {
_showErrorSnackBar('Group name cannot be empty');
return;
}
if (!mounted) return;
setState(() => _isLoading = true);
try {
final userId = _groupManagementService.currentUserId;
if (userId == null) {
throw Exception('User not authenticated');
}
// Periksa kembali apakah sudah ada grup default
bool hasDefaultGroup = _groups.any((group) => group.isDefault);
// Jika sudah ada grup default, maka grup baru tidak boleh menjadi default
if (hasDefaultGroup) {
_isDefaultGroup = false;
}
// Gunakan RPC untuk membuat grup secara atomik
final createdGroup = await _groupManagementService.createGroupViaRPC(
name: name,
description: description,
isPublic: _isPublicGroup,
isDefault: _isDefaultGroup,
);
if (!mounted) return;
if (createdGroup != null) {
_showSuccessSnackBar('Group created successfully');
// Jika grup adalah default, sinkronkan semua pengguna
if (_isDefaultGroup) {
try {
final supabase = Supabase.instance.client;
final result = await supabase.rpc('sync_users_to_default_groups');
if (!mounted) return;
// Log hasil
int added = 0, already = 0, reactivated = 0;
if (result != null && result is List) {
for (final item in result) {
if (item['status'] == 'added_new') added++;
if (item['status'] == 'already_member') already++;
if (item['status'] == 'reactivated') reactivated++;
}
}
print('[DEBUG] Admin: Synchronized all users to default groups:');
print(' - Added: $added users');
print(' - Already members: $already users');
print(' - Reactivated: $reactivated users');
// Jika ada yang berhasil ditambahkan, tampilkan pesan
if (added > 0 || reactivated > 0) {
_showSuccessSnackBar(
'Added ${added + reactivated} users to the default group',
);
}
} catch (e) {
print(
'[WARNING] Admin: Failed to sync users to default groups: $e',
);
}
}
// Refresh groups list dan user list
_loadGroups();
_loadUsers();
} else {
_showErrorSnackBar('Failed to create group');
setState(() => _isLoading = false);
}
} catch (e) {
print('[ERROR] Failed to create group: $e');
if (mounted) {
_showErrorSnackBar(
'Error: ${e.toString().split('Exception:').last.trim()}',
);
setState(() => _isLoading = false);
}
}
}
Future<void> _showDeleteGroupConfirmation(Group group) async {
final shouldDelete = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Delete Group'),
content: Text(
'Are you sure you want to delete "${group.name}"? '
'This action cannot be undone and will delete all messages in this group.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
),
child: const Text('Delete'),
),
],
),
);
if (shouldDelete == true) {
await _deleteGroup(group);
}
}
Future<void> _deleteGroup(Group group) async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
print(
'[DEBUG] Admin: Attempting to delete group: ${group.id} (${group.name})',
);
final success = await _groupManagementService.deleteGroup(group.id);
if (!mounted) return;
if (success) {
print('[DEBUG] Admin: Group deleted successfully');
_showSuccessSnackBar('Group deleted successfully');
_loadGroups(); // Refresh groups list
} else {
print('[DEBUG] Admin: Group deletion failed - Service returned false');
_showErrorSnackBar(
'Failed to delete group. It may be a default group which cannot be deleted.',
);
setState(() => _isLoading = false);
}
} catch (e) {
print('[ERROR] Admin: Failed to delete group: $e');
if (mounted) {
_showErrorSnackBar(
'Error: ${e.toString().split('Exception:').last.trim()}',
);
setState(() => _isLoading = false);
}
}
}
void _showSuccessSnackBar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.green),
);
}
void _showErrorSnackBar(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
);
}
void _showEditGroupDialog(Group group) {
// Set nilai awal
_groupNameController.text = group.name;
_groupDescriptionController.text = group.description;
_isPublicGroup = group.isPublic;
_isDefaultGroup = group.isDefault;
// Cek apakah ada grup default lain selain grup ini
bool hasOtherDefaultGroup = _groups.any(
(g) => g.isDefault && g.id != group.id,
);
showDialog(
context: context,
builder:
(context) => StatefulBuilder(
builder: (BuildContext context, StateSetter setDialogState) {
return AlertDialog(
title: const Text('Edit Group'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _groupNameController,
decoration: const InputDecoration(
labelText: 'Group Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _groupDescriptionController,
decoration: const InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
maxLines: 3,
),
const SizedBox(height: 16),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
color: group.isDefault ? Colors.green.shade50 : null,
),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Default Group',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color:
group.isDefault
? Colors.green.shade700
: null,
),
),
Text(
'All new users join automatically',
style: TextStyle(
color:
group.isDefault
? Colors.green.shade700
: Colors.grey[600],
fontSize: 12,
),
),
],
),
),
if (group.isDefault)
// Jika grup ini adalah default, tampilkan switch yang aktif dan tidak bisa diubah
Tooltip(
message:
'This is a default group and cannot be changed',
child: Switch(
value: true,
activeColor: Colors.green,
onChanged: null, // Tidak bisa diubah
),
)
else if (hasOtherDefaultGroup)
// Jika ada grup default lain, switch tidak aktif dan tidak bisa diubah
Tooltip(
message: 'Default group already exists',
child: Switch(
value: false,
activeColor: Colors.grey,
onChanged: null, // Tidak bisa diubah
),
)
else
// Jika tidak ada grup default lain, bisa mengubah grup ini menjadi default
Switch(
value: _isDefaultGroup,
activeColor: Colors.green,
onChanged: (value) {
setDialogState(() {
_isDefaultGroup = value;
});
},
),
],
),
),
if (hasOtherDefaultGroup && !group.isDefault)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Icon(
Icons.info_outline,
color: Colors.orange,
size: 16,
),
SizedBox(width: 8),
Expanded(
child: Text(
'Default group already exists',
style: TextStyle(
color: Colors.grey[600],
fontStyle: FontStyle.italic,
fontSize: 12,
),
),
),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// Jika grup ini adalah default, tetap default
// Jika ada grup default lain, grup ini tidak bisa menjadi default
if (hasOtherDefaultGroup && !group.isDefault) {
_isDefaultGroup = false;
}
_updateGroup(group);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
child: const Text('Update'),
),
],
);
},
),
);
}
Future<void> _updateGroup(Group group) async {
final name = _groupNameController.text.trim();
final description = _groupDescriptionController.text.trim();
if (name.isEmpty) {
_showErrorSnackBar('Group name cannot be empty');
return;
}
if (!mounted) return;
setState(() => _isLoading = true);
try {
// Periksa kembali apakah ada grup default lain
bool hasOtherDefaultGroup = _groups.any(
(g) => g.isDefault && g.id != group.id,
);
// Jika grup ini bukan default dan ada grup default lain, maka tidak bisa menjadi default
if (hasOtherDefaultGroup && !group.isDefault) {
_isDefaultGroup = false;
}
// Jika grup ini adalah default, tetap pertahankan sebagai default
if (group.isDefault) {
_isDefaultGroup = true;
}
// Buat objek grup yang diperbarui
final updatedGroup = Group(
id: group.id,
name: name,
description: description,
createdBy: group.createdBy,
isPublic: _isPublicGroup, // Selalu true sesuai permintaan
isDefault: _isDefaultGroup,
iconUrl: group.iconUrl,
createdAt: group.createdAt,
memberCount: group.memberCount, // Tambahkan ini untuk mencegah null
);
// Panggil service untuk memperbarui grup
final success = await _groupManagementService.updateGroup(updatedGroup);
if (!mounted) return;
if (success) {
_showSuccessSnackBar('Group updated successfully');
_loadGroups(); // Refresh groups list
} else {
_showErrorSnackBar('Failed to update group');
setState(() => _isLoading = false);
}
} catch (e) {
print('[ERROR] Failed to update group: $e');
if (mounted) {
_showErrorSnackBar(
'Error: ${e.toString().split('Exception:').last.trim()}',
);
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: const Text(
'Community Management',
style: TextStyle(color: AppColors.primary),
),
bottom: TabBar(
controller: _tabController,
labelColor: AppColors.primary,
unselectedLabelColor: Colors.grey,
indicatorColor: AppColors.primary,
tabs: const [Tab(text: 'Groups'), Tab(text: 'Users')],
),
),
body: TabBarView(
controller: _tabController,
children: [
// Groups tab
_buildGroupsTab(),
// Users tab
_buildUsersTab(),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _showCreateGroupDialog,
backgroundColor: AppColors.primary,
child: const Icon(Icons.add),
),
);
}
Widget _buildGroupsTab() {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_groups.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.group, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'No groups yet',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
Text(
'Tap the + button to create a new group',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _createDefaultGroupAndSyncMembers,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
child: const Text('Create Default Group'),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _loadGroups,
child: Column(
children: [
// Groups list
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _groups.length,
itemBuilder: (context, index) {
final group = _groups[index];
return Card(
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
child: ListTile(
contentPadding: const EdgeInsets.all(16),
leading: CircleAvatar(
backgroundColor: AppColors.primary.withOpacity(0.2),
child: Text(
group.name.substring(0, 1).toUpperCase(),
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
),
title: Row(
children: [
Expanded(
child: Text(
group.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
if (group.isDefault)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Default',
style: TextStyle(
fontSize: 12,
color: Colors.blue,
fontWeight: FontWeight.w500,
),
),
),
if (!group.isPublic)
Container(
margin: const EdgeInsets.only(left: 4),
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'Private',
style: TextStyle(
fontSize: 12,
color: Colors.orange,
fontWeight: FontWeight.w500,
),
),
),
],
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text(
group.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 8),
Row(
children: [
Icon(
Icons.people,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
'${group.memberCount} members',
style: TextStyle(color: Colors.grey[600]),
),
],
),
],
),
trailing: IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(16),
),
),
builder:
(context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(
Icons.edit,
color: Colors.blue,
),
title: const Text('Edit Group'),
onTap: () {
Navigator.pop(context);
_showEditGroupDialog(group);
},
),
ListTile(
leading: const Icon(
Icons.people,
color: Colors.green,
),
title: const Text('Manage Members'),
onTap: () {
Navigator.pop(context);
_showGroupDetailDialog(group);
},
),
if (!group.isDefault)
ListTile(
leading: const Icon(
Icons.delete,
color: Colors.red,
),
title: const Text('Delete Group'),
onTap: () {
Navigator.pop(context);
_showDeleteGroupConfirmation(group);
},
),
],
),
),
);
},
),
onTap: () {
_showGroupDetailDialog(group);
},
),
);
},
),
),
],
),
);
}
void _showGroupDetailDialog(Group group) {
showDialog(
context: context,
builder:
(context) => GroupDetailDialog(
group: group,
onGroupUpdated: () {
// Refresh data setelah perubahan grup
_loadGroups();
_loadUsers();
},
),
);
}
Widget _buildUsersTab() {
if (_isLoadingUsers) {
return const Center(child: CircularProgressIndicator());
}
// Tampilkan tombol refresh jika daftar pengguna kosong
if (_users.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.people, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'No users found',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.grey[700],
),
),
const SizedBox(height: 16),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('Refresh'),
onPressed: () async {
// Tampilkan loading
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Loading users...'),
duration: Duration(seconds: 2),
),
);
// Muat ulang data pengguna
await _loadUsers();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _loadUsers,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return Card(
margin: const EdgeInsets.only(bottom: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
child: ExpansionTile(
leading: CircleAvatar(
backgroundColor: AppColors.primary.withOpacity(0.2),
backgroundImage:
user.avatarUrl != null && user.avatarUrl!.isNotEmpty
? NetworkImage(user.avatarUrl!)
: null,
child:
user.avatarUrl == null || user.avatarUrl!.isEmpty
? Text(
user.username.isNotEmpty
? user.username.substring(0, 1).toUpperCase()
: '?',
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
)
: null,
),
title: Text(
user.username,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
subtitle: Text(
user.groups.isEmpty
? 'No groups joined'
: '${user.groups.length} groups: ${user.groups.map((g) => g.groupName).join(", ")}',
style: TextStyle(color: Colors.grey[600]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
trailing: Icon(
user.isActive ? Icons.check_circle : Icons.block,
color: user.isActive ? Colors.green : Colors.red,
),
children: [
if (user.groups.isEmpty)
Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text('User is not a member of any group'),
],
),
)
else
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Groups:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 8),
...user.groups.map(
(group) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Icon(
Icons.group,
size: 16,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Expanded(
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: group.groupName,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
if (group.groupId.isNotEmpty &&
group.role.isNotEmpty)
TextSpan(
text: ' (${group.role})',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontStyle: FontStyle.italic,
),
),
],
),
),
),
],
),
),
),
],
),
),
],
),
);
},
),
);
}
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}';
}
}
Future<void> _createDefaultGroupAndSyncMembers() async {
if (!mounted) return;
setState(() => _isLoading = true);
try {
// Periksa apakah sudah ada grup default
bool hasDefaultGroup = _groups.any((group) => group.isDefault);
if (hasDefaultGroup) {
_showErrorSnackBar('Default group already exists');
setState(() => _isLoading = false);
return;
}
// Set up a new default group
_groupNameController.text = 'General';
_groupDescriptionController.text = 'Default group for all users';
_isPublicGroup = true;
_isDefaultGroup = true;
// Create the group
await _createGroup();
// Refresh data setelah pembuatan grup
await _loadGroups();
await _loadUsers();
} catch (e) {
print('[ERROR] Failed to create default group: $e');
if (mounted) {
_showErrorSnackBar(
'Error: ${e.toString().split('Exception:').last.trim()}',
);
setState(() => _isLoading = false);
}
}
}
}