From 817672c8b2000b5a778d7c9f29d1f7adc2da84eb Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 3 Nov 2025 16:51:51 +0530 Subject: [PATCH] refactor: Improve Attendance and Project Progress Charts with enhanced styling and tooltip formatting --- .../dashbaord/attendance_overview_chart.dart | 100 ++++++------ .../dashbaord/project_progress_chart.dart | 149 ++++++++++-------- 2 files changed, 136 insertions(+), 113 deletions(-) diff --git a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart index 3de75cd..3435b20 100644 --- a/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart +++ b/lib/helpers/widgets/dashbaord/attendance_overview_chart.dart @@ -60,7 +60,6 @@ class AttendanceDashboardChart extends StatelessWidget { final filteredData = _getFilteredData(); - return Container( decoration: _containerDecoration, padding: EdgeInsets.symmetric( @@ -254,7 +253,7 @@ class _AttendanceChart extends StatelessWidget { @override Widget build(BuildContext context) { - final dateFormat = DateFormat('d MMMM'); + final dateFormat = DateFormat('d MMM'); final uniqueDates = data .map((e) => DateTime.parse(e['date'] as String)) .toSet() @@ -273,10 +272,6 @@ class _AttendanceChart extends StatelessWidget { if (allZero) { return Container( height: 600, - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(5), - ), child: const Center( child: Text( 'No attendance data for the selected range.', @@ -302,14 +297,22 @@ class _AttendanceChart extends StatelessWidget { height: 600, padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.transparent, borderRadius: BorderRadius.circular(5), ), 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), + primaryXAxis: CategoryAxis( + labelRotation: 45, + majorGridLines: + const MajorGridLines(width: 0), // removes vertical grid lines + ), + primaryYAxis: NumericAxis( + minimum: 0, + interval: 1, + majorGridLines: + const MajorGridLines(width: 0), // removes horizontal grid lines + ), series: rolesWithData.map((role) { final seriesData = filteredDates .map((date) { @@ -317,7 +320,7 @@ class _AttendanceChart extends StatelessWidget { return {'date': date, 'present': formattedMap[key] ?? 0}; }) .where((d) => (d['present'] ?? 0) > 0) - .toList(); // ✅ remove 0 bars + .toList(); return StackedColumnSeries, String>( dataSource: seriesData, @@ -358,7 +361,7 @@ class _AttendanceTable extends StatelessWidget { @override Widget build(BuildContext context) { - final dateFormat = DateFormat('d MMMM'); + final dateFormat = DateFormat('d MMM'); final uniqueDates = data .map((e) => DateTime.parse(e['date'] as String)) .toSet() @@ -377,10 +380,6 @@ class _AttendanceTable extends StatelessWidget { if (allZero) { return Container( height: 300, - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(5), - ), child: const Center( child: Text( 'No attendance data for the selected range.', @@ -402,38 +401,49 @@ class _AttendanceTable extends StatelessWidget { decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(5), - 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), - ), + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: + BoxConstraints(minWidth: MediaQuery.of(context).size.width), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: DataTable( + 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('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(), + }).toList(), + ), + ), + ), ), ), ); diff --git a/lib/helpers/widgets/dashbaord/project_progress_chart.dart b/lib/helpers/widgets/dashbaord/project_progress_chart.dart index 648fc75..9ada6ef 100644 --- a/lib/helpers/widgets/dashbaord/project_progress_chart.dart +++ b/lib/helpers/widgets/dashbaord/project_progress_chart.dart @@ -5,6 +5,7 @@ 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; @@ -47,7 +48,6 @@ class ProjectProgressChart extends StatelessWidget { Color(0xFFFFB74D), Color(0xFF64B5F6), ]; - static final NumberFormat _commaFormatter = NumberFormat.decimalPattern(); Color _getTaskColor(String taskName) { final index = taskName.hashCode % _flatColors.length; @@ -71,7 +71,7 @@ class ProjectProgressChart extends StatelessWidget { color: Colors.grey.withOpacity(0.04), blurRadius: 6, spreadRadius: 1, - offset: Offset(0, 2), + offset: const Offset(0, 2), ), ], ), @@ -102,6 +102,7 @@ class ProjectProgressChart extends StatelessWidget { }); } + // ================= HEADER ================= Widget _buildHeader( String selectedRange, bool isChartView, double screenWidth) { return Column( @@ -129,7 +130,7 @@ class ProjectProgressChart extends StatelessWidget { color: Colors.grey, constraints: BoxConstraints( minHeight: 30, - minWidth: (screenWidth < 400 ? 28 : 36), + minWidth: screenWidth < 400 ? 28 : 36, ), isSelected: [isChartView, !isChartView], onPressed: (index) { @@ -185,50 +186,64 @@ class ProjectProgressChart extends StatelessWidget { ); } + // ================= CHART ================= Widget _buildChart(double height) { final nonZeroData = data.where((d) => d.planned != 0 || d.completed != 0).toList(); - if (nonZeroData.isEmpty) { - return _buildNoDataContainer(height); - } + if (nonZeroData.isEmpty) return _buildNoDataContainer(height); return Container( height: height > 280 ? 280 : height, padding: const EdgeInsets.all(6), decoration: BoxDecoration( - color: Colors.blueGrey.shade50, + color: Colors.transparent, borderRadius: BorderRadius.circular(5), ), child: SfCartesianChart( - tooltipBehavior: TooltipBehavior(enable: true), + 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), + ), + ); + }, + ), legend: Legend(isVisible: true, position: LegendPosition.bottom), - // ✅ Use CategoryAxis so only nonZeroData dates show up primaryXAxis: CategoryAxis( majorGridLines: const MajorGridLines(width: 0), axisLine: const AxisLine(width: 0), - labelRotation: 0, + labelRotation: 45, ), primaryYAxis: NumericAxis( - labelFormat: '{value}', axisLine: const AxisLine(width: 0), - majorTickLines: const MajorTickLines(size: 0), + majorGridLines: const MajorGridLines(width: 0), + labelFormat: '{value}', + numberFormat: NumberFormat.compact(), ), - series: [ + series: >[ ColumnSeries( name: 'Planned', dataSource: nonZeroData, - xValueMapper: (d, _) => DateFormat('MMM d').format(d.date), + xValueMapper: (d, _) => DateFormat('d MMM').format(d.date), yValueMapper: (d, _) => d.planned, color: _getTaskColor('Planned'), dataLabelSettings: DataLabelSettings( isVisible: true, - builder: (data, point, series, pointIndex, seriesIndex) { - final value = seriesIndex == 0 - ? (data as ChartTaskData).planned - : (data as ChartTaskData).completed; + builder: (data, _, __, ___, ____) { + final value = (data as ChartTaskData).planned; return Text( - _commaFormatter.format(value), + Utils.formatCurrency(value), style: const TextStyle(fontSize: 11), ); }, @@ -237,17 +252,15 @@ class ProjectProgressChart extends StatelessWidget { ColumnSeries( name: 'Completed', dataSource: nonZeroData, - xValueMapper: (d, _) => DateFormat('MMM d').format(d.date), + xValueMapper: (d, _) => DateFormat('d MMM').format(d.date), yValueMapper: (d, _) => d.completed, color: _getTaskColor('Completed'), dataLabelSettings: DataLabelSettings( isVisible: true, - builder: (data, point, series, pointIndex, seriesIndex) { - final value = seriesIndex == 0 - ? (data as ChartTaskData).planned - : (data as ChartTaskData).completed; + builder: (data, _, __, ___, ____) { + final value = (data as ChartTaskData).completed; return Text( - _commaFormatter.format(value), + Utils.formatCurrency(value), style: const TextStyle(fontSize: 11), ); }, @@ -258,14 +271,13 @@ class ProjectProgressChart extends StatelessWidget { ); } + // ================= 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(); - if (nonZeroData.isEmpty) { - return _buildNoDataContainer(containerHeight); - } + if (nonZeroData.isEmpty) return _buildNoDataContainer(containerHeight); return Container( height: containerHeight, @@ -273,57 +285,58 @@ class ProjectProgressChart extends StatelessWidget { decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(5), - color: Colors.grey.shade50, + color: Colors.transparent, ), - child: LayoutBuilder( - builder: (context, constraints) { - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: DataTable( - columnSpacing: screenWidth < 600 ? 16 : 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('Date')), - DataColumn(label: Text('Planned')), - DataColumn(label: Text('Completed')), - ], - rows: nonZeroData.map((task) { - return DataRow( - cells: [ - DataCell(Text(DateFormat('d MMM').format(task.date))), - DataCell(Text( - '${task.planned}', - style: TextStyle(color: _getTaskColor('Planned')), - )), - DataCell(Text( - '${task.completed}', - style: TextStyle(color: _getTaskColor('Completed')), - )), - ], - ); - }).toList(), - ), + child: Scrollbar( + thumbVisibility: true, + trackVisibility: true, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: screenWidth), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: DataTable( + columnSpacing: screenWidth < 600 ? 16 : 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('Date')), + DataColumn(label: Text('Planned')), + DataColumn(label: Text('Completed')), + ], + rows: nonZeroData.map((task) { + 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')), + )), + ], + ); + }).toList(), ), ), - ); - }, + ), + ), ), ); } + // ================= NO DATA WIDGETS ================= Widget _buildNoDataContainer(double height) { return Container( height: height > 280 ? 280 : height, decoration: BoxDecoration( - color: Colors.blueGrey.shade50, + color: Colors.transparent, borderRadius: BorderRadius.circular(5), ), child: const Center(