1618 lines
50 KiB
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();
|
|
}
|
|
});
|
|
}
|
|
}
|