marco.pms.mobileapp/lib/view/dashboard/dashboard_chart.dart

307 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.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/helpers/widgets/my_custom_skeleton.dart';
class AttendanceDashboardChart extends StatelessWidget {
final DashboardController controller = Get.find<DashboardController>();
AttendanceDashboardChart({super.key});
List<Map<String, dynamic>> get filteredData {
final now = DateTime.now();
final daysBack = controller.rangeDays;
return controller.roleWiseData.where((entry) {
final date = DateTime.parse(entry['date']);
return date.isAfter(now.subtract(Duration(days: daysBack))) &&
!date.isAfter(now);
}).toList();
}
List<DateTime> get filteredDateTimes {
final uniqueDates = filteredData
.map((e) => DateTime.parse(e['date'] as String))
.toSet()
.toList()
..sort();
return uniqueDates;
}
List<String> get filteredDates =>
filteredDateTimes.map((d) => DateFormat('d MMMM').format(d)).toList();
List<String> get filteredRoles =>
filteredData.map((e) => e['role'] as String).toSet().toList();
List<String> get rolesWithData => filteredRoles.where((role) {
return filteredData.any(
(entry) => entry['role'] == role && (entry['present'] ?? 0) > 0);
}).toList();
final Map<String, Color> _roleColorMap = {};
final List<Color> flatColors = [
const Color(0xFFE57373),
const Color(0xFF64B5F6),
const Color(0xFF81C784),
const Color(0xFFFFB74D),
const Color(0xFFBA68C8),
const Color(0xFFFF8A65),
const Color(0xFF4DB6AC),
const Color(0xFFA1887F),
const Color(0xFFDCE775),
const Color(0xFF9575CD),
];
Color _getRoleColor(String role) {
if (_roleColorMap.containsKey(role)) return _roleColorMap[role]!;
final index = _roleColorMap.length % flatColors.length;
final color = flatColors[index];
_roleColorMap[role] = color;
return color;
}
@override
Widget build(BuildContext context) {
return Obx(() {
final isChartView = controller.isChartView.value;
final selectedRange = controller.selectedRange.value;
final isLoading = controller.isLoading.value;
return Container(
// flat white background
color: Colors.white,
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(selectedRange, isChartView),
const SizedBox(height: 12),
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: isLoading
? SkeletonLoaders.buildLoadingSkeleton()
: filteredData.isEmpty
? _buildNoDataMessage()
: isChartView
? _buildChart()
: _buildTable(),
),
],
),
);
});
}
Widget _buildHeader(String selectedRange, bool isChartView) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium('Attendance Overview', fontWeight: 600),
MyText.bodySmall('Role-wise present count', color: Colors.grey),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey.shade300),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: 'Select Range',
onSelected: (value) => controller.selectedRange.value = value,
itemBuilder: (context) => const [
PopupMenuItem(value: '7D', child: Text('Last 7 Days')),
PopupMenuItem(value: '15D', child: Text('Last 15 Days')),
PopupMenuItem(value: '30D', child: Text('Last 30 Days')),
],
child: Row(
children: [
const Icon(Icons.calendar_today_outlined, size: 18),
const SizedBox(width: 4),
MyText.labelSmall(selectedRange),
],
),
),
const SizedBox(width: 8),
IconButton(
icon: Icon(
Icons.bar_chart_rounded,
size: 20,
color: isChartView ? Colors.blueAccent : Colors.grey,
),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
onPressed: () => controller.isChartView.value = true,
tooltip: 'Chart View',
),
IconButton(
icon: Icon(
Icons.table_chart,
size: 20,
color: !isChartView ? Colors.blueAccent : Colors.grey,
),
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
onPressed: () => controller.isChartView.value = false,
tooltip: 'Table View',
),
],
),
),
],
),
);
}
Widget _buildChart() {
final formattedDateMap = {
for (var e in filteredData)
'${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}':
e['present']
};
return SizedBox(
height: 360,
child: SfCartesianChart(
tooltipBehavior: TooltipBehavior(
enable: true,
shared: true,
activationMode: ActivationMode.singleTap,
tooltipPosition: TooltipPosition.pointer,
),
legend: const Legend(
isVisible: true,
position: LegendPosition.bottom,
overflowMode: LegendItemOverflowMode.wrap,
),
primaryXAxis: CategoryAxis(
labelRotation: 45,
majorGridLines: const MajorGridLines(width: 0),
),
primaryYAxis: NumericAxis(
minimum: 0,
interval: 1,
majorGridLines: const MajorGridLines(width: 0),
),
series: rolesWithData.map((role) {
final data = filteredDates.map((formattedDate) {
final key = '${role}_$formattedDate';
return {
'date': formattedDate,
'present': formattedDateMap[key] ?? 0
};
}).toList();
return StackedColumnSeries<Map<String, dynamic>, String>(
dataSource: data,
xValueMapper: (d, _) => d['date'],
yValueMapper: (d, _) => d['present'],
name: role,
legendIconType: LegendIconType.circle,
dataLabelSettings: const DataLabelSettings(isVisible: true),
dataLabelMapper: (d, _) =>
d['present'] == 0 ? '' : d['present'].toString(),
color: _getRoleColor(role),
);
}).toList(),
),
);
}
Widget _buildNoDataMessage() {
return SizedBox(
height: 200,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.info_outline, color: Colors.grey.shade500, size: 48),
const SizedBox(height: 12),
MyText.bodyMedium(
'No attendance data available for the selected range or project.',
textAlign: TextAlign.center,
color: Colors.grey.shade600,
),
],
),
),
);
}
Widget _buildTable() {
final formattedDateMap = {
for (var e in filteredData)
'${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}':
e['present']
};
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columnSpacing: 28,
headingRowHeight: 42,
headingRowColor:
WidgetStateProperty.all(Colors.blueAccent.withOpacity(0.1)),
headingTextStyle: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.black87),
columns: [
DataColumn(label: MyText.labelSmall('Role', fontWeight: 600)),
...filteredDates.map((date) => DataColumn(
label: MyText.labelSmall(date, fontWeight: 600),
)),
],
rows: filteredRoles.map((role) {
return DataRow(
cells: [
DataCell(Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: _rolePill(role),
)),
...filteredDates.map((date) {
final key = '${role}_$date';
return DataCell(Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: MyText.labelSmall('${formattedDateMap[key] ?? 0}'),
));
}),
],
);
}).toList(),
),
),
);
}
Widget _rolePill(String role) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: _getRoleColor(role).withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
),
child: MyText.labelSmall(role, fontWeight: 500),
);
}
}