refactor: Improve Attendance and Project Progress Charts with enhanced styling and tooltip formatting
This commit is contained in:
parent
4f0261bf0b
commit
817672c8b2
@ -60,7 +60,6 @@ class AttendanceDashboardChart extends StatelessWidget {
|
|||||||
|
|
||||||
final filteredData = _getFilteredData();
|
final filteredData = _getFilteredData();
|
||||||
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: _containerDecoration,
|
decoration: _containerDecoration,
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
@ -254,7 +253,7 @@ class _AttendanceChart extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dateFormat = DateFormat('d MMMM');
|
final dateFormat = DateFormat('d MMM');
|
||||||
final uniqueDates = data
|
final uniqueDates = data
|
||||||
.map((e) => DateTime.parse(e['date'] as String))
|
.map((e) => DateTime.parse(e['date'] as String))
|
||||||
.toSet()
|
.toSet()
|
||||||
@ -273,10 +272,6 @@ class _AttendanceChart extends StatelessWidget {
|
|||||||
if (allZero) {
|
if (allZero) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 600,
|
height: 600,
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No attendance data for the selected range.',
|
'No attendance data for the selected range.',
|
||||||
@ -302,14 +297,22 @@ class _AttendanceChart extends StatelessWidget {
|
|||||||
height: 600,
|
height: 600,
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
child: SfCartesianChart(
|
child: SfCartesianChart(
|
||||||
tooltipBehavior: TooltipBehavior(enable: true, shared: true),
|
tooltipBehavior: TooltipBehavior(enable: true, shared: true),
|
||||||
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
||||||
primaryXAxis: CategoryAxis(labelRotation: 45),
|
primaryXAxis: CategoryAxis(
|
||||||
primaryYAxis: NumericAxis(minimum: 0, interval: 1),
|
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) {
|
series: rolesWithData.map((role) {
|
||||||
final seriesData = filteredDates
|
final seriesData = filteredDates
|
||||||
.map((date) {
|
.map((date) {
|
||||||
@ -317,7 +320,7 @@ class _AttendanceChart extends StatelessWidget {
|
|||||||
return {'date': date, 'present': formattedMap[key] ?? 0};
|
return {'date': date, 'present': formattedMap[key] ?? 0};
|
||||||
})
|
})
|
||||||
.where((d) => (d['present'] ?? 0) > 0)
|
.where((d) => (d['present'] ?? 0) > 0)
|
||||||
.toList(); // ✅ remove 0 bars
|
.toList();
|
||||||
|
|
||||||
return StackedColumnSeries<Map<String, dynamic>, String>(
|
return StackedColumnSeries<Map<String, dynamic>, String>(
|
||||||
dataSource: seriesData,
|
dataSource: seriesData,
|
||||||
@ -358,7 +361,7 @@ class _AttendanceTable extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final dateFormat = DateFormat('d MMMM');
|
final dateFormat = DateFormat('d MMM');
|
||||||
final uniqueDates = data
|
final uniqueDates = data
|
||||||
.map((e) => DateTime.parse(e['date'] as String))
|
.map((e) => DateTime.parse(e['date'] as String))
|
||||||
.toSet()
|
.toSet()
|
||||||
@ -377,10 +380,6 @@ class _AttendanceTable extends StatelessWidget {
|
|||||||
if (allZero) {
|
if (allZero) {
|
||||||
return Container(
|
return Container(
|
||||||
height: 300,
|
height: 300,
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
child: const Center(
|
child: const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'No attendance data for the selected range.',
|
'No attendance data for the selected range.',
|
||||||
@ -402,15 +401,22 @@ class _AttendanceTable extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
color: Colors.grey.shade50,
|
|
||||||
),
|
),
|
||||||
|
child: Scrollbar(
|
||||||
|
thumbVisibility: true,
|
||||||
|
trackVisibility: true,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints:
|
||||||
|
BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
child: DataTable(
|
child: DataTable(
|
||||||
columnSpacing: screenWidth < 600 ? 20 : 36,
|
columnSpacing: 20,
|
||||||
headingRowHeight: 44,
|
headingRowHeight: 44,
|
||||||
headingRowColor:
|
headingRowColor: MaterialStateProperty.all(
|
||||||
MaterialStateProperty.all(Colors.blueAccent.withOpacity(0.08)),
|
Colors.blueAccent.withOpacity(0.08)),
|
||||||
headingTextStyle: const TextStyle(
|
headingTextStyle: const TextStyle(
|
||||||
fontWeight: FontWeight.bold, color: Colors.black87),
|
fontWeight: FontWeight.bold, color: Colors.black87),
|
||||||
columns: [
|
columns: [
|
||||||
@ -420,7 +426,8 @@ class _AttendanceTable extends StatelessWidget {
|
|||||||
rows: filteredRoles.map((role) {
|
rows: filteredRoles.map((role) {
|
||||||
return DataRow(
|
return DataRow(
|
||||||
cells: [
|
cells: [
|
||||||
DataCell(_RolePill(role: role, color: getRoleColor(role))),
|
DataCell(
|
||||||
|
_RolePill(role: role, color: getRoleColor(role))),
|
||||||
...filteredDates.map((date) {
|
...filteredDates.map((date) {
|
||||||
final key = '${role}_$date';
|
final key = '${role}_$date';
|
||||||
return DataCell(
|
return DataCell(
|
||||||
@ -436,6 +443,9 @@ class _AttendanceTable extends StatelessWidget {
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import 'package:syncfusion_flutter_charts/charts.dart';
|
|||||||
import 'package:marco/model/dashboard/project_progress_model.dart';
|
import 'package:marco/model/dashboard/project_progress_model.dart';
|
||||||
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
import 'package:marco/helpers/widgets/my_text.dart';
|
||||||
|
import 'package:marco/helpers/utils/utils.dart';
|
||||||
|
|
||||||
class ProjectProgressChart extends StatelessWidget {
|
class ProjectProgressChart extends StatelessWidget {
|
||||||
final List<ChartTaskData> data;
|
final List<ChartTaskData> data;
|
||||||
@ -47,7 +48,6 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
Color(0xFFFFB74D),
|
Color(0xFFFFB74D),
|
||||||
Color(0xFF64B5F6),
|
Color(0xFF64B5F6),
|
||||||
];
|
];
|
||||||
static final NumberFormat _commaFormatter = NumberFormat.decimalPattern();
|
|
||||||
|
|
||||||
Color _getTaskColor(String taskName) {
|
Color _getTaskColor(String taskName) {
|
||||||
final index = taskName.hashCode % _flatColors.length;
|
final index = taskName.hashCode % _flatColors.length;
|
||||||
@ -71,7 +71,7 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
color: Colors.grey.withOpacity(0.04),
|
color: Colors.grey.withOpacity(0.04),
|
||||||
blurRadius: 6,
|
blurRadius: 6,
|
||||||
spreadRadius: 1,
|
spreadRadius: 1,
|
||||||
offset: Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -102,6 +102,7 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= HEADER =================
|
||||||
Widget _buildHeader(
|
Widget _buildHeader(
|
||||||
String selectedRange, bool isChartView, double screenWidth) {
|
String selectedRange, bool isChartView, double screenWidth) {
|
||||||
return Column(
|
return Column(
|
||||||
@ -129,7 +130,7 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
minHeight: 30,
|
minHeight: 30,
|
||||||
minWidth: (screenWidth < 400 ? 28 : 36),
|
minWidth: screenWidth < 400 ? 28 : 36,
|
||||||
),
|
),
|
||||||
isSelected: [isChartView, !isChartView],
|
isSelected: [isChartView, !isChartView],
|
||||||
onPressed: (index) {
|
onPressed: (index) {
|
||||||
@ -185,50 +186,64 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= CHART =================
|
||||||
Widget _buildChart(double height) {
|
Widget _buildChart(double height) {
|
||||||
final nonZeroData =
|
final nonZeroData =
|
||||||
data.where((d) => d.planned != 0 || d.completed != 0).toList();
|
data.where((d) => d.planned != 0 || d.completed != 0).toList();
|
||||||
|
|
||||||
if (nonZeroData.isEmpty) {
|
if (nonZeroData.isEmpty) return _buildNoDataContainer(height);
|
||||||
return _buildNoDataContainer(height);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: height > 280 ? 280 : height,
|
height: height > 280 ? 280 : height,
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blueGrey.shade50,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
child: SfCartesianChart(
|
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),
|
legend: Legend(isVisible: true, position: LegendPosition.bottom),
|
||||||
// ✅ Use CategoryAxis so only nonZeroData dates show up
|
|
||||||
primaryXAxis: CategoryAxis(
|
primaryXAxis: CategoryAxis(
|
||||||
majorGridLines: const MajorGridLines(width: 0),
|
majorGridLines: const MajorGridLines(width: 0),
|
||||||
axisLine: const AxisLine(width: 0),
|
axisLine: const AxisLine(width: 0),
|
||||||
labelRotation: 0,
|
labelRotation: 45,
|
||||||
),
|
),
|
||||||
primaryYAxis: NumericAxis(
|
primaryYAxis: NumericAxis(
|
||||||
labelFormat: '{value}',
|
|
||||||
axisLine: const AxisLine(width: 0),
|
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>(
|
ColumnSeries<ChartTaskData, String>(
|
||||||
name: 'Planned',
|
name: 'Planned',
|
||||||
dataSource: nonZeroData,
|
dataSource: nonZeroData,
|
||||||
xValueMapper: (d, _) => DateFormat('MMM d').format(d.date),
|
xValueMapper: (d, _) => DateFormat('d MMM').format(d.date),
|
||||||
yValueMapper: (d, _) => d.planned,
|
yValueMapper: (d, _) => d.planned,
|
||||||
color: _getTaskColor('Planned'),
|
color: _getTaskColor('Planned'),
|
||||||
dataLabelSettings: DataLabelSettings(
|
dataLabelSettings: DataLabelSettings(
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
builder: (data, point, series, pointIndex, seriesIndex) {
|
builder: (data, _, __, ___, ____) {
|
||||||
final value = seriesIndex == 0
|
final value = (data as ChartTaskData).planned;
|
||||||
? (data as ChartTaskData).planned
|
|
||||||
: (data as ChartTaskData).completed;
|
|
||||||
return Text(
|
return Text(
|
||||||
_commaFormatter.format(value),
|
Utils.formatCurrency(value),
|
||||||
style: const TextStyle(fontSize: 11),
|
style: const TextStyle(fontSize: 11),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -237,17 +252,15 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
ColumnSeries<ChartTaskData, String>(
|
ColumnSeries<ChartTaskData, String>(
|
||||||
name: 'Completed',
|
name: 'Completed',
|
||||||
dataSource: nonZeroData,
|
dataSource: nonZeroData,
|
||||||
xValueMapper: (d, _) => DateFormat('MMM d').format(d.date),
|
xValueMapper: (d, _) => DateFormat('d MMM').format(d.date),
|
||||||
yValueMapper: (d, _) => d.completed,
|
yValueMapper: (d, _) => d.completed,
|
||||||
color: _getTaskColor('Completed'),
|
color: _getTaskColor('Completed'),
|
||||||
dataLabelSettings: DataLabelSettings(
|
dataLabelSettings: DataLabelSettings(
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
builder: (data, point, series, pointIndex, seriesIndex) {
|
builder: (data, _, __, ___, ____) {
|
||||||
final value = seriesIndex == 0
|
final value = (data as ChartTaskData).completed;
|
||||||
? (data as ChartTaskData).planned
|
|
||||||
: (data as ChartTaskData).completed;
|
|
||||||
return Text(
|
return Text(
|
||||||
_commaFormatter.format(value),
|
Utils.formatCurrency(value),
|
||||||
style: const TextStyle(fontSize: 11),
|
style: const TextStyle(fontSize: 11),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -258,14 +271,13 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= TABLE =================
|
||||||
Widget _buildTable(double maxHeight, double screenWidth) {
|
Widget _buildTable(double maxHeight, double screenWidth) {
|
||||||
final containerHeight = maxHeight > 300 ? 300.0 : maxHeight;
|
final containerHeight = maxHeight > 300 ? 300.0 : maxHeight;
|
||||||
final nonZeroData =
|
final nonZeroData =
|
||||||
data.where((d) => d.planned != 0 || d.completed != 0).toList();
|
data.where((d) => d.planned != 0 || d.completed != 0).toList();
|
||||||
|
|
||||||
if (nonZeroData.isEmpty) {
|
if (nonZeroData.isEmpty) return _buildNoDataContainer(containerHeight);
|
||||||
return _buildNoDataContainer(containerHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
height: containerHeight,
|
height: containerHeight,
|
||||||
@ -273,14 +285,15 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
border: Border.all(color: Colors.grey.shade300),
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
color: Colors.grey.shade50,
|
color: Colors.transparent,
|
||||||
),
|
),
|
||||||
child: LayoutBuilder(
|
child: Scrollbar(
|
||||||
builder: (context, constraints) {
|
thumbVisibility: true,
|
||||||
return SingleChildScrollView(
|
trackVisibility: true,
|
||||||
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
constraints: BoxConstraints(minWidth: screenWidth),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
child: DataTable(
|
child: DataTable(
|
||||||
@ -300,11 +313,11 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
cells: [
|
cells: [
|
||||||
DataCell(Text(DateFormat('d MMM').format(task.date))),
|
DataCell(Text(DateFormat('d MMM').format(task.date))),
|
||||||
DataCell(Text(
|
DataCell(Text(
|
||||||
'${task.planned}',
|
Utils.formatCurrency(task.planned),
|
||||||
style: TextStyle(color: _getTaskColor('Planned')),
|
style: TextStyle(color: _getTaskColor('Planned')),
|
||||||
)),
|
)),
|
||||||
DataCell(Text(
|
DataCell(Text(
|
||||||
'${task.completed}',
|
Utils.formatCurrency(task.completed),
|
||||||
style: TextStyle(color: _getTaskColor('Completed')),
|
style: TextStyle(color: _getTaskColor('Completed')),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
@ -313,17 +326,17 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= NO DATA WIDGETS =================
|
||||||
Widget _buildNoDataContainer(double height) {
|
Widget _buildNoDataContainer(double height) {
|
||||||
return Container(
|
return Container(
|
||||||
height: height > 280 ? 280 : height,
|
height: height > 280 ? 280 : height,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blueGrey.shade50,
|
color: Colors.transparent,
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user