member screen updates

This commit is contained in:
Deep Koluguri 2025-11-06 09:42:17 -05:00
parent 8d4fbfed00
commit e187618437
3 changed files with 713 additions and 14 deletions

View File

@ -28,15 +28,7 @@ const addMemberToGroup = async (req, res) => {
}); });
} }
// Check if group is still forming // Check if group is full (allow adding members to any status as long as not full)
if (chitGroup.status !== 'forming') {
return res.status(400).json({
success: false,
message: 'Cannot add members after chit group has started'
});
}
// Check if group is full
const currentMemberCount = await GroupMember.count({ const currentMemberCount = await GroupMember.count({
where: { group_id: groupId, status: 'active' } where: { group_id: groupId, status: 'active' }
}); });
@ -44,7 +36,7 @@ const addMemberToGroup = async (req, res) => {
if (currentMemberCount >= chitGroup.max_members) { if (currentMemberCount >= chitGroup.max_members) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: 'Chit group is full' message: `Chit group is full (${currentMemberCount}/${chitGroup.max_members} members)`
}); });
} }

View File

@ -14,6 +14,7 @@ import '../../features/notifications/notification_center_page.dart';
import 'chit_groups_page.dart'; import 'chit_groups_page.dart';
import 'create_group_page.dart'; import 'create_group_page.dart';
import 'import_existing_group_dialog.dart'; import 'import_existing_group_dialog.dart';
import 'members_page.dart';
import '../../test_animated_draw.dart'; import '../../test_animated_draw.dart';
import '../../features/recordings/recordings_page.dart'; import '../../features/recordings/recordings_page.dart';
@ -574,10 +575,7 @@ class ManagerDashboard extends StatelessWidget {
} }
void _navigateToMembers() { void _navigateToMembers() {
SnackbarUtil.showInfo( Get.to(() => const MembersPage());
'Members page will be implemented next',
title: 'Coming Soon',
);
} }
void _navigateToPayments() { void _navigateToPayments() {

View File

@ -0,0 +1,709 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../core/services/chit_group_service.dart';
import '../../core/models/group_member.dart';
import '../../core/models/chit_group.dart';
import 'add_user_dialog.dart';
class MembersPage extends StatefulWidget {
const MembersPage({super.key});
@override
State<MembersPage> createState() => _MembersPageState();
}
class _MembersPageState extends State<MembersPage> {
final _chitGroupService = Get.find<ChitGroupService>();
final _searchController = TextEditingController();
String _searchQuery = '';
String _selectedFilter = 'all'; // all, active, inactive
@override
void initState() {
super.initState();
_loadAllMembers();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadAllMembers() async {
// Load all groups to get all members
await _chitGroupService.loadManagerChitGroups();
}
List<Map<String, dynamic>> _getAllMembers() {
final allMembers = <Map<String, dynamic>>[];
final groups = _chitGroupService.chitGroups;
for (var group in groups) {
if (group.members != null) {
for (var member in group.members!) {
// Check if member already in list (user can be in multiple groups)
final existingIndex = allMembers.indexWhere(
(m) => m['userId'] == member.userId,
);
if (existingIndex == -1) {
allMembers.add({
'userId': member.userId,
'member': member,
'groups': [group],
});
} else {
// Add group to existing member's group list
(allMembers[existingIndex]['groups'] as List).add(group);
}
}
}
}
return allMembers;
}
List<Map<String, dynamic>> _getFilteredMembers() {
var members = _getAllMembers();
// Apply status filter
if (_selectedFilter == 'active') {
members = members.where((m) {
final member = m['member'] as GroupMember;
return member.status.toLowerCase() == 'active';
}).toList();
} else if (_selectedFilter == 'inactive') {
members = members.where((m) {
final member = m['member'] as GroupMember;
return member.status.toLowerCase() != 'active';
}).toList();
}
// Apply search filter
if (_searchQuery.isNotEmpty) {
members = members.where((m) {
final member = m['member'] as GroupMember;
final name = member.user?.fullName?.toLowerCase() ?? '';
final mobile = member.user?.mobileNumber ?? '';
final query = _searchQuery.toLowerCase();
return name.contains(query) || mobile.contains(query);
}).toList();
}
return members;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
title: Text(
'All Members',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
),
),
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
elevation: 2,
actions: [
IconButton(
icon: Icon(Icons.person_add, size: 24.w),
onPressed: _showAddUserDialog,
tooltip: 'Add New Member',
),
],
),
body: Obx(() {
if (_chitGroupService.isLoading.value) {
return Center(
child: CircularProgressIndicator(
color: Colors.green.shade600,
),
);
}
final filteredMembers = _getFilteredMembers();
return Column(
children: [
// Search and Filter Bar
Container(
padding: EdgeInsets.all(16.w),
color: Colors.white,
child: Column(
children: [
// Search Bar
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search by name or mobile...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
setState(() {
_searchController.clear();
_searchQuery = '';
});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12.r),
borderSide: BorderSide(color: Colors.grey.shade300),
),
filled: true,
fillColor: Colors.grey.shade50,
contentPadding: EdgeInsets.symmetric(
horizontal: 16.w,
vertical: 12.h,
),
),
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
),
SizedBox(height: 12.h),
// Filter Chips
Row(
children: [
Text(
'Filter:',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
SizedBox(width: 12.w),
_buildFilterChip('All', 'all'),
SizedBox(width: 8.w),
_buildFilterChip('Active', 'active'),
SizedBox(width: 8.w),
_buildFilterChip('Inactive', 'inactive'),
],
),
],
),
),
// Member Count
Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
color: Colors.blue.shade50,
child: Row(
children: [
Icon(Icons.people, color: Colors.blue.shade700, size: 20.w),
SizedBox(width: 8.w),
Text(
'${filteredMembers.length} member${filteredMembers.length != 1 ? 's' : ''}',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.blue.shade800,
),
),
],
),
),
// Members List
Expanded(
child: filteredMembers.isEmpty
? _buildEmptyState()
: RefreshIndicator(
onRefresh: _loadAllMembers,
child: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: filteredMembers.length,
itemBuilder: (context, index) {
final memberData = filteredMembers[index];
final member = memberData['member'] as GroupMember;
final groups = memberData['groups'] as List<ChitGroup>;
return _buildMemberCard(member, groups);
},
),
),
),
],
);
}),
);
}
Widget _buildFilterChip(String label, String value) {
final isSelected = _selectedFilter == value;
return FilterChip(
label: Text(
label,
style: TextStyle(
fontSize: 14.sp,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
selected: isSelected,
onSelected: (selected) {
setState(() {
_selectedFilter = value;
});
},
selectedColor: Colors.green.shade100,
checkmarkColor: Colors.green.shade700,
backgroundColor: Colors.grey.shade100,
side: BorderSide(
color: isSelected ? Colors.green.shade600 : Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
);
}
Widget _buildMemberCard(GroupMember member, List<ChitGroup> groups) {
final user = member.user;
final isActive = member.status.toLowerCase() == 'active';
return Card(
margin: EdgeInsets.only(bottom: 12.h),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: InkWell(
onTap: () => _showMemberDetails(member, groups),
borderRadius: BorderRadius.circular(16.r),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
children: [
// Avatar
CircleAvatar(
radius: 28.r,
backgroundColor: isActive ? Colors.green.shade600 : Colors.grey.shade400,
child: Text(
user?.fullName?.substring(0, 1).toUpperCase() ?? 'M',
style: TextStyle(
color: Colors.white,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(width: 16.w),
// Member Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?.fullName ?? 'Unknown',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 4.h),
Text(
user?.mobileNumber ?? '',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
),
),
SizedBox(height: 8.h),
Wrap(
spacing: 8.w,
children: [
// Status Badge
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: isActive ? Colors.green.shade100 : Colors.grey.shade200,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isActive ? Colors.green.shade600 : Colors.grey.shade400,
),
),
child: Text(
isActive ? 'Active' : 'Inactive',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,
color: isActive ? Colors.green : Colors.grey.shade600,
),
),
),
// Groups Count
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.blue.shade200),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.group, size: 14.w, color: Colors.blue.shade700),
SizedBox(width: 4.w),
Text(
'${groups.length} group${groups.length != 1 ? 's' : ''}',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,
color: Colors.blue,
),
),
],
),
),
],
),
],
),
),
// Arrow
Icon(
Icons.arrow_forward_ios,
size: 16.w,
color: Colors.grey.shade400,
),
],
),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: EdgeInsets.all(32.w),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 80.w,
color: Colors.grey.shade300,
),
SizedBox(height: 24.h),
Text(
_searchQuery.isNotEmpty
? 'No members found'
: 'No members yet',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
color: Colors.grey.shade700,
),
),
SizedBox(height: 12.h),
Text(
_searchQuery.isNotEmpty
? 'Try a different search term'
: 'Add members to your chit groups to see them here',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey.shade600,
),
),
if (_searchQuery.isEmpty) ...[
SizedBox(height: 32.h),
ElevatedButton.icon(
onPressed: _showAddUserDialog,
icon: Icon(Icons.person_add, size: 20.w),
label: Text(
'Add Member',
style: TextStyle(fontSize: 16.sp),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
),
),
],
],
),
),
);
}
void _showMemberDetails(GroupMember member, List<ChitGroup> groups) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(24.r),
topRight: Radius.circular(24.r),
),
),
child: Column(
children: [
// Handle
Container(
margin: EdgeInsets.only(top: 12.h),
width: 40.w,
height: 4.h,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2.r),
),
),
SizedBox(height: 20.h),
// Header
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
child: Row(
children: [
CircleAvatar(
radius: 32.r,
backgroundColor: Colors.green.shade600,
child: Text(
member.user?.fullName?.substring(0, 1).toUpperCase() ?? 'M',
style: TextStyle(
color: Colors.white,
fontSize: 24.sp,
fontWeight: FontWeight.bold,
),
),
),
SizedBox(width: 16.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
member.user?.fullName ?? 'Unknown',
style: TextStyle(
fontSize: 20.sp,
fontWeight: FontWeight.bold,
),
),
Text(
member.user?.mobileNumber ?? '',
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey.shade600,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
SizedBox(height: 24.h),
// Content
Expanded(
child: ListView(
padding: EdgeInsets.symmetric(horizontal: 24.w),
children: [
// Stats
Row(
children: [
Expanded(
child: _buildStatCard(
'Total Paid',
'${_formatCurrency(member.totalPaid)}',
Icons.payment,
Colors.green,
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildStatCard(
'Total Won',
'${_formatCurrency(member.totalWon)}',
Icons.emoji_events,
Colors.amber,
),
),
],
),
SizedBox(height: 24.h),
// Groups
Text(
'Member of Groups (${groups.length})',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 12.h),
...groups.map((group) => _buildGroupChip(group)),
SizedBox(height: 24.h),
// Additional Info
_buildInfoRow('Joined', _formatDate(member.joinedDate)),
SizedBox(height: 12.h),
_buildInfoRow('Status', member.status),
],
),
),
],
),
),
);
}
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Column(
children: [
Icon(icon, color: color, size: 24.w),
SizedBox(height: 8.h),
Text(
value,
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
color: color,
),
),
SizedBox(height: 4.h),
Text(
label,
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey.shade600,
),
),
],
),
);
}
Widget _buildGroupChip(ChitGroup group) {
return Container(
margin: EdgeInsets.only(bottom: 8.h),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: Colors.grey.shade200),
),
child: Row(
children: [
Icon(
Icons.account_balance_wallet,
color: Colors.green.shade600,
size: 20.w,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
group.name,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
Text(
'${group.status} • ₹${_formatCurrency(group.monthlyInstallment)}/month',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey.shade600,
),
),
],
),
),
],
),
);
}
Widget _buildInfoRow(String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 100.w,
child: Text(
label,
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
),
),
),
Expanded(
child: Text(
value,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
),
],
);
}
void _showAddUserDialog() {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const AddUserDialog(),
).then((result) {
if (result == true) {
// Reload members
_loadAllMembers();
}
});
}
String _formatCurrency(double amount) {
final amountStr = amount.toStringAsFixed(0);
return amountStr.replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]},',
);
}
String _formatDate(DateTime date) {
return '${date.day}/${date.month}/${date.year}';
}
}