519 lines
17 KiB
Dart
519 lines
17 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
|
|
/// Bar chart for monthly payment overview
|
|
class MonthlyPaymentChart extends StatelessWidget {
|
|
final List<PaymentData> data;
|
|
final Color barColor;
|
|
|
|
const MonthlyPaymentChart({
|
|
super.key,
|
|
required this.data,
|
|
this.barColor = const Color(0xFF2E7D32),
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Monthly Payments',
|
|
style: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 20.h),
|
|
SizedBox(
|
|
height: 200.h,
|
|
child: BarChart(
|
|
BarChartData(
|
|
alignment: BarChartAlignment.spaceAround,
|
|
maxY: data.isNotEmpty
|
|
? data.map((e) => e.amount).reduce((a, b) => a > b ? a : b) * 1.2
|
|
: 100,
|
|
barTouchData: BarTouchData(
|
|
enabled: true,
|
|
touchTooltipData: BarTouchTooltipData(
|
|
getTooltipItem: (group, groupIndex, rod, rodIndex) {
|
|
return BarTooltipItem(
|
|
'₹${rod.toY.toStringAsFixed(0)}',
|
|
TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14.sp,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
titlesData: FlTitlesData(
|
|
show: true,
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: (value, meta) {
|
|
if (value.toInt() < 0 || value.toInt() >= data.length) {
|
|
return const SizedBox();
|
|
}
|
|
return Padding(
|
|
padding: EdgeInsets.only(top: 8.h),
|
|
child: Text(
|
|
data[value.toInt()].month,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
reservedSize: 28.h,
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: (value, meta) {
|
|
return Text(
|
|
'₹${(value / 1000).toStringAsFixed(0)}k',
|
|
style: TextStyle(
|
|
fontSize: 10.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
);
|
|
},
|
|
reservedSize: 42.w,
|
|
),
|
|
),
|
|
topTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
rightTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
),
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
horizontalInterval: 5000,
|
|
getDrawingHorizontalLine: (value) {
|
|
return FlLine(
|
|
color: Colors.grey.shade200,
|
|
strokeWidth: 1,
|
|
);
|
|
},
|
|
),
|
|
borderData: FlBorderData(show: false),
|
|
barGroups: data.asMap().entries.map((entry) {
|
|
return BarChartGroupData(
|
|
x: entry.key,
|
|
barRods: [
|
|
BarChartRodData(
|
|
toY: entry.value.amount,
|
|
color: barColor,
|
|
width: 20.w,
|
|
borderRadius: BorderRadius.vertical(
|
|
top: Radius.circular(4.r),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Pie chart for payment distribution
|
|
class PaymentDistributionChart extends StatelessWidget {
|
|
final List<PaymentCategory> categories;
|
|
|
|
const PaymentDistributionChart({
|
|
super.key,
|
|
required this.categories,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Payment Distribution',
|
|
style: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 20.h),
|
|
Row(
|
|
children: [
|
|
// Pie Chart
|
|
SizedBox(
|
|
width: 150.w,
|
|
height: 150.h,
|
|
child: PieChart(
|
|
PieChartData(
|
|
sectionsSpace: 2,
|
|
centerSpaceRadius: 40.r,
|
|
sections: categories.map((category) {
|
|
return PieChartSectionData(
|
|
value: category.percentage,
|
|
title: '${category.percentage.toStringAsFixed(0)}%',
|
|
color: category.color,
|
|
radius: 50.r,
|
|
titleStyle: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
);
|
|
}).toList(),
|
|
pieTouchData: PieTouchData(
|
|
touchCallback: (FlTouchEvent event, pieTouchResponse) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 24.w),
|
|
|
|
// Legend
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: categories.map((category) {
|
|
return Padding(
|
|
padding: EdgeInsets.only(bottom: 12.h),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 16.w,
|
|
height: 16.h,
|
|
decoration: BoxDecoration(
|
|
color: category.color,
|
|
borderRadius: BorderRadius.circular(4.r),
|
|
),
|
|
),
|
|
SizedBox(width: 8.w),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
category.label,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
Text(
|
|
'₹${category.amount.toStringAsFixed(0)}',
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Line chart for payment trends
|
|
class PaymentTrendChart extends StatelessWidget {
|
|
final List<TrendData> data;
|
|
final Color lineColor;
|
|
|
|
const PaymentTrendChart({
|
|
super.key,
|
|
required this.data,
|
|
this.lineColor = const Color(0xFF2E7D32),
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Payment Trends',
|
|
style: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 20.h),
|
|
SizedBox(
|
|
height: 200.h,
|
|
child: LineChart(
|
|
LineChartData(
|
|
gridData: FlGridData(
|
|
show: true,
|
|
drawVerticalLine: false,
|
|
horizontalInterval: 5000,
|
|
getDrawingHorizontalLine: (value) {
|
|
return FlLine(
|
|
color: Colors.grey.shade200,
|
|
strokeWidth: 1,
|
|
);
|
|
},
|
|
),
|
|
titlesData: FlTitlesData(
|
|
bottomTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: (value, meta) {
|
|
if (value.toInt() < 0 || value.toInt() >= data.length) {
|
|
return const SizedBox();
|
|
}
|
|
return Padding(
|
|
padding: EdgeInsets.only(top: 8.h),
|
|
child: Text(
|
|
data[value.toInt()].label,
|
|
style: TextStyle(
|
|
fontSize: 12.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
reservedSize: 28.h,
|
|
),
|
|
),
|
|
leftTitles: AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
getTitlesWidget: (value, meta) {
|
|
return Text(
|
|
'₹${(value / 1000).toStringAsFixed(0)}k',
|
|
style: TextStyle(
|
|
fontSize: 10.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
);
|
|
},
|
|
reservedSize: 42.w,
|
|
),
|
|
),
|
|
topTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
rightTitles: AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
),
|
|
borderData: FlBorderData(show: false),
|
|
minX: 0,
|
|
maxX: (data.length - 1).toDouble(),
|
|
minY: 0,
|
|
maxY: data.isNotEmpty
|
|
? data.map((e) => e.value).reduce((a, b) => a > b ? a : b) * 1.2
|
|
: 100,
|
|
lineBarsData: [
|
|
LineChartBarData(
|
|
spots: data.asMap().entries.map((entry) {
|
|
return FlSpot(entry.key.toDouble(), entry.value.value);
|
|
}).toList(),
|
|
isCurved: true,
|
|
color: lineColor,
|
|
barWidth: 3,
|
|
isStrokeCapRound: true,
|
|
dotData: FlDotData(
|
|
show: true,
|
|
getDotPainter: (spot, percent, barData, index) {
|
|
return FlDotCirclePainter(
|
|
radius: 4,
|
|
color: lineColor,
|
|
strokeWidth: 2,
|
|
strokeColor: Colors.white,
|
|
);
|
|
},
|
|
),
|
|
belowBarData: BarAreaData(
|
|
show: true,
|
|
color: lineColor.withOpacity(0.1),
|
|
),
|
|
),
|
|
],
|
|
lineTouchData: LineTouchData(
|
|
touchTooltipData: LineTouchTooltipData(
|
|
getTooltipItems: (touchedSpots) {
|
|
return touchedSpots.map((spot) {
|
|
return LineTooltipItem(
|
|
'₹${spot.y.toStringAsFixed(0)}',
|
|
TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14.sp,
|
|
),
|
|
);
|
|
}).toList();
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Payment status overview widget
|
|
class PaymentStatusWidget extends StatelessWidget {
|
|
final int totalPayments;
|
|
final int successfulPayments;
|
|
final int pendingPayments;
|
|
final int failedPayments;
|
|
|
|
const PaymentStatusWidget({
|
|
super.key,
|
|
required this.totalPayments,
|
|
required this.successfulPayments,
|
|
required this.pendingPayments,
|
|
required this.failedPayments,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Card(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(20.w),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Payment Status',
|
|
style: TextStyle(
|
|
fontSize: 18.sp,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(height: 20.h),
|
|
_buildStatusRow(
|
|
'Successful',
|
|
successfulPayments,
|
|
totalPayments,
|
|
Colors.green.shade600,
|
|
),
|
|
SizedBox(height: 12.h),
|
|
_buildStatusRow(
|
|
'Pending',
|
|
pendingPayments,
|
|
totalPayments,
|
|
Colors.orange.shade600,
|
|
),
|
|
SizedBox(height: 12.h),
|
|
_buildStatusRow(
|
|
'Failed',
|
|
failedPayments,
|
|
totalPayments,
|
|
Colors.red.shade600,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildStatusRow(String label, int count, int total, Color color) {
|
|
final percentage = total > 0 ? (count / total * 100) : 0.0;
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
Text(
|
|
'$count (${percentage.toStringAsFixed(0)}%)',
|
|
style: TextStyle(
|
|
fontSize: 14.sp,
|
|
fontWeight: FontWeight.w600,
|
|
color: color,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
SizedBox(height: 8.h),
|
|
ClipRRect(
|
|
borderRadius: BorderRadius.circular(4.r),
|
|
child: LinearProgressIndicator(
|
|
value: percentage / 100,
|
|
backgroundColor: Colors.grey.shade200,
|
|
valueColor: AlwaysStoppedAnimation<Color>(color),
|
|
minHeight: 8.h,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
// Data classes
|
|
class PaymentData {
|
|
final String month;
|
|
final double amount;
|
|
|
|
PaymentData({required this.month, required this.amount});
|
|
}
|
|
|
|
class PaymentCategory {
|
|
final String label;
|
|
final double amount;
|
|
final double percentage;
|
|
final Color color;
|
|
|
|
PaymentCategory({
|
|
required this.label,
|
|
required this.amount,
|
|
required this.percentage,
|
|
required this.color,
|
|
});
|
|
}
|
|
|
|
class TrendData {
|
|
final String label;
|
|
final double value;
|
|
|
|
TrendData({required this.label, required this.value});
|
|
}
|
|
|