diff --git a/lib/helpers/widgets/dashbaord/project_progress_chart.dart b/lib/helpers/widgets/dashbaord/project_progress_chart.dart index 9ada6ef..e0f1fad 100644 --- a/lib/helpers/widgets/dashbaord/project_progress_chart.dart +++ b/lib/helpers/widgets/dashbaord/project_progress_chart.dart @@ -2,55 +2,51 @@ 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/model/dashboard/project_progress_model.dart'; import 'package:marco/controller/dashboard/dashboard_controller.dart'; import 'package:marco/helpers/widgets/my_text.dart'; -import 'package:marco/helpers/utils/utils.dart'; -class ProjectProgressChart extends StatelessWidget { - final List data; - final DashboardController controller = Get.find(); +class AttendanceDashboardChart extends StatelessWidget { + AttendanceDashboardChart({Key? key}) : super(key: key); - ProjectProgressChart({super.key, required this.data}); + final DashboardController _controller = Get.find(); - // ================= Flat Colors ================= static const List _flatColors = [ - Color(0xFFE57373), - Color(0xFF64B5F6), - Color(0xFF81C784), - Color(0xFFFFB74D), - Color(0xFFBA68C8), - Color(0xFFFF8A65), - Color(0xFF4DB6AC), - Color(0xFFA1887F), - Color(0xFFDCE775), - Color(0xFF9575CD), - Color(0xFF7986CB), - Color(0xFFAED581), - Color(0xFFFF7043), - Color(0xFF4FC3F7), - Color(0xFFFFD54F), - Color(0xFF90A4AE), - Color(0xFFE573BB), - Color(0xFF81D4FA), - Color(0xFFBCAAA4), - Color(0xFFA5D6A7), - Color(0xFFCE93D8), - Color(0xFFFF8A65), - Color(0xFF80CBC4), - Color(0xFFFFF176), - Color(0xFF90CAF9), - Color(0xFFE0E0E0), - Color(0xFFF48FB1), - Color(0xFFA1887F), - Color(0xFFB0BEC5), - Color(0xFF81C784), - Color(0xFFFFB74D), - Color(0xFF64B5F6), + 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 _getTaskColor(String taskName) { - final index = taskName.hashCode % _flatColors.length; + Color _getRoleColor(String role) { + final index = role.hashCode.abs() % _flatColors.length; return _flatColors[index]; } @@ -59,42 +55,39 @@ class ProjectProgressChart extends StatelessWidget { final screenWidth = MediaQuery.of(context).size.width; return Obx(() { - final isChartView = controller.projectIsChartView.value; - final selectedRange = controller.projectSelectedRange.value; + final isChartView = _controller.attendanceIsChartView.value; + final selectedRange = _controller.attendanceSelectedRange.value; + + final filteredData = _getFilteredData(); return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(5), - boxShadow: [ - BoxShadow( - color: Colors.grey.withOpacity(0.04), - blurRadius: 6, - spreadRadius: 1, - offset: const Offset(0, 2), - ), - ], - ), + decoration: _containerDecoration, padding: EdgeInsets.symmetric( vertical: 16, - horizontal: screenWidth < 600 ? 8 : 24, + horizontal: screenWidth < 600 ? 8 : 20, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildHeader(selectedRange, isChartView, screenWidth), - const SizedBox(height: 14), + _Header( + selectedRange: selectedRange, + isChartView: isChartView, + screenWidth: screenWidth, + onToggleChanged: (isChart) => + _controller.attendanceIsChartView.value = isChart, + onRangeChanged: _controller.updateAttendanceRange, + ), + const SizedBox(height: 12), Expanded( - child: LayoutBuilder( - builder: (context, constraints) => AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: data.isEmpty - ? _buildNoDataMessage() - : isChartView - ? _buildChart(constraints.maxHeight) - : _buildTable(constraints.maxHeight, screenWidth), - ), - ), + child: filteredData.isEmpty + ? _NoDataMessage() + : isChartView + ? _AttendanceChart( + data: filteredData, getRoleColor: _getRoleColor) + : _AttendanceTable( + data: filteredData, + getRoleColor: _getRoleColor, + screenWidth: screenWidth), ), ], ), @@ -102,22 +95,62 @@ class ProjectProgressChart extends StatelessWidget { }); } - // ================= HEADER ================= - Widget _buildHeader( - String selectedRange, bool isChartView, double screenWidth) { + 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), + ), + ], + ); + + 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( - crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyText.bodyMedium('Project Progress', fontWeight: 700), - MyText.bodySmall('Planned vs Completed', - color: Colors.grey.shade700), + MyText.bodyMedium('Attendance Overview', fontWeight: 700), + const SizedBox(height: 2), + MyText.bodySmall('Role-wise present count', + color: Colors.grey), ], ), ), @@ -133,9 +166,7 @@ class ProjectProgressChart extends StatelessWidget { minWidth: screenWidth < 400 ? 28 : 36, ), isSelected: [isChartView, !isChartView], - onPressed: (index) { - controller.projectIsChartView.value = index == 0; - }, + onPressed: (index) => onToggleChanged(index == 0), children: const [ Icon(Icons.bar_chart_rounded, size: 15), Icon(Icons.table_chart, size: 15), @@ -143,149 +174,233 @@ class ProjectProgressChart extends StatelessWidget { ), ], ), - const SizedBox(height: 6), + const SizedBox(height: 8), Row( - children: [ - _buildRangeButton("7D", selectedRange), - _buildRangeButton("15D", selectedRange), - _buildRangeButton("30D", selectedRange), - _buildRangeButton("3M", selectedRange), - _buildRangeButton("6M", selectedRange), - ], + 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(5), + side: BorderSide( + color: selectedRange == label + ? Colors.blueAccent + : Colors.grey.shade300, + ), + ), + ), + ), + ) + .toList(), ), ], ); } +} - Widget _buildRangeButton(String label, String selectedRange) { - return Padding( - padding: const EdgeInsets.only(right: 4.0), - 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: (_) => controller.updateProjectRange(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(5), - side: BorderSide( - color: selectedRange == label - ? Colors.blueAccent - : Colors.grey.shade300, - ), +// 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 ================= - Widget _buildChart(double height) { - final nonZeroData = - data.where((d) => d.planned != 0 || d.completed != 0).toList(); +// Chart +class _AttendanceChart extends StatelessWidget { + const _AttendanceChart({ + Key? key, + required this.data, + required this.getRoleColor, + }) : super(key: key); - if (nonZeroData.isEmpty) return _buildNoDataContainer(height); + final List> data; + final Color Function(String role) getRoleColor; + + @override + Widget build(BuildContext context) { + final dateFormat = DateFormat('d MMM'); + 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, + 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: height > 280 ? 280 : height, + height: 600, padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.transparent, borderRadius: BorderRadius.circular(5), ), child: SfCartesianChart( - tooltipBehavior: TooltipBehavior( - enable: true, - builder: (data, point, series, pointIndex, seriesIndex) { - final task = data as ChartTaskData; - final value = seriesIndex == 0 ? task.planned : task.completed; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - decoration: BoxDecoration( - color: Colors.blueAccent, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - Utils.formatCurrency(value), - style: const TextStyle(color: Colors.white), - ), - ); - }, - ), + tooltipBehavior: TooltipBehavior(enable: true, shared: true), legend: Legend(isVisible: true, position: LegendPosition.bottom), primaryXAxis: CategoryAxis( - majorGridLines: const MajorGridLines(width: 0), - axisLine: const AxisLine(width: 0), labelRotation: 45, + majorGridLines: + const MajorGridLines(width: 0), // removes vertical grid lines ), primaryYAxis: NumericAxis( - axisLine: const AxisLine(width: 0), - majorGridLines: const MajorGridLines(width: 0), - labelFormat: '{value}', - numberFormat: NumberFormat.compact(), + minimum: 0, + interval: 1, + majorGridLines: + const MajorGridLines(width: 0), // removes horizontal grid lines ), - series: >[ - ColumnSeries( - name: 'Planned', - dataSource: nonZeroData, - xValueMapper: (d, _) => DateFormat('d MMM').format(d.date), - yValueMapper: (d, _) => d.planned, - color: _getTaskColor('Planned'), + 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(); + + return StackedColumnSeries, String>( + dataSource: seriesData, + xValueMapper: (d, _) => d['date'], + yValueMapper: (d, _) => d['present'], + name: role, + color: getRoleColor(role), dataLabelSettings: DataLabelSettings( isVisible: true, - builder: (data, _, __, ___, ____) { - final value = (data as ChartTaskData).planned; - return Text( - Utils.formatCurrency(value), - style: const TextStyle(fontSize: 11), - ); + builder: (dynamic data, _, __, ___, ____) { + return (data['present'] ?? 0) > 0 + ? Text( + NumberFormat.decimalPattern().format(data['present']), + style: const TextStyle(fontSize: 11), + ) + : const SizedBox.shrink(); }, ), - ), - ColumnSeries( - name: 'Completed', - dataSource: nonZeroData, - xValueMapper: (d, _) => DateFormat('d MMM').format(d.date), - yValueMapper: (d, _) => d.completed, - color: _getTaskColor('Completed'), - dataLabelSettings: DataLabelSettings( - isVisible: true, - builder: (data, _, __, ___, ____) { - final value = (data as ChartTaskData).completed; - return Text( - Utils.formatCurrency(value), - style: const TextStyle(fontSize: 11), - ); - }, - ), - ), - ], + ); + }).toList(), ), ); } +} - // ================= TABLE ================= - Widget _buildTable(double maxHeight, double screenWidth) { - final containerHeight = maxHeight > 300 ? 300.0 : maxHeight; - final nonZeroData = - data.where((d) => d.planned != 0 || d.completed != 0).toList(); +// Table +class _AttendanceTable extends StatelessWidget { + const _AttendanceTable({ + Key? key, + required this.data, + required this.getRoleColor, + required this.screenWidth, + }) : super(key: key); - if (nonZeroData.isEmpty) return _buildNoDataContainer(containerHeight); + final List> data; + final Color Function(String role) getRoleColor; + final double screenWidth; + + @override + Widget build(BuildContext context) { + final dateFormat = DateFormat('d MMM'); + 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, + 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: containerHeight, - padding: const EdgeInsets.symmetric(vertical: 8), + height: 300, decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(5), - color: Colors.transparent, ), child: Scrollbar( thumbVisibility: true, @@ -293,33 +408,40 @@ class ProjectProgressChart extends StatelessWidget { child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: ConstrainedBox( - constraints: BoxConstraints(minWidth: screenWidth), + constraints: + BoxConstraints(minWidth: MediaQuery.of(context).size.width), child: SingleChildScrollView( scrollDirection: Axis.vertical, child: DataTable( - columnSpacing: screenWidth < 600 ? 16 : 36, + columnSpacing: 20, headingRowHeight: 44, headingRowColor: MaterialStateProperty.all( Colors.blueAccent.withOpacity(0.08)), headingTextStyle: const TextStyle( fontWeight: FontWeight.bold, color: Colors.black87), - columns: const [ - DataColumn(label: Text('Date')), - DataColumn(label: Text('Planned')), - DataColumn(label: Text('Completed')), + columns: [ + const DataColumn(label: Text('Role')), + ...filteredDates + .map((d) => DataColumn(label: Center(child: Text(d)))), ], - rows: nonZeroData.map((task) { + rows: filteredRoles.map((role) { return DataRow( cells: [ - DataCell(Text(DateFormat('d MMM').format(task.date))), - DataCell(Text( - Utils.formatCurrency(task.planned), - style: TextStyle(color: _getTaskColor('Planned')), - )), - DataCell(Text( - Utils.formatCurrency(task.completed), - style: TextStyle(color: _getTaskColor('Completed')), - )), + DataCell( + _RolePill(role: role, color: getRoleColor(role))), + ...filteredDates.map((date) { + final key = '${role}_$date'; + return DataCell( + Center( + child: Text( + NumberFormat.decimalPattern() + .format(formattedMap[key] ?? 0), + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13), + ), + ), + ); + }), ], ); }).toList(), @@ -330,42 +452,24 @@ class ProjectProgressChart extends StatelessWidget { ), ); } +} - // ================= NO DATA WIDGETS ================= - Widget _buildNoDataContainer(double height) { +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( - height: height > 280 ? 280 : height, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: Colors.transparent, + color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(5), ), - child: const Center( - child: Text( - 'No project progress data for the selected range.', - style: TextStyle(fontSize: 14, color: Colors.grey), - textAlign: TextAlign.center, - ), - ), - ); - } - - Widget _buildNoDataMessage() { - return SizedBox( - height: 180, - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.info_outline, color: Colors.grey.shade400, size: 54), - const SizedBox(height: 10), - MyText.bodyMedium( - 'No project progress data available for the selected range.', - textAlign: TextAlign.center, - color: Colors.grey.shade500, - ), - ], - ), - ), + child: MyText.labelSmall(role, fontWeight: 500), ); } }