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();
|
||||
|
||||
|
||||
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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user