230 lines
7.3 KiB
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(),
|
|
);
|
|
}
|
|
}
|