chitfund/luckychit/lib/interfaces/manager/payment_history_page.dart

1618 lines
50 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../core/models/chit_group.dart';
import '../../core/models/payment.dart';
import '../../core/services/payment_service.dart';
import 'record_payment_dialog.dart';
class PaymentHistoryPage extends StatefulWidget {
final ChitGroup group;
const PaymentHistoryPage({
super.key,
required this.group,
});
@override
State<PaymentHistoryPage> createState() => _PaymentHistoryPageState();
}
class _PaymentHistoryPageState extends State<PaymentHistoryPage>
with SingleTickerProviderStateMixin {
late final PaymentService _paymentService;
late TabController _tabController;
String? _selectedStatus;
int? _selectedMonth;
int? _selectedYear;
// Track which months are expanded
final Set<String> _expandedMonths = <String>{};
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
// Get payment service (already initialized in app.dart)
_paymentService = Get.find<PaymentService>();
// Load data after widget is fully built
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
_loadData();
}
});
});
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _loadData() async {
await Future.wait([
_paymentService.loadGroupPayments(widget.group.id),
_paymentService.loadPaymentSummary(widget.group.id),
_paymentService.loadPendingPayments(widget.group.id),
]);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade50,
appBar: AppBar(
title: Row(
children: [
Flexible(
child: Text(
'Payment History',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
if (_expandedMonths.isNotEmpty) ...[
SizedBox(width: 6.w),
Container(
padding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(8.r),
),
child: Text(
'${_expandedMonths.length}',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
],
],
),
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
elevation: 2,
actions: [
IconButton(
icon: Icon(Icons.expand_more, size: 24.w),
onPressed: _expandAllMonths,
tooltip: 'Expand All',
),
IconButton(
icon: Icon(Icons.expand_less, size: 24.w),
onPressed: _collapseAllMonths,
tooltip: 'Collapse All',
),
IconButton(
icon: Icon(Icons.add, size: 24.w),
onPressed: () => _showRecordPaymentDialog(),
tooltip: 'Record Payment',
),
IconButton(
icon: Icon(Icons.refresh, size: 24.w),
onPressed: _loadData,
tooltip: 'Refresh',
),
],
bottom: TabBar(
controller: _tabController,
indicatorColor: Colors.white,
labelColor: Colors.white,
unselectedLabelColor: Colors.white70,
labelStyle: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.w600),
unselectedLabelStyle: TextStyle(fontSize: 14.sp),
tabs: const [
Tab(icon: Icon(Icons.history), text: 'History'),
Tab(icon: Icon(Icons.pending), text: 'Pending'),
Tab(icon: Icon(Icons.analytics), text: 'Summary'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildHistoryTab(),
_buildPendingTab(),
_buildSummaryTab(),
],
),
);
}
Widget _buildHistoryTab() {
return Column(
children: [
// Filters
Container(
padding: EdgeInsets.all(6.w),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filters',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 4.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
SizedBox(
width: 80.w,
child: _buildStatusFilter(),
),
SizedBox(width: 4.w),
SizedBox(
width: 80.w,
child: _buildMonthFilter(),
),
SizedBox(width: 4.w),
SizedBox(
width: 80.w,
child: _buildYearFilter(),
),
],
),
),
],
),
),
// Payment List
Expanded(
child: Obx(() {
if (_paymentService.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_paymentService.error.isNotEmpty) {
return _buildErrorWidget(_paymentService.error);
}
final payments = _paymentService.payments;
if (payments.isEmpty) {
return _buildEmptyWidget('No payments found');
}
return RefreshIndicator(
onRefresh: _loadData,
child: _buildGroupedPaymentsList(payments),
);
}),
),
],
);
}
Widget _buildGroupedPaymentsList(List<Payment> payments) {
// Get all months from chitfund start date to current date
final allMonths = _getAllMonthsFromStartDate();
// Group payments by month and year
final Map<String, List<Payment>> groupedPayments = {};
for (final payment in payments) {
final key = '${payment.year}-${payment.month.toString().padLeft(2, '0')}';
if (!groupedPayments.containsKey(key)) {
groupedPayments[key] = [];
}
groupedPayments[key]!.add(payment);
}
// Sort months in descending order (newest first)
final sortedKeys = allMonths.toList()
..sort((a, b) => b.compareTo(a));
return ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: sortedKeys.length,
itemBuilder: (context, index) {
final monthKey = sortedKeys[index];
final monthPayments = groupedPayments[monthKey] ?? [];
final monthYear = monthKey.split('-');
final year = int.parse(monthYear[0]);
final month = int.parse(monthYear[1]);
return _buildMonthGroup(monthKey, month, year, monthPayments);
},
);
}
Set<String> _getAllMonthsFromStartDate() {
final Set<String> months = {};
if (widget.group.startDate == null) {
return months;
}
final startDate = widget.group.startDate!;
final currentDate = DateTime.now();
// Start from the chitfund start date
DateTime currentMonth = DateTime(startDate.year, startDate.month);
final endMonth = DateTime(currentDate.year, currentDate.month);
// Add all months from start to current (inclusive)
while (currentMonth.isBefore(endMonth) || currentMonth.isAtSameMomentAs(endMonth)) {
final key = '${currentMonth.year}-${currentMonth.month.toString().padLeft(2, '0')}';
months.add(key);
// Move to next month
currentMonth = DateTime(currentMonth.year, currentMonth.month + 1);
}
return months;
}
Widget _buildMonthGroup(String monthKey, int month, int year, List<Payment> payments) {
final monthName = _getMonthName(month);
final totalAmount = payments.fold(0.0, (sum, payment) => sum + payment.amount);
final successfulCount = payments.where((p) => p.status == 'success').length;
final totalCount = payments.length;
final allMembers = widget.group.members ?? [];
final expectedCount = allMembers.length;
final isExpanded = _expandedMonths.contains(monthKey);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Month Header (Clickable to expand/collapse)
GestureDetector(
onTap: () => _toggleMonthExpansion(monthKey),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.h),
decoration: BoxDecoration(
color: isExpanded
? (totalCount == 0 ? Colors.grey.shade100 : Colors.green.shade100)
: (totalCount == 0 ? Colors.grey.shade50 : Colors.green.shade50),
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isExpanded
? (totalCount == 0 ? Colors.grey.shade300 : Colors.green.shade300)
: (totalCount == 0 ? Colors.grey.shade200 : Colors.green.shade200),
width: isExpanded ? 1.5 : 1.0,
),
),
child: Row(
children: [
Icon(
Icons.calendar_month,
color: totalCount == 0 ? Colors.grey.shade700 : Colors.green.shade700,
size: 20.w,
),
SizedBox(width: 8.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$monthName $year',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: totalCount == 0 ? Colors.grey.shade800 : Colors.green.shade800,
),
),
Text(
'$successfulCount/$expectedCount payments • ${_formatIndianCurrency(totalAmount)}',
style: TextStyle(
fontSize: 11.sp,
color: totalCount == 0 ? Colors.grey.shade600 : Colors.green.shade600,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
),
Flexible(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 3.h),
decoration: BoxDecoration(
color: totalCount == 0 ? Colors.grey.shade100 : Colors.green.shade100,
borderRadius: BorderRadius.circular(10.r),
),
child: Text(
'$totalCount/$expectedCount',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w600,
color: totalCount == 0 ? Colors.grey.shade700 : Colors.green.shade700,
),
),
),
SizedBox(width: 4.w),
GestureDetector(
onTap: () => _updateMonthPayments(month, year),
child: Container(
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(4.r),
),
child: Icon(
Icons.edit,
size: 12.w,
color: Colors.blue.shade700,
),
),
),
SizedBox(width: 4.w),
// Expand/Collapse Icon
AnimatedRotation(
turns: isExpanded ? 0.5 : 0.0,
duration: const Duration(milliseconds: 200),
child: Icon(
Icons.keyboard_arrow_down,
color: totalCount == 0 ? Colors.grey.shade700 : Colors.green.shade700,
size: 16.w,
),
),
],
),
),
],
),
),
),
// Collapsible Content
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: isExpanded ? null : 0,
child: isExpanded
? Column(
children: [
SizedBox(height: 8.h),
// Member Payment Status Grid
_buildMemberPaymentGrid(payments, monthName, year, month),
],
)
: const SizedBox.shrink(),
),
SizedBox(height: 16.h),
],
);
}
void _toggleMonthExpansion(String monthKey) {
setState(() {
if (_expandedMonths.contains(monthKey)) {
_expandedMonths.remove(monthKey);
} else {
_expandedMonths.add(monthKey);
}
});
}
void _expandAllMonths() {
setState(() {
final allMonths = _getAllMonthsFromStartDate();
_expandedMonths.addAll(allMonths);
});
}
void _collapseAllMonths() {
setState(() {
_expandedMonths.clear();
});
}
Widget _buildMemberPaymentGrid(List<Payment> payments, String monthName, int year, int month) {
// Get all members from the group
final allMembers = widget.group.members ?? [];
// Create a map of user ID to payment for quick lookup
final paymentMap = <String, Payment>{};
for (final payment in payments) {
paymentMap[payment.userId] = payment;
}
return Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Member Payment Status',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 8.h),
// Show message if no payments for this month
if (payments.isEmpty)
Container(
width: double.infinity,
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(color: Colors.grey.shade200),
),
child: Column(
children: [
Icon(
Icons.payment_outlined,
size: 32.w,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'No payments recorded for this month',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4.h),
Text(
'Tap the edit button to add payments',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey.shade500,
),
),
],
),
)
else
// Grid of member payment statuses
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 4.0, // Increased aspect ratio to make cards shorter
crossAxisSpacing: 6.w,
mainAxisSpacing: 6.h,
),
itemCount: allMembers.length,
itemBuilder: (context, index) {
final member = allMembers[index];
final payment = paymentMap[member.userId];
return _buildMemberPaymentStatusCard(member, payment, monthName, year);
},
),
],
),
);
}
Widget _buildMemberPaymentStatusCard(dynamic member, Payment? payment, String monthName, int year) {
final hasPayment = payment != null;
final isSuccess = payment?.status == 'success';
final isPending = payment?.status == 'pending';
final isFailed = payment?.status == 'failed';
Color statusColor;
IconData statusIcon;
String statusText;
if (hasPayment) {
if (isSuccess) {
statusColor = Colors.green;
statusIcon = Icons.check_circle;
statusText = 'Paid';
} else if (isPending) {
statusColor = Colors.orange;
statusIcon = Icons.schedule;
statusText = 'Pending';
} else if (isFailed) {
statusColor = Colors.red;
statusIcon = Icons.error;
statusText = 'Failed';
} else {
statusColor = Colors.grey;
statusIcon = Icons.help;
statusText = 'Unknown';
}
} else {
statusColor = Colors.grey.shade400;
statusIcon = Icons.cancel;
statusText = 'Not Paid';
}
return Container(
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(6.r),
border: Border.all(color: statusColor.withOpacity(0.3)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
children: [
Icon(
statusIcon,
color: statusColor,
size: 14.w,
),
SizedBox(width: 4.w),
Expanded(
child: Text(
member.user?.fullName ?? 'Unknown',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
SizedBox(height: 2.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
hasPayment ? _formatIndianCurrency(payment.amount) : '₹0',
style: TextStyle(
fontSize: 9.sp,
color: statusColor,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
Container(
padding: EdgeInsets.symmetric(horizontal: 3.w, vertical: 1.h),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.2),
borderRadius: BorderRadius.circular(3.r),
),
child: Text(
statusText,
style: TextStyle(
fontSize: 7.sp,
fontWeight: FontWeight.w600,
color: statusColor,
),
),
),
],
),
],
),
);
}
void _updateMonthPayments(int month, int year) {
showDialog(
context: context,
builder: (context) => _buildUpdateMonthPaymentsDialog(month, year),
).then((result) {
if (result == true) {
_loadData(); // Refresh the payment data
}
});
}
Widget _buildUpdateMonthPaymentsDialog(int month, int year) {
final monthName = _getMonthName(month);
final allMembers = widget.group.members ?? [];
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.r),
),
child: Container(
width: double.infinity,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.r),
topRight: Radius.circular(16.r),
),
),
child: Row(
children: [
Icon(
Icons.edit_calendar,
color: Colors.blue.shade700,
size: 24.w,
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Update Payments',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.blue.shade800,
),
),
Text(
'$monthName $year',
style: TextStyle(
fontSize: 14.sp,
color: Colors.blue.shade600,
),
),
],
),
),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: Icon(
Icons.close,
color: Colors.blue.shade700,
),
),
],
),
),
// Content
Flexible(
child: SingleChildScrollView(
padding: EdgeInsets.all(20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Member Payment Status',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 16.h),
// Member payment status list
...allMembers.map((member) => _buildMemberPaymentUpdateCard(member, month, year)),
],
),
),
),
// Footer
Container(
padding: EdgeInsets.all(20.w),
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(16.r),
bottomRight: Radius.circular(16.r),
),
),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text('Cancel'),
),
),
SizedBox(width: 12.w),
Expanded(
child: ElevatedButton(
onPressed: () {
// TODO: Implement bulk payment update
Navigator.of(context).pop(true);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade600,
padding: EdgeInsets.symmetric(vertical: 12.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
),
child: Text('Update Payments'),
),
),
],
),
),
],
),
),
);
}
Widget _buildMemberPaymentUpdateCard(dynamic member, int month, int year) {
// Find existing payment for this member
Payment? existingPayment;
try {
existingPayment = _paymentService.payments.firstWhere(
(p) => p.userId == member.userId && p.month == month && p.year == year,
);
} catch (e) {
existingPayment = null;
}
final hasPayment = existingPayment != null;
final isSuccess = existingPayment?.status == 'success';
return Container(
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
border: Border.all(
color: isSuccess ? Colors.green.shade200 : Colors.grey.shade200,
),
),
child: Row(
children: [
// Member info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
member.user?.fullName ?? 'Unknown Member',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
Text(
member.user?.mobileNumber ?? '',
style: TextStyle(
fontSize: 12.sp,
color: Colors.grey.shade600,
),
),
],
),
),
// Payment status
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: isSuccess ? Colors.green.shade100 : Colors.grey.shade100,
borderRadius: BorderRadius.circular(4.r),
),
child: Text(
hasPayment ? (isSuccess ? 'Paid' : 'Pending') : 'Not Paid',
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w600,
color: isSuccess ? Colors.green.shade700 : Colors.grey.shade700,
),
),
),
SizedBox(height: 4.h),
Text(
hasPayment ? _formatIndianCurrency(existingPayment.amount) : '₹0',
style: TextStyle(
fontSize: 12.sp,
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
),
),
],
),
SizedBox(width: 12.w),
// Action button
GestureDetector(
onTap: () => _recordPaymentForMember(member.userId),
child: Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(6.r),
),
child: Icon(
Icons.add,
size: 16.w,
color: Colors.blue.shade700,
),
),
),
],
),
);
}
Widget _buildPendingTab() {
return Obx(() {
if (_paymentService.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_paymentService.error.isNotEmpty) {
return _buildErrorWidget(_paymentService.error);
}
final pendingPayments = _paymentService.pendingPayments;
if (pendingPayments.isEmpty) {
return _buildEmptyWidget('No pending payments');
}
return RefreshIndicator(
onRefresh: _loadData,
child: ListView.separated(
padding: EdgeInsets.all(16.w),
itemCount: pendingPayments.length,
separatorBuilder: (context, index) => SizedBox(height: 12.h),
itemBuilder: (context, index) {
return _buildPendingPaymentCard(pendingPayments[index]);
},
),
);
});
}
Widget _buildSummaryTab() {
return Obx(() {
if (_paymentService.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_paymentService.error.isNotEmpty) {
return _buildErrorWidget(_paymentService.error);
}
final summary = _paymentService.paymentSummary;
if (summary.isEmpty) {
return _buildEmptyWidget('No summary data available');
}
return RefreshIndicator(
onRefresh: _loadData,
child: SingleChildScrollView(
padding: EdgeInsets.all(16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSummaryCards(summary),
SizedBox(height: 24.h),
_buildCollectionProgress(summary),
],
),
),
);
});
}
Widget _buildPaymentCard(Payment payment) {
return Card(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.r),
),
child: Padding(
padding: EdgeInsets.all(12.w),
child: Row(
children: [
Container(
padding: EdgeInsets.all(6.w),
decoration: BoxDecoration(
color: _getStatusColor(payment.status).withOpacity(0.1),
borderRadius: BorderRadius.circular(6.r),
),
child: Icon(
_getStatusIcon(payment.status),
color: _getStatusColor(payment.status),
size: 16.w,
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
payment.user?.fullName ?? 'Unknown Member',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 2.h),
Row(
children: [
_buildInfoChip(
'Method',
_paymentService.getPaymentMethodLabel(payment.paymentMethod),
Icons.payment,
),
if (payment.transactionId != null) ...[
SizedBox(width: 6.w),
_buildInfoChip(
'TXN',
payment.transactionId!.substring(0, 8) + '...',
Icons.receipt,
),
],
],
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
_formatIndianCurrency(payment.amount),
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 4.h),
_buildStatusChip(payment.status),
],
),
],
),
),
);
}
Widget _buildPendingPaymentCard(Map<String, dynamic> pendingPayment) {
final member = pendingPayment['member'];
final amountDue = pendingPayment['amount_due'];
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
child: Padding(
padding: EdgeInsets.all(16.w),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8.w),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(8.r),
),
child: Icon(
Icons.schedule,
color: Colors.orange.shade600,
size: 20.w,
),
),
SizedBox(width: 12.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
member['full_name'] ?? 'Unknown Member',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
Text(
member['mobile_number'] ?? '',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${(amountDue is num ? amountDue.toStringAsFixed(0) : '0')}',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 2.h),
ElevatedButton(
onPressed: () => _recordPaymentForMember(member['id']),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
minimumSize: Size(0, 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.r),
),
),
child: Text(
'Record',
style: TextStyle(fontSize: 12.sp),
),
),
],
),
],
),
),
);
}
Widget _buildSummaryCards(Map<String, dynamic> summary) {
final stats = summary['payment_stats'];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Payment Statistics',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 16.h),
Column(
children: [
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: _buildSummaryCard(
'Total Payments',
stats['total_payments'].toString(),
Icons.payments,
Colors.blue,
),
),
SizedBox(width: 8.w),
Expanded(
child: _buildSummaryCard(
'Successful',
stats['successful_payments'].toString(),
Icons.check_circle,
Colors.green,
),
),
],
),
),
SizedBox(height: 8.h),
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: _buildSummaryCard(
'Pending',
stats['pending_payments'].toString(),
Icons.schedule,
Colors.orange,
),
),
SizedBox(width: 8.w),
Expanded(
child: _buildSummaryCard(
'Failed',
stats['failed_payments'].toString(),
Icons.error,
Colors.red,
),
),
],
),
),
],
),
],
);
}
Widget _buildCollectionProgress(Map<String, dynamic> summary) {
final stats = summary['payment_stats'];
final totalCollection = stats['total_collection'] ?? 0.0;
final expectedCollection = stats['expected_collection'] ?? 1.0;
final percentage = (stats['collection_percentage'] ?? 0.0).clamp(0.0, 100.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Collection Progress',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
SizedBox(height: 16.h),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
child: Padding(
padding: EdgeInsets.all(20.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Collected: ₹${(totalCollection is num ? totalCollection.toStringAsFixed(0) : '0')}',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.green.shade700,
),
),
Text(
'${(percentage is num ? percentage.toStringAsFixed(1) : '0.0')}%',
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
color: Colors.green.shade700,
),
),
],
),
SizedBox(height: 8.h),
LinearProgressIndicator(
value: (percentage is num ? percentage / 100 : 0.0),
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation<Color>(Colors.green.shade600),
minHeight: 8.h,
),
SizedBox(height: 8.h),
Text(
'Expected: ₹${(expectedCollection is num ? expectedCollection.toStringAsFixed(0) : '0')}',
style: TextStyle(
fontSize: 14.sp,
color: Colors.grey.shade600,
),
),
],
),
),
),
],
);
}
Widget _buildSummaryCard(String title, String value, IconData icon, Color color) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.r),
),
child: Padding(
padding: EdgeInsets.all(4.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(icon, color: color, size: 12.w),
SizedBox(width: 2.w),
Expanded(
child: Text(
title,
style: TextStyle(
fontSize: 8.sp,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
Text(
value,
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w600,
color: color,
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
],
),
),
);
}
Widget _buildStatusFilter() {
return DropdownButtonFormField<String>(
value: _selectedStatus,
decoration: InputDecoration(
labelText: 'Status',
labelStyle: TextStyle(fontSize: 9.sp),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(3.r)),
contentPadding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
isDense: true,
),
style: TextStyle(fontSize: 9.sp),
items: [
const DropdownMenuItem(value: null, child: Text('All')),
const DropdownMenuItem(value: 'success', child: Text('Success')),
const DropdownMenuItem(value: 'pending', child: Text('Pending')),
const DropdownMenuItem(value: 'failed', child: Text('Failed')),
const DropdownMenuItem(value: 'cancelled', child: Text('Cancelled')),
],
onChanged: (value) {
setState(() {
_selectedStatus = value;
});
_applyFilters();
},
);
}
Widget _buildMonthFilter() {
final months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];
return DropdownButtonFormField<int>(
value: _selectedMonth,
decoration: InputDecoration(
labelText: 'Month',
labelStyle: TextStyle(fontSize: 9.sp),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(3.r)),
contentPadding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
isDense: true,
),
style: TextStyle(fontSize: 9.sp),
items: [
const DropdownMenuItem(value: null, child: Text('All')),
...List.generate(12, (index) {
return DropdownMenuItem(
value: index + 1,
child: Text(months[index]),
);
}),
],
onChanged: (value) {
setState(() {
_selectedMonth = value;
});
_applyFilters();
},
);
}
Widget _buildYearFilter() {
final currentYear = DateTime.now().year;
final years = List.generate(5, (index) => currentYear - 2 + index);
return DropdownButtonFormField<int>(
value: _selectedYear,
decoration: InputDecoration(
labelText: 'Year',
labelStyle: TextStyle(fontSize: 9.sp),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(3.r)),
contentPadding: EdgeInsets.symmetric(horizontal: 4.w, vertical: 2.h),
isDense: true,
),
style: TextStyle(fontSize: 9.sp),
items: [
const DropdownMenuItem(value: null, child: Text('All')),
...years.map((year) {
return DropdownMenuItem(
value: year,
child: Text(year.toString()),
);
}),
],
onChanged: (value) {
setState(() {
_selectedYear = value;
});
_applyFilters();
},
);
}
Widget _buildStatusChip(String status) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
decoration: BoxDecoration(
color: _getStatusColor(status).withOpacity(0.1),
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: _getStatusColor(status).withOpacity(0.3)),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 10.sp,
fontWeight: FontWeight.w600,
color: _getStatusColor(status),
),
),
);
}
Widget _buildInfoChip(String label, String value, IconData icon) {
return Container(
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 2.h),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4.r),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 10.w, color: Colors.grey.shade600),
SizedBox(width: 2.w),
Text(
'$label: $value',
style: TextStyle(
fontSize: 9.sp,
color: Colors.grey.shade700,
),
),
],
),
);
}
Widget _buildErrorWidget(String error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64.w,
color: Colors.red.shade300,
),
SizedBox(height: 16.h),
Text(
'Error',
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.w600,
color: Colors.red.shade700,
),
),
SizedBox(height: 8.h),
Text(
error,
style: TextStyle(
fontSize: 14.sp,
color: Colors.red.shade600,
),
textAlign: TextAlign.center,
),
SizedBox(height: 16.h),
ElevatedButton(
onPressed: _loadData,
child: const Text('Retry'),
),
],
),
);
}
Widget _buildEmptyWidget(String message) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.payment,
size: 64.w,
color: Colors.grey.shade300,
),
SizedBox(height: 16.h),
Text(
message,
style: TextStyle(
fontSize: 16.sp,
color: Colors.grey.shade600,
),
),
],
),
);
}
Color _getStatusColor(String status) {
switch (status) {
case 'success':
return Colors.green;
case 'pending':
return Colors.orange;
case 'failed':
return Colors.red;
case 'cancelled':
return Colors.grey;
default:
return Colors.grey;
}
}
IconData _getStatusIcon(String status) {
switch (status) {
case 'success':
return Icons.check_circle;
case 'pending':
return Icons.schedule;
case 'failed':
return Icons.error;
case 'cancelled':
return Icons.cancel;
default:
return Icons.help;
}
}
String _getMonthName(int month) {
const months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];
return months[month - 1];
}
String _formatIndianCurrency(double amount) {
// Convert to integer to avoid decimal places
int intAmount = amount.round();
// Format with Indian numbering system (commas every 2 digits after the first 3)
String amountStr = intAmount.toString();
String formatted = '';
if (amountStr.length <= 3) {
formatted = amountStr;
} else {
// For amounts > 999, use Indian comma system
int remaining = amountStr.length;
int start = 0;
// First group (rightmost 3 digits)
if (remaining > 3) {
formatted = amountStr.substring(amountStr.length - 3);
remaining -= 3;
start = amountStr.length - 3;
} else {
formatted = amountStr;
remaining = 0;
}
// Subsequent groups (2 digits each)
while (remaining > 0) {
int groupSize = remaining >= 2 ? 2 : remaining;
int groupStart = start - groupSize;
String group = amountStr.substring(groupStart, start);
formatted = group + ',' + formatted;
start = groupStart;
remaining -= groupSize;
}
}
return '$formatted';
}
void _applyFilters() {
_paymentService.loadGroupPayments(
widget.group.id,
status: _selectedStatus,
month: _selectedMonth,
year: _selectedYear,
);
}
void _showRecordPaymentDialog() {
showDialog(
context: context,
builder: (context) => RecordPaymentDialog(group: widget.group),
).then((result) {
if (result == true) {
_loadData();
}
});
}
void _recordPaymentForMember(String memberId) {
final member = widget.group.members?.firstWhere(
(m) => m.userId == memberId,
orElse: () => widget.group.members!.first,
);
showDialog(
context: context,
builder: (context) => RecordPaymentDialog(
group: widget.group,
selectedMember: member,
),
).then((result) {
if (result == true) {
_loadData();
}
});
}
}