marco.pms.mobileapp/lib/helpers/widgets/dashbaord/expense_breakdown_chart.dart

230 lines
7.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:marco/controller/dashboard/dashboard_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/model/dashboard/expense_type_report_model.dart';
import 'package:marco/helpers/utils/utils.dart';
class ExpenseTypeReportChart extends StatelessWidget {
ExpenseTypeReportChart({Key? key}) : super(key: key);
final DashboardController _controller = Get.find<DashboardController>();
static const List<Color> _flatColors = [
Color(0xFFE57373), // Red 300
Color(0xFF64B5F6), // Blue 300
Color(0xFF81C784), // Green 300
Color(0xFFFFB74D), // Orange 300
Color(0xFFBA68C8), // Purple 300
Color(0xFFFF8A65), // Deep Orange 300
Color(0xFF4DB6AC), // Teal 300
Color(0xFFA1887F), // Brown 400
Color(0xFFDCE775), // Lime 300
Color(0xFF9575CD), // Deep Purple 300
Color(0xFF7986CB), // Indigo 300
Color(0xFFAED581), // Light Green 300
Color(0xFFFF7043), // Deep Orange 400
Color(0xFF4FC3F7), // Light Blue 300
Color(0xFFFFD54F), // Amber 300
Color(0xFF90A4AE), // Blue Grey 300
Color(0xFFE573BB), // Pink 300
Color(0xFF81D4FA), // Light Blue 200
Color(0xFFBCAAA4), // Brown 300
Color(0xFFA5D6A7), // Green 300
Color(0xFFCE93D8), // Purple 200
Color(0xFFFF8A65), // Deep Orange 300 (repeat to fill)
Color(0xFF80CBC4), // Teal 200
Color(0xFFFFF176), // Yellow 300
Color(0xFF90CAF9), // Blue 200
Color(0xFFE0E0E0), // Grey 300
Color(0xFFF48FB1), // Pink 200
Color(0xFFA1887F), // Brown 400 (repeat)
Color(0xFFB0BEC5), // Blue Grey 200
Color(0xFF81C784), // Green 300 (repeat)
Color(0xFFFFB74D), // Orange 300 (repeat)
Color(0xFF64B5F6), // Blue 300 (repeat)
];
Color _getSeriesColor(int index) => _flatColors[index % _flatColors.length];
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return Obx(() {
final isLoading = _controller.isExpenseTypeReportLoading.value;
final data = _controller.expenseTypeReportData.value;
return Container(
decoration: _containerDecoration,
padding: EdgeInsets.symmetric(
vertical: 16,
horizontal: screenWidth < 600 ? 8 : 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Header(),
const SizedBox(height: 12),
// 👇 replace Expanded with fixed height
SizedBox(
height: 350, // choose based on your design
child: isLoading
? const Center(child: CircularProgressIndicator())
: data == null || data.report.isEmpty
? const _NoDataMessage()
: _ExpenseChart(
data: data,
getSeriesColor: _getSeriesColor,
),
),
],
),
);
});
}
BoxDecoration get _containerDecoration => BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(5),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 6,
spreadRadius: 1,
offset: const Offset(0, 2),
),
],
);
}
class _Header extends StatelessWidget {
const _Header({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('Expense Type Overview', fontWeight: 700),
const SizedBox(height: 2),
MyText.bodySmall(
'Project-wise approved, pending, rejected & processed expenses',
color: Colors.grey),
],
);
}
}
// No data
class _NoDataMessage extends StatelessWidget {
const _NoDataMessage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return SizedBox(
height: 200,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, color: Colors.grey.shade400, size: 48),
const SizedBox(height: 10),
MyText.bodyMedium(
'No expense data available.',
textAlign: TextAlign.center,
color: Colors.grey.shade500,
),
],
),
),
);
}
}
// Chart
class _ExpenseChart extends StatelessWidget {
const _ExpenseChart({
Key? key,
required this.data,
required this.getSeriesColor,
}) : super(key: key);
final ExpenseTypeReportData data;
final Color Function(int index) getSeriesColor;
@override
Widget build(BuildContext context) {
final List<Map<String, dynamic>> chartSeries = [
{
'name': 'Approved',
'color': getSeriesColor(0),
'yValue': (ExpenseTypeReportItem e) => e.totalApprovedAmount,
},
{
'name': 'Pending',
'color': getSeriesColor(1),
'yValue': (ExpenseTypeReportItem e) => e.totalPendingAmount,
},
{
'name': 'Rejected',
'color': getSeriesColor(2),
'yValue': (ExpenseTypeReportItem e) => e.totalRejectedAmount,
},
{
'name': 'Processed',
'color': getSeriesColor(3),
'yValue': (ExpenseTypeReportItem e) => e.totalProcessedAmount,
},
];
return SfCartesianChart(
tooltipBehavior: TooltipBehavior(
enable: true,
shared: true,
builder: (data, point, series, pointIndex, seriesIndex) {
final ExpenseTypeReportItem item = data;
final value = chartSeries[seriesIndex]['yValue'](item);
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black87,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${chartSeries[seriesIndex]['name']}: ${Utils.formatCurrency(value)}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
);
},
),
legend: Legend(isVisible: true, position: LegendPosition.bottom),
primaryXAxis: CategoryAxis(labelRotation: 45),
primaryYAxis: NumericAxis(
// ✅ Format axis labels with Utils
axisLabelFormatter: (AxisLabelRenderDetails details) {
final num value = details.value;
return ChartAxisLabel(
Utils.formatCurrency(value), const TextStyle(fontSize: 10));
},
axisLine: const AxisLine(width: 0),
majorGridLines: const MajorGridLines(width: 0.5),
),
series: chartSeries.map((seriesInfo) {
return ColumnSeries<ExpenseTypeReportItem, String>(
dataSource: data.report,
xValueMapper: (item, _) => item.projectName,
yValueMapper: (item, _) => seriesInfo['yValue'](item),
name: seriesInfo['name'],
color: seriesInfo['color'],
dataLabelSettings: const DataLabelSettings(isVisible: true),
// ✅ Format data labels as well
dataLabelMapper: (item, _) =>
Utils.formatCurrency(seriesInfo['yValue'](item)),
);
}).toList(),
);
}
}