refactor: Improve Attendance and Project Progress Charts with enhanced styling and tooltip formatting

This commit is contained in:
Vaibhav Surve 2025-11-03 16:51:51 +05:30
parent 4f0261bf0b
commit 817672c8b2
2 changed files with 136 additions and 113 deletions

View File

@ -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<Map<String, dynamic>, 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(),
),
),
),
),
),
);

View File

@ -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<ChartTaskData> 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: <CartesianSeries>[
series: <ColumnSeries<ChartTaskData, String>>[
ColumnSeries<ChartTaskData, String>(
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<ChartTaskData, String>(
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(