refactor: Rename ProjectProgressChart to AttendanceDashboardChart and update data handling for attendance overview
This commit is contained in:
parent
817672c8b2
commit
b33b3da6c0
@ -2,55 +2,51 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:syncfusion_flutter_charts/charts.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/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 AttendanceDashboardChart extends StatelessWidget {
|
||||||
final List<ChartTaskData> data;
|
AttendanceDashboardChart({Key? key}) : super(key: key);
|
||||||
final DashboardController controller = Get.find<DashboardController>();
|
|
||||||
|
|
||||||
ProjectProgressChart({super.key, required this.data});
|
final DashboardController _controller = Get.find<DashboardController>();
|
||||||
|
|
||||||
// ================= Flat Colors =================
|
|
||||||
static const List<Color> _flatColors = [
|
static const List<Color> _flatColors = [
|
||||||
Color(0xFFE57373),
|
Color(0xFFE57373), // Red 300
|
||||||
Color(0xFF64B5F6),
|
Color(0xFF64B5F6), // Blue 300
|
||||||
Color(0xFF81C784),
|
Color(0xFF81C784), // Green 300
|
||||||
Color(0xFFFFB74D),
|
Color(0xFFFFB74D), // Orange 300
|
||||||
Color(0xFFBA68C8),
|
Color(0xFFBA68C8), // Purple 300
|
||||||
Color(0xFFFF8A65),
|
Color(0xFFFF8A65), // Deep Orange 300
|
||||||
Color(0xFF4DB6AC),
|
Color(0xFF4DB6AC), // Teal 300
|
||||||
Color(0xFFA1887F),
|
Color(0xFFA1887F), // Brown 400
|
||||||
Color(0xFFDCE775),
|
Color(0xFFDCE775), // Lime 300
|
||||||
Color(0xFF9575CD),
|
Color(0xFF9575CD), // Deep Purple 300
|
||||||
Color(0xFF7986CB),
|
Color(0xFF7986CB), // Indigo 300
|
||||||
Color(0xFFAED581),
|
Color(0xFFAED581), // Light Green 300
|
||||||
Color(0xFFFF7043),
|
Color(0xFFFF7043), // Deep Orange 400
|
||||||
Color(0xFF4FC3F7),
|
Color(0xFF4FC3F7), // Light Blue 300
|
||||||
Color(0xFFFFD54F),
|
Color(0xFFFFD54F), // Amber 300
|
||||||
Color(0xFF90A4AE),
|
Color(0xFF90A4AE), // Blue Grey 300
|
||||||
Color(0xFFE573BB),
|
Color(0xFFE573BB), // Pink 300
|
||||||
Color(0xFF81D4FA),
|
Color(0xFF81D4FA), // Light Blue 200
|
||||||
Color(0xFFBCAAA4),
|
Color(0xFFBCAAA4), // Brown 300
|
||||||
Color(0xFFA5D6A7),
|
Color(0xFFA5D6A7), // Green 300
|
||||||
Color(0xFFCE93D8),
|
Color(0xFFCE93D8), // Purple 200
|
||||||
Color(0xFFFF8A65),
|
Color(0xFFFF8A65), // Deep Orange 300 (repeat to fill)
|
||||||
Color(0xFF80CBC4),
|
Color(0xFF80CBC4), // Teal 200
|
||||||
Color(0xFFFFF176),
|
Color(0xFFFFF176), // Yellow 300
|
||||||
Color(0xFF90CAF9),
|
Color(0xFF90CAF9), // Blue 200
|
||||||
Color(0xFFE0E0E0),
|
Color(0xFFE0E0E0), // Grey 300
|
||||||
Color(0xFFF48FB1),
|
Color(0xFFF48FB1), // Pink 200
|
||||||
Color(0xFFA1887F),
|
Color(0xFFA1887F), // Brown 400 (repeat)
|
||||||
Color(0xFFB0BEC5),
|
Color(0xFFB0BEC5), // Blue Grey 200
|
||||||
Color(0xFF81C784),
|
Color(0xFF81C784), // Green 300 (repeat)
|
||||||
Color(0xFFFFB74D),
|
Color(0xFFFFB74D), // Orange 300 (repeat)
|
||||||
Color(0xFF64B5F6),
|
Color(0xFF64B5F6), // Blue 300 (repeat)
|
||||||
];
|
];
|
||||||
|
|
||||||
Color _getTaskColor(String taskName) {
|
Color _getRoleColor(String role) {
|
||||||
final index = taskName.hashCode % _flatColors.length;
|
final index = role.hashCode.abs() % _flatColors.length;
|
||||||
return _flatColors[index];
|
return _flatColors[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,42 +55,39 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
final screenWidth = MediaQuery.of(context).size.width;
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
final isChartView = controller.projectIsChartView.value;
|
final isChartView = _controller.attendanceIsChartView.value;
|
||||||
final selectedRange = controller.projectSelectedRange.value;
|
final selectedRange = _controller.attendanceSelectedRange.value;
|
||||||
|
|
||||||
|
final filteredData = _getFilteredData();
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: _containerDecoration,
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.grey.withOpacity(0.04),
|
|
||||||
blurRadius: 6,
|
|
||||||
spreadRadius: 1,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.symmetric(
|
padding: EdgeInsets.symmetric(
|
||||||
vertical: 16,
|
vertical: 16,
|
||||||
horizontal: screenWidth < 600 ? 8 : 24,
|
horizontal: screenWidth < 600 ? 8 : 20,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(selectedRange, isChartView, screenWidth),
|
_Header(
|
||||||
const SizedBox(height: 14),
|
selectedRange: selectedRange,
|
||||||
|
isChartView: isChartView,
|
||||||
|
screenWidth: screenWidth,
|
||||||
|
onToggleChanged: (isChart) =>
|
||||||
|
_controller.attendanceIsChartView.value = isChart,
|
||||||
|
onRangeChanged: _controller.updateAttendanceRange,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: LayoutBuilder(
|
child: filteredData.isEmpty
|
||||||
builder: (context, constraints) => AnimatedSwitcher(
|
? _NoDataMessage()
|
||||||
duration: const Duration(milliseconds: 300),
|
: isChartView
|
||||||
child: data.isEmpty
|
? _AttendanceChart(
|
||||||
? _buildNoDataMessage()
|
data: filteredData, getRoleColor: _getRoleColor)
|
||||||
: isChartView
|
: _AttendanceTable(
|
||||||
? _buildChart(constraints.maxHeight)
|
data: filteredData,
|
||||||
: _buildTable(constraints.maxHeight, screenWidth),
|
getRoleColor: _getRoleColor,
|
||||||
),
|
screenWidth: screenWidth),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -102,22 +95,62 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= HEADER =================
|
BoxDecoration get _containerDecoration => BoxDecoration(
|
||||||
Widget _buildHeader(
|
color: Colors.white,
|
||||||
String selectedRange, bool isChartView, double screenWidth) {
|
borderRadius: BorderRadius.circular(5),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.grey.withOpacity(0.05),
|
||||||
|
blurRadius: 6,
|
||||||
|
spreadRadius: 1,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
List<Map<String, dynamic>> _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<bool> onToggleChanged;
|
||||||
|
final ValueChanged<String> onRangeChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
MyText.bodyMedium('Project Progress', fontWeight: 700),
|
MyText.bodyMedium('Attendance Overview', fontWeight: 700),
|
||||||
MyText.bodySmall('Planned vs Completed',
|
const SizedBox(height: 2),
|
||||||
color: Colors.grey.shade700),
|
MyText.bodySmall('Role-wise present count',
|
||||||
|
color: Colors.grey),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -133,9 +166,7 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
minWidth: screenWidth < 400 ? 28 : 36,
|
minWidth: screenWidth < 400 ? 28 : 36,
|
||||||
),
|
),
|
||||||
isSelected: [isChartView, !isChartView],
|
isSelected: [isChartView, !isChartView],
|
||||||
onPressed: (index) {
|
onPressed: (index) => onToggleChanged(index == 0),
|
||||||
controller.projectIsChartView.value = index == 0;
|
|
||||||
},
|
|
||||||
children: const [
|
children: const [
|
||||||
Icon(Icons.bar_chart_rounded, size: 15),
|
Icon(Icons.bar_chart_rounded, size: 15),
|
||||||
Icon(Icons.table_chart, 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(
|
Row(
|
||||||
children: [
|
children: ["7D", "15D", "30D"]
|
||||||
_buildRangeButton("7D", selectedRange),
|
.map(
|
||||||
_buildRangeButton("15D", selectedRange),
|
(label) => Padding(
|
||||||
_buildRangeButton("30D", selectedRange),
|
padding: const EdgeInsets.only(right: 4),
|
||||||
_buildRangeButton("3M", selectedRange),
|
child: ChoiceChip(
|
||||||
_buildRangeButton("6M", selectedRange),
|
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) {
|
// No Data
|
||||||
return Padding(
|
class _NoDataMessage extends StatelessWidget {
|
||||||
padding: const EdgeInsets.only(right: 4.0),
|
@override
|
||||||
child: ChoiceChip(
|
Widget build(BuildContext context) {
|
||||||
label: Text(label, style: const TextStyle(fontSize: 12)),
|
return SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 0),
|
height: 180,
|
||||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
child: Center(
|
||||||
visualDensity: VisualDensity.compact,
|
child: Column(
|
||||||
selected: selectedRange == label,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
onSelected: (_) => controller.updateProjectRange(label),
|
children: [
|
||||||
selectedColor: Colors.blueAccent.withOpacity(0.15),
|
Icon(Icons.info_outline, color: Colors.grey.shade400, size: 48),
|
||||||
backgroundColor: Colors.grey.shade200,
|
const SizedBox(height: 10),
|
||||||
labelStyle: TextStyle(
|
MyText.bodyMedium(
|
||||||
color: selectedRange == label ? Colors.blueAccent : Colors.black87,
|
'No attendance data available for this range.',
|
||||||
fontWeight:
|
textAlign: TextAlign.center,
|
||||||
selectedRange == label ? FontWeight.w600 : FontWeight.normal,
|
color: Colors.grey.shade500,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(
|
],
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
side: BorderSide(
|
|
||||||
color: selectedRange == label
|
|
||||||
? Colors.blueAccent
|
|
||||||
: Colors.grey.shade300,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================= CHART =================
|
// Chart
|
||||||
Widget _buildChart(double height) {
|
class _AttendanceChart extends StatelessWidget {
|
||||||
final nonZeroData =
|
const _AttendanceChart({
|
||||||
data.where((d) => d.planned != 0 || d.completed != 0).toList();
|
Key? key,
|
||||||
|
required this.data,
|
||||||
|
required this.getRoleColor,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
if (nonZeroData.isEmpty) return _buildNoDataContainer(height);
|
final List<Map<String, dynamic>> 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(
|
return Container(
|
||||||
height: height > 280 ? 280 : height,
|
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(
|
tooltipBehavior: TooltipBehavior(enable: true, shared: true),
|
||||||
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),
|
||||||
primaryXAxis: CategoryAxis(
|
primaryXAxis: CategoryAxis(
|
||||||
majorGridLines: const MajorGridLines(width: 0),
|
|
||||||
axisLine: const AxisLine(width: 0),
|
|
||||||
labelRotation: 45,
|
labelRotation: 45,
|
||||||
|
majorGridLines:
|
||||||
|
const MajorGridLines(width: 0), // removes vertical grid lines
|
||||||
),
|
),
|
||||||
primaryYAxis: NumericAxis(
|
primaryYAxis: NumericAxis(
|
||||||
axisLine: const AxisLine(width: 0),
|
minimum: 0,
|
||||||
majorGridLines: const MajorGridLines(width: 0),
|
interval: 1,
|
||||||
labelFormat: '{value}',
|
majorGridLines:
|
||||||
numberFormat: NumberFormat.compact(),
|
const MajorGridLines(width: 0), // removes horizontal grid lines
|
||||||
),
|
),
|
||||||
series: <ColumnSeries<ChartTaskData, String>>[
|
series: rolesWithData.map((role) {
|
||||||
ColumnSeries<ChartTaskData, String>(
|
final seriesData = filteredDates
|
||||||
name: 'Planned',
|
.map((date) {
|
||||||
dataSource: nonZeroData,
|
final key = '${role}_$date';
|
||||||
xValueMapper: (d, _) => DateFormat('d MMM').format(d.date),
|
return {'date': date, 'present': formattedMap[key] ?? 0};
|
||||||
yValueMapper: (d, _) => d.planned,
|
})
|
||||||
color: _getTaskColor('Planned'),
|
.where((d) => (d['present'] ?? 0) > 0)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return StackedColumnSeries<Map<String, dynamic>, String>(
|
||||||
|
dataSource: seriesData,
|
||||||
|
xValueMapper: (d, _) => d['date'],
|
||||||
|
yValueMapper: (d, _) => d['present'],
|
||||||
|
name: role,
|
||||||
|
color: getRoleColor(role),
|
||||||
dataLabelSettings: DataLabelSettings(
|
dataLabelSettings: DataLabelSettings(
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
builder: (data, _, __, ___, ____) {
|
builder: (dynamic data, _, __, ___, ____) {
|
||||||
final value = (data as ChartTaskData).planned;
|
return (data['present'] ?? 0) > 0
|
||||||
return Text(
|
? Text(
|
||||||
Utils.formatCurrency(value),
|
NumberFormat.decimalPattern().format(data['present']),
|
||||||
style: const TextStyle(fontSize: 11),
|
style: const TextStyle(fontSize: 11),
|
||||||
);
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
ColumnSeries<ChartTaskData, String>(
|
}).toList(),
|
||||||
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),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================= TABLE =================
|
// Table
|
||||||
Widget _buildTable(double maxHeight, double screenWidth) {
|
class _AttendanceTable extends StatelessWidget {
|
||||||
final containerHeight = maxHeight > 300 ? 300.0 : maxHeight;
|
const _AttendanceTable({
|
||||||
final nonZeroData =
|
Key? key,
|
||||||
data.where((d) => d.planned != 0 || d.completed != 0).toList();
|
required this.data,
|
||||||
|
required this.getRoleColor,
|
||||||
|
required this.screenWidth,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
if (nonZeroData.isEmpty) return _buildNoDataContainer(containerHeight);
|
final List<Map<String, dynamic>> 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(
|
return Container(
|
||||||
height: containerHeight,
|
height: 300,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
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.transparent,
|
|
||||||
),
|
),
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
thumbVisibility: true,
|
thumbVisibility: true,
|
||||||
@ -293,33 +408,40 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: screenWidth),
|
constraints:
|
||||||
|
BoxConstraints(minWidth: MediaQuery.of(context).size.width),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.vertical,
|
scrollDirection: Axis.vertical,
|
||||||
child: DataTable(
|
child: DataTable(
|
||||||
columnSpacing: screenWidth < 600 ? 16 : 36,
|
columnSpacing: 20,
|
||||||
headingRowHeight: 44,
|
headingRowHeight: 44,
|
||||||
headingRowColor: MaterialStateProperty.all(
|
headingRowColor: 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: const [
|
columns: [
|
||||||
DataColumn(label: Text('Date')),
|
const DataColumn(label: Text('Role')),
|
||||||
DataColumn(label: Text('Planned')),
|
...filteredDates
|
||||||
DataColumn(label: Text('Completed')),
|
.map((d) => DataColumn(label: Center(child: Text(d)))),
|
||||||
],
|
],
|
||||||
rows: nonZeroData.map((task) {
|
rows: filteredRoles.map((role) {
|
||||||
return DataRow(
|
return DataRow(
|
||||||
cells: [
|
cells: [
|
||||||
DataCell(Text(DateFormat('d MMM').format(task.date))),
|
DataCell(
|
||||||
DataCell(Text(
|
_RolePill(role: role, color: getRoleColor(role))),
|
||||||
Utils.formatCurrency(task.planned),
|
...filteredDates.map((date) {
|
||||||
style: TextStyle(color: _getTaskColor('Planned')),
|
final key = '${role}_$date';
|
||||||
)),
|
return DataCell(
|
||||||
DataCell(Text(
|
Center(
|
||||||
Utils.formatCurrency(task.completed),
|
child: Text(
|
||||||
style: TextStyle(color: _getTaskColor('Completed')),
|
NumberFormat.decimalPattern()
|
||||||
)),
|
.format(formattedMap[key] ?? 0),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@ -330,42 +452,24 @@ class ProjectProgressChart extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================= NO DATA WIDGETS =================
|
class _RolePill extends StatelessWidget {
|
||||||
Widget _buildNoDataContainer(double height) {
|
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(
|
return Container(
|
||||||
height: height > 280 ? 280 : height,
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.transparent,
|
color: color.withOpacity(0.15),
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(5),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: MyText.labelSmall(role, fontWeight: 500),
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user