From 38fdc84128681051c9fee42335a268c82a42f17d Mon Sep 17 00:00:00 2001 From: Deep Koluguri Date: Wed, 5 Nov 2025 22:09:49 -0500 Subject: [PATCH] member group detials page --- .../interfaces/member/member_dashboard.dart | 3 +- .../member/member_group_details_page.dart | 931 ++++++++++++++++++ 2 files changed, 933 insertions(+), 1 deletion(-) create mode 100644 luckychit/lib/interfaces/member/member_group_details_page.dart diff --git a/luckychit/lib/interfaces/member/member_dashboard.dart b/luckychit/lib/interfaces/member/member_dashboard.dart index 9498ff5..14cf7d4 100644 --- a/luckychit/lib/interfaces/member/member_dashboard.dart +++ b/luckychit/lib/interfaces/member/member_dashboard.dart @@ -8,6 +8,7 @@ import '../../core/utils/snackbar_util.dart'; import '../../shared/widgets/interactive_card.dart'; import '../../shared/widgets/notification_badge.dart'; import '../../features/notifications/notification_center_page.dart'; +import 'member_group_details_page.dart'; class MemberDashboard extends StatefulWidget { const MemberDashboard({super.key}); @@ -308,7 +309,7 @@ class _MemberDashboardState extends State { return InteractiveCard( onTap: () { - SnackbarUtil.showInfo('Group details page coming soon!'); + Get.to(() => MemberGroupDetailsPage(group: group)); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/luckychit/lib/interfaces/member/member_group_details_page.dart b/luckychit/lib/interfaces/member/member_group_details_page.dart new file mode 100644 index 0000000..65d163f --- /dev/null +++ b/luckychit/lib/interfaces/member/member_group_details_page.dart @@ -0,0 +1,931 @@ +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/services/payment_service.dart'; +import '../../core/services/auth_service.dart'; +import '../../core/models/chit_group.dart'; +import '../../core/models/payment.dart'; +import '../../core/models/monthly_draw.dart'; + +class MemberGroupDetailsPage extends StatefulWidget { + final ChitGroup group; + + const MemberGroupDetailsPage({ + super.key, + required this.group, + }); + + @override + State createState() => _MemberGroupDetailsPageState(); +} + +class _MemberGroupDetailsPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final _chitGroupService = Get.find(); + final _paymentService = Get.find(); + final _authService = Get.find(); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 3, vsync: this); + + // Load data + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadData(); + }); + } + + Future _loadData() async { + final groupId = widget.group.id; + await Future.wait([ + _chitGroupService.loadChitGroupDetails(groupId), + _chitGroupService.loadGroupMembers(groupId), + _chitGroupService.loadGroupMonthlyDraws(groupId), + _paymentService.loadGroupPayments(groupId), + ]); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + title: Text( + widget.group.name, + style: TextStyle(fontSize: 20.sp, fontWeight: FontWeight.w600), + ), + backgroundColor: Colors.green.shade600, + foregroundColor: Colors.white, + elevation: 2, + bottom: TabBar( + controller: _tabController, + indicatorColor: Colors.white, + indicatorWeight: 3, + labelStyle: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600), + unselectedLabelStyle: TextStyle(fontSize: 14.sp), + tabs: const [ + Tab(text: 'Overview'), + Tab(text: 'Payments'), + Tab(text: 'Draws'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildOverviewTab(), + _buildPaymentsTab(), + _buildDrawsTab(), + ], + ), + ); + } + + Widget _buildOverviewTab() { + return Obx(() { + final group = _chitGroupService.selectedGroup.value ?? widget.group; + final members = _chitGroupService.groupMembers; + final myMember = members.firstWhereOrNull( + (m) => m.userId == _authService.currentUser.value?.id, + ); + + return RefreshIndicator( + onRefresh: _loadData, + child: ListView( + padding: EdgeInsets.all(16.w), + children: [ + // Group Info Card + _buildGroupInfoCard(group), + SizedBox(height: 16.h), + + // My Status Card + if (myMember != null) _buildMyStatusCard(myMember), + if (myMember != null) SizedBox(height: 16.h), + + // Members Card + _buildMembersCard(members), + SizedBox(height: 16.h), + + // Quick Stats + _buildQuickStats(group, myMember), + ], + ), + ); + }); + } + + Widget _buildPaymentsTab() { + final myUserId = _authService.currentUser.value?.id; + + return Obx(() { + final payments = _paymentService.payments; + final isLoading = _paymentService.isLoading; + + // Filter to show only my payments + final myPayments = payments.where((p) => p.userId == myUserId).toList(); + + if (isLoading) { + return Center( + child: CircularProgressIndicator(color: Colors.green.shade600), + ); + } + + return RefreshIndicator( + onRefresh: () => _paymentService.loadGroupPayments(widget.group.id), + child: myPayments.isEmpty + ? _buildEmptyPayments() + : ListView.builder( + padding: EdgeInsets.all(16.w), + itemCount: myPayments.length, + itemBuilder: (context, index) { + return _buildPaymentCard(myPayments[index]); + }, + ), + ); + }); + } + + Widget _buildDrawsTab() { + return Obx(() { + final draws = _chitGroupService.monthlyDraws; + + if (_chitGroupService.isLoading.value) { + return Center( + child: CircularProgressIndicator(color: Colors.green.shade600), + ); + } + + return RefreshIndicator( + onRefresh: () => _chitGroupService.loadGroupMonthlyDraws(widget.group.id), + child: draws.isEmpty + ? _buildEmptyDraws() + : ListView.builder( + padding: EdgeInsets.all(16.w), + itemCount: draws.length, + itemBuilder: (context, index) { + return _buildDrawCard(draws[index]); + }, + ), + ); + }); + } + + Widget _buildGroupInfoCard(ChitGroup group) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)), + child: Padding( + padding: EdgeInsets.all(20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Group Details', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + _buildStatusBadge(group.status), + ], + ), + SizedBox(height: 20.h), + _buildInfoRow('Total Value', '₹${_formatCurrency(group.totalValue)}', Icons.account_balance_wallet), + _buildInfoRow('Monthly Installment', '₹${_formatCurrency(group.monthlyInstallment)}', Icons.payment), + _buildInfoRow('Duration', '${group.durationMonths} months', Icons.calendar_today), + _buildInfoRow('Draw Date', '${group.drawDate}th of each month', Icons.event), + _buildInfoRow('Members', '${group.currentMemberCount}/${group.maxMembers}', Icons.people), + if (group.startDate != null) + _buildInfoRow('Started On', _formatDate(group.startDate!), Icons.play_circle), + ], + ), + ), + ); + } + + Widget _buildMyStatusCard(dynamic member) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)), + child: Container( + padding: EdgeInsets.all(20.w), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade50, Colors.blue.shade100], + ), + borderRadius: BorderRadius.circular(16.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.person, color: Colors.blue.shade700, size: 24.w), + SizedBox(width: 12.w), + Text( + 'My Status', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.blue.shade800, + ), + ), + ], + ), + SizedBox(height: 16.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatItem('Total Paid', '₹${_formatCurrency(member.totalPaid)}', Colors.green), + Container(width: 1, height: 40.h, color: Colors.blue.shade300), + _buildStatItem('Total Won', '₹${_formatCurrency(member.totalWon)}', Colors.orange), + ], + ), + SizedBox(height: 12.h), + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Joined', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + Text( + _formatDate(member.joinedDate), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildMembersCard(List members) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)), + child: Padding( + padding: EdgeInsets.all(20.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Members', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + Text( + '${members.length} members', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + SizedBox(height: 16.h), + ...members.map((member) => _buildMemberItem(member)), + ], + ), + ), + ); + } + + Widget _buildMemberItem(dynamic member) { + final user = member.user; + final isMe = member.userId == _authService.currentUser.value?.id; + + return Container( + margin: EdgeInsets.only(bottom: 12.h), + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isMe ? Colors.green.shade50 : Colors.grey.shade50, + borderRadius: BorderRadius.circular(12.r), + border: Border.all( + color: isMe ? Colors.green.shade300 : Colors.grey.shade200, + ), + ), + child: Row( + children: [ + CircleAvatar( + radius: 20.r, + backgroundColor: isMe ? Colors.green.shade600 : Colors.grey.shade400, + child: Text( + user?.fullName?.substring(0, 1).toUpperCase() ?? 'M', + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + user?.fullName ?? 'Unknown', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + ), + if (isMe) + Container( + padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h), + decoration: BoxDecoration( + color: Colors.green.shade600, + borderRadius: BorderRadius.circular(8.r), + ), + child: Text( + 'You', + style: TextStyle( + fontSize: 12.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + SizedBox(height: 4.h), + Text( + user?.mobileNumber ?? '', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildQuickStats(ChitGroup group, dynamic myMember) { + return Row( + children: [ + Expanded( + child: _buildQuickStatCard( + 'Total Members', + '${group.currentMemberCount}', + Icons.people, + Colors.blue.shade600, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: _buildQuickStatCard( + 'Duration', + '${group.durationMonths}M', + Icons.calendar_month, + Colors.orange.shade600, + ), + ), + ], + ); + } + + Widget _buildQuickStatCard(String label, String value, IconData icon, Color color) { + return Card( + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)), + child: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [color.withOpacity(0.1), color.withOpacity(0.05)], + ), + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + children: [ + Icon(icon, color: color, size: 28.w), + SizedBox(height: 8.h), + Text( + value, + style: TextStyle( + fontSize: 24.sp, + fontWeight: FontWeight.bold, + color: color, + ), + ), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey.shade600, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + Widget _buildPaymentCard(Payment payment) { + return Card( + margin: EdgeInsets.only(bottom: 12.h), + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)), + child: Padding( + padding: EdgeInsets.all(16.w), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Month ${payment.month}, ${payment.year}', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + _buildPaymentStatusBadge(payment.status), + ], + ), + SizedBox(height: 12.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Amount', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + SizedBox(height: 4.h), + Text( + '₹${_formatCurrency(payment.amount)}', + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: Colors.green.shade700, + ), + ), + ], + ), + if (payment.paidAt != null) + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + 'Paid On', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + SizedBox(height: 4.h), + Text( + _formatDate(payment.paidAt!), + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + ], + ), + ], + ), + if (payment.notes != null && payment.notes!.isNotEmpty) ...[ + SizedBox(height: 12.h), + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(8.r), + ), + child: Row( + children: [ + Icon(Icons.note, size: 16.w, color: Colors.grey.shade600), + SizedBox(width: 8.w), + Expanded( + child: Text( + payment.notes!, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade700, + ), + ), + ), + ], + ), + ), + ], + ], + ), + ), + ); + } + + Widget _buildDrawCard(MonthlyDraw draw) { + final isWinner = draw.winnerId == _authService.currentUser.value?.id; + + return Card( + margin: EdgeInsets.only(bottom: 12.h), + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12.r)), + child: Container( + padding: EdgeInsets.all(16.w), + decoration: BoxDecoration( + gradient: isWinner + ? LinearGradient( + colors: [Colors.amber.shade50, Colors.amber.shade100], + ) + : null, + borderRadius: BorderRadius.circular(12.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Month ${draw.month}', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: isWinner ? Colors.amber.shade900 : Colors.grey.shade800, + ), + ), + if (isWinner) + Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: Colors.amber.shade600, + borderRadius: BorderRadius.circular(12.r), + ), + child: Row( + children: [ + Icon(Icons.emoji_events, color: Colors.white, size: 16.w), + SizedBox(width: 4.w), + Text( + 'Winner!', + style: TextStyle( + fontSize: 14.sp, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + ], + ), + SizedBox(height: 16.h), + _buildDrawInfo('Prize Amount', '₹${_formatCurrency(draw.prizeAmount)}'), + SizedBox(height: 12.h), + Container( + padding: EdgeInsets.all(12.w), + decoration: BoxDecoration( + color: isWinner ? Colors.amber.shade100 : Colors.grey.shade100, + borderRadius: BorderRadius.circular(8.r), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Winner', + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey.shade600, + ), + ), + SizedBox(height: 4.h), + Text( + draw.winner?.fullName ?? 'Unknown', + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: isWinner ? Colors.amber.shade900 : Colors.grey.shade800, + ), + ), + ], + ), + ), + SizedBox(height: 12.h), + Row( + children: [ + Icon(Icons.event, size: 16.w, color: Colors.grey.shade600), + SizedBox(width: 8.w), + Text( + 'Drawn on ${_formatDate(draw.drawDate)}', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildDrawInfo(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 12.sp, + color: Colors.grey.shade600, + ), + ), + SizedBox(height: 4.h), + Text( + value, + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + ], + ); + } + + Widget _buildEmptyPayments() { + return Center( + child: Padding( + padding: EdgeInsets.all(32.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.payment_outlined, + size: 64.w, + color: Colors.grey.shade300, + ), + SizedBox(height: 16.h), + Text( + 'No Payments Yet', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 8.h), + Text( + 'Your payment history will appear here', + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ); + } + + Widget _buildEmptyDraws() { + return Center( + child: Padding( + padding: EdgeInsets.all(32.w), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.casino_outlined, + size: 64.w, + color: Colors.grey.shade300, + ), + SizedBox(height: 16.h), + Text( + 'No Draws Yet', + style: TextStyle( + fontSize: 18.sp, + fontWeight: FontWeight.bold, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 8.h), + Text( + widget.group.status == 'forming' + ? 'Group needs to start before draws can happen' + : 'Draw results will appear here', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + ], + ), + ), + ); + } + + Widget _buildStatItem(String label, String value, Color color) { + return Column( + children: [ + Text( + value, + style: TextStyle( + fontSize: 20.sp, + fontWeight: FontWeight.bold, + color: color, + ), + ), + SizedBox(height: 4.h), + Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + ], + ); + } + + Widget _buildInfoRow(String label, String value, IconData icon) { + return Padding( + padding: EdgeInsets.only(bottom: 16.h), + child: Row( + children: [ + Icon(icon, size: 20.w, color: Colors.green.shade600), + SizedBox(width: 12.w), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14.sp, + color: Colors.grey.shade600, + ), + ), + SizedBox(height: 4.h), + Text( + value, + style: TextStyle( + fontSize: 16.sp, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildStatusBadge(String status) { + Color color; + String text; + + switch (status.toLowerCase()) { + case 'active': + color = Colors.green.shade600; + text = 'Active'; + break; + case 'forming': + color = Colors.orange.shade600; + text = 'Forming'; + break; + case 'completed': + color = Colors.blue.shade600; + text = 'Completed'; + break; + default: + color = Colors.grey.shade600; + text = status; + } + + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12.r), + border: Border.all(color: color, width: 1.5), + ), + child: Text( + text, + style: TextStyle( + fontSize: 14.sp, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + Widget _buildPaymentStatusBadge(String status) { + Color color; + String text; + + switch (status.toLowerCase()) { + case 'success': + color = Colors.green.shade600; + text = 'Paid'; + break; + case 'pending': + color = Colors.orange.shade600; + text = 'Pending'; + break; + case 'failed': + color = Colors.red.shade600; + text = 'Failed'; + break; + case 'cancelled': + color = Colors.grey.shade600; + text = 'Cancelled'; + break; + default: + color = Colors.grey.shade600; + text = status; + } + + return Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(8.r), + border: Border.all(color: color, width: 1.5), + ), + child: Text( + text, + style: TextStyle( + fontSize: 12.sp, + color: color, + fontWeight: FontWeight.w600, + ), + ), + ); + } + + 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}'; + } +} +