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 { AttendanceDashboardChart({Key? key}) : super(key: key); final DashboardController _controller = Get.find(); static const List _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 _getRoleColor(String role) { final index = role.hashCode.abs() % _flatColors.length; return _flatColors[index]; } @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; return Obx(() { final isChartView = _controller.attendanceIsChartView.value; final selectedRange = _controller.attendanceSelectedRange.value; final isLoading = _controller.isAttendanceLoading.value; final filteredData = _getFilteredData(); if (isLoading) { return SkeletonLoaders.buildLoadingSkeleton(); } return Container( decoration: _containerDecoration, padding: EdgeInsets.symmetric( vertical: 16, horizontal: screenWidth < 600 ? 8 : 20, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _Header( selectedRange: selectedRange, isChartView: isChartView, screenWidth: screenWidth, onToggleChanged: (isChart) => _controller.attendanceIsChartView.value = isChart, onRangeChanged: _controller.updateAttendanceRange, ), const SizedBox(height: 12), Expanded( child: filteredData.isEmpty ? _NoDataMessage() : isChartView ? _AttendanceChart( data: filteredData, getRoleColor: _getRoleColor) : _AttendanceTable( data: filteredData, getRoleColor: _getRoleColor, screenWidth: screenWidth), ), ], ), ); }); } BoxDecoration get _containerDecoration => BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(14), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.05), blurRadius: 6, spreadRadius: 1, offset: const Offset(0, 2), ), ], ); List> _getFilteredData() { final now = DateTime.now(); final daysBack = _controller.getAttendanceDays(); return _controller.roleWiseData.where((entry) { final date = DateTime.parse(entry['date'] as String); return date.isAfter(now.subtract(Duration(days: daysBack))) && !date.isAfter(now); }).toList(); } } // Header class _Header extends StatelessWidget { const _Header({ Key? key, required this.selectedRange, required this.isChartView, required this.screenWidth, required this.onToggleChanged, required this.onRangeChanged, }) : super(key: key); final String selectedRange; final bool isChartView; final double screenWidth; final ValueChanged onToggleChanged; final ValueChanged onRangeChanged; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyText.bodyMedium('Attendance Overview', fontWeight: 700), const SizedBox(height: 2), MyText.bodySmall('Role-wise present count', color: Colors.grey), ], ), ), ToggleButtons( borderRadius: BorderRadius.circular(6), borderColor: Colors.grey, fillColor: Colors.blueAccent.withOpacity(0.15), selectedBorderColor: Colors.blueAccent, selectedColor: Colors.blueAccent, color: Colors.grey, constraints: BoxConstraints( minHeight: 30, minWidth: screenWidth < 400 ? 28 : 36, ), isSelected: [isChartView, !isChartView], onPressed: (index) => onToggleChanged(index == 0), children: const [ Icon(Icons.bar_chart_rounded, size: 15), Icon(Icons.table_chart, size: 15), ], ), ], ), const SizedBox(height: 8), Row( children: ["7D", "15D", "30D"] .map( (label) => Padding( padding: const EdgeInsets.only(right: 4), child: ChoiceChip( label: Text(label, style: const TextStyle(fontSize: 12)), padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, visualDensity: VisualDensity.compact, selected: selectedRange == label, onSelected: (_) => onRangeChanged(label), selectedColor: Colors.blueAccent.withOpacity(0.15), backgroundColor: Colors.grey.shade200, labelStyle: TextStyle( color: selectedRange == label ? Colors.blueAccent : Colors.black87, fontWeight: selectedRange == label ? FontWeight.w600 : FontWeight.normal, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(6), side: BorderSide( color: selectedRange == label ? Colors.blueAccent : Colors.grey.shade300, ), ), ), ), ) .toList(), ), ], ); } } // No Data class _NoDataMessage extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox( height: 180, 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 attendance data available for this range.', textAlign: TextAlign.center, color: Colors.grey.shade500, ), ], ), ), ); } } // Chart class _AttendanceChart extends StatelessWidget { const _AttendanceChart({ Key? key, required this.data, required this.getRoleColor, }) : super(key: key); final List> data; final Color Function(String role) getRoleColor; @override Widget build(BuildContext context) { final dateFormat = DateFormat('d MMMM'); final uniqueDates = data .map((e) => DateTime.parse(e['date'] as String)) .toSet() .toList() ..sort(); final filteredDates = uniqueDates.map(dateFormat.format).toList(); final filteredRoles = data.map((e) => e['role'] as String).toSet().toList(); final allZero = filteredRoles.every((role) { return data .where((entry) => entry['role'] == role) .every((entry) => (entry['present'] ?? 0) == 0); }); if (allZero) { return Container( height: 600, decoration: BoxDecoration( color: Colors.blueGrey.shade50, borderRadius: BorderRadius.circular(8), ), child: const Center( child: Text( 'No attendance data for the selected range.', textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: Colors.grey), ), ), ); } final formattedMap = { for (var e in data) '${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}': e['present'], }; final rolesWithData = filteredRoles.where((role) { return data .any((entry) => entry['role'] == role && (entry['present'] ?? 0) > 0); }).toList(); return Container( height: 600, padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: Colors.blueGrey.shade50, borderRadius: BorderRadius.circular(8), ), child: SfCartesianChart( tooltipBehavior: TooltipBehavior(enable: true, shared: true), legend: Legend(isVisible: true, position: LegendPosition.bottom), primaryXAxis: CategoryAxis(labelRotation: 45), primaryYAxis: NumericAxis(minimum: 0, interval: 1), series: rolesWithData.map((role) { final seriesData = filteredDates .map((date) { final key = '${role}_$date'; return {'date': date, 'present': formattedMap[key] ?? 0}; }) .where((d) => (d['present'] ?? 0) > 0) .toList(); // ✅ remove 0 bars return StackedColumnSeries, String>( dataSource: seriesData, xValueMapper: (d, _) => d['date'], yValueMapper: (d, _) => d['present'], name: role, color: getRoleColor(role), dataLabelSettings: DataLabelSettings( isVisible: true, builder: (dynamic data, _, __, ___, ____) { return (data['present'] ?? 0) > 0 ? Text( NumberFormat.decimalPattern().format(data['present']), style: const TextStyle(fontSize: 11), ) : const SizedBox.shrink(); }, ), ); }).toList(), ), ); } } // Table class _AttendanceTable extends StatelessWidget { const _AttendanceTable({ Key? key, required this.data, required this.getRoleColor, required this.screenWidth, }) : super(key: key); final List> data; final Color Function(String role) getRoleColor; final double screenWidth; @override Widget build(BuildContext context) { final dateFormat = DateFormat('d MMMM'); final uniqueDates = data .map((e) => DateTime.parse(e['date'] as String)) .toSet() .toList() ..sort(); final filteredDates = uniqueDates.map(dateFormat.format).toList(); final filteredRoles = data.map((e) => e['role'] as String).toSet().toList(); final allZero = filteredRoles.every((role) { return data .where((entry) => entry['role'] == role) .every((entry) => (entry['present'] ?? 0) == 0); }); if (allZero) { return Container( height: 300, decoration: BoxDecoration( color: Colors.grey.shade50, borderRadius: BorderRadius.circular(12), ), child: const Center( child: Text( 'No attendance data for the selected range.', textAlign: TextAlign.center, style: TextStyle(fontSize: 14, color: Colors.grey), ), ), ); } final formattedMap = { for (var e in data) '${e['role']}_${dateFormat.format(DateTime.parse(e['date'] as String))}': e['present'], }; return Container( height: 300, decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(12), color: Colors.grey.shade50, ), child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: DataTable( columnSpacing: screenWidth < 600 ? 20 : 36, headingRowHeight: 44, headingRowColor: MaterialStateProperty.all(Colors.blueAccent.withOpacity(0.08)), headingTextStyle: const TextStyle( fontWeight: FontWeight.bold, color: Colors.black87), columns: [ const DataColumn(label: Text('Role')), ...filteredDates.map((d) => DataColumn(label: Text(d))), ], rows: filteredRoles.map((role) { return DataRow( cells: [ DataCell(_RolePill(role: role, color: getRoleColor(role))), ...filteredDates.map((date) { final key = '${role}_$date'; return DataCell( Text( NumberFormat.decimalPattern() .format(formattedMap[key] ?? 0), style: const TextStyle(fontSize: 13), ), ); }), ], ); }).toList(), ), ), ); } } class _RolePill extends StatelessWidget { const _RolePill({Key? key, required this.role, required this.color}) : super(key: key); final String role; final Color color; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(6), ), child: MyText.labelSmall(role, fontWeight: 500), ); } }