710 lines
23 KiB
Dart
710 lines
23 KiB
Dart
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}';
|
|
}
|
|
}
|
|
|