321 lines
11 KiB
Dart
321 lines
11 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:syncfusion_flutter_charts/charts.dart';
|
|
import 'package:marco/controller/dashboard/dashboard_controller.dart';
|
|
import 'package:marco/helpers/widgets/my_text.dart';
|
|
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
|
|
|
class AttendanceDashboardChart extends StatelessWidget {
|
|
final DashboardController controller = Get.find<DashboardController>();
|
|
|
|
AttendanceDashboardChart({super.key});
|
|
|
|
List<Map<String, dynamic>> get filteredData {
|
|
final now = DateTime.now();
|
|
final daysBack = controller.rangeDays;
|
|
return controller.roleWiseData.where((entry) {
|
|
final date = DateTime.parse(entry['date']);
|
|
return date.isAfter(now.subtract(Duration(days: daysBack))) &&
|
|
!date.isAfter(now);
|
|
}).toList();
|
|
}
|
|
|
|
List<DateTime> get filteredDateTimes {
|
|
final uniqueDates = filteredData
|
|
.map((e) => DateTime.parse(e['date'] as String))
|
|
.toSet()
|
|
.toList()
|
|
..sort();
|
|
return uniqueDates;
|
|
}
|
|
|
|
List<String> get filteredDates =>
|
|
filteredDateTimes.map((d) => DateFormat('d MMMM').format(d)).toList();
|
|
|
|
List<String> get filteredRoles =>
|
|
filteredData.map((e) => e['role'] as String).toSet().toList();
|
|
|
|
final Map<String, Color> _roleColorMap = {};
|
|
final List<Color> flatColors = [
|
|
const Color(0xFFE57373),
|
|
const Color(0xFF64B5F6),
|
|
const Color(0xFF81C784),
|
|
const Color(0xFFFFB74D),
|
|
const Color(0xFFBA68C8),
|
|
const Color(0xFFFF8A65),
|
|
const Color(0xFF4DB6AC),
|
|
const Color(0xFFA1887F),
|
|
const Color(0xFFDCE775),
|
|
const Color(0xFF9575CD),
|
|
];
|
|
|
|
Color _getRoleColor(String role) {
|
|
if (_roleColorMap.containsKey(role)) return _roleColorMap[role]!;
|
|
|
|
final index = _roleColorMap.length % flatColors.length;
|
|
final color = flatColors[index];
|
|
_roleColorMap[role] = color;
|
|
return color;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Obx(() {
|
|
// Now this observes `controller.roleWiseData`, `isLoading`, `isChartView`, etc.
|
|
final isChartView = controller.isChartView.value;
|
|
final selectedRange = controller.selectedRange.value;
|
|
final isLoading = controller.isLoading.value;
|
|
|
|
return Container(
|
|
decoration: const BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [Color(0xfff0f4f8), Color(0xffe2ebf0)],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
),
|
|
child: Card(
|
|
color: Colors.white,
|
|
elevation: 6,
|
|
shape:
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
shadowColor: Colors.black12,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildHeader(selectedRange, isChartView),
|
|
const SizedBox(height: 12),
|
|
AnimatedSwitcher(
|
|
duration: const Duration(milliseconds: 300),
|
|
child: isLoading
|
|
? SkeletonLoaders.buildLoadingSkeleton()
|
|
: filteredData.isEmpty
|
|
? _buildNoDataMessage()
|
|
: isChartView
|
|
? _buildChart()
|
|
: _buildTable(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _buildHeader(String selectedRange, bool isChartView) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
MyText.bodyMedium('Attendance Overview', fontWeight: 600),
|
|
MyText.bodySmall(
|
|
'Role-wise present count',
|
|
color: Colors.grey,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
PopupMenuButton<String>(
|
|
padding: EdgeInsets.zero,
|
|
tooltip: 'Select Range',
|
|
onSelected: (value) => controller.selectedRange.value = value,
|
|
itemBuilder: (context) => const [
|
|
PopupMenuItem(value: '7D', child: Text('Last 7 Days')),
|
|
PopupMenuItem(value: '15D', child: Text('Last 15 Days')),
|
|
PopupMenuItem(value: '30D', child: Text('Last 30 Days')),
|
|
],
|
|
child: Row(
|
|
children: [
|
|
const Icon(Icons.calendar_today_outlined, size: 18),
|
|
const SizedBox(width: 4),
|
|
MyText.labelSmall(selectedRange),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.bar_chart_rounded,
|
|
size: 20,
|
|
color: isChartView ? Colors.blueAccent : Colors.grey,
|
|
),
|
|
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
|
constraints: const BoxConstraints(),
|
|
padding: EdgeInsets.zero,
|
|
onPressed: () => controller.isChartView.value = true,
|
|
tooltip: 'Chart View',
|
|
),
|
|
IconButton(
|
|
icon: Icon(
|
|
Icons.table_chart,
|
|
size: 20,
|
|
color: !isChartView ? Colors.blueAccent : Colors.grey,
|
|
),
|
|
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
|
|
constraints: const BoxConstraints(),
|
|
padding: EdgeInsets.zero,
|
|
onPressed: () => controller.isChartView.value = false,
|
|
tooltip: 'Table View',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildChart() {
|
|
final formattedDateMap = {
|
|
for (var e in filteredData)
|
|
'${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}':
|
|
e['present']
|
|
};
|
|
|
|
return SizedBox(
|
|
height: 360,
|
|
child: SfCartesianChart(
|
|
tooltipBehavior: TooltipBehavior(
|
|
enable: true,
|
|
shared: true,
|
|
activationMode: ActivationMode.singleTap,
|
|
tooltipPosition: TooltipPosition.pointer,
|
|
),
|
|
legend: const Legend(
|
|
isVisible: true,
|
|
position: LegendPosition.bottom,
|
|
overflowMode: LegendItemOverflowMode.wrap,
|
|
),
|
|
primaryXAxis: CategoryAxis(
|
|
labelRotation: 45,
|
|
majorGridLines: const MajorGridLines(width: 0),
|
|
),
|
|
primaryYAxis: NumericAxis(
|
|
minimum: 0,
|
|
interval: 1,
|
|
majorGridLines: const MajorGridLines(width: 0),
|
|
),
|
|
series: filteredRoles.map((role) {
|
|
final data = filteredDates.map((formattedDate) {
|
|
final key = '${role}_$formattedDate';
|
|
return {
|
|
'date': formattedDate,
|
|
'present': formattedDateMap[key] ?? 0
|
|
};
|
|
}).toList();
|
|
|
|
return StackedColumnSeries<Map<String, dynamic>, String>(
|
|
dataSource: data,
|
|
xValueMapper: (d, _) => d['date'],
|
|
yValueMapper: (d, _) => d['present'],
|
|
name: role,
|
|
legendIconType: LegendIconType.circle,
|
|
dataLabelSettings: const DataLabelSettings(isVisible: true),
|
|
dataLabelMapper: (d, _) =>
|
|
d['present'] == 0 ? '' : d['present'].toString(),
|
|
color: _getRoleColor(role),
|
|
);
|
|
}).toList(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildNoDataMessage() {
|
|
return SizedBox(
|
|
height: 200,
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(Icons.info_outline, color: Colors.grey.shade500, size: 48),
|
|
const SizedBox(height: 12),
|
|
MyText.bodyMedium(
|
|
'No attendance data available for the selected range or project.',
|
|
textAlign: TextAlign.center,
|
|
color: Colors.grey.shade600,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTable() {
|
|
final formattedDateMap = {
|
|
for (var e in filteredData)
|
|
'${e['role']}_${DateFormat('d MMMM').format(DateTime.parse(e['date']))}':
|
|
e['present']
|
|
};
|
|
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
border: Border.all(color: Colors.grey.shade300),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: DataTable(
|
|
columnSpacing: 28,
|
|
headingRowHeight: 42,
|
|
headingRowColor:
|
|
WidgetStateProperty.all(Colors.blueAccent.withOpacity(0.1)),
|
|
headingTextStyle: const TextStyle(
|
|
fontWeight: FontWeight.bold, color: Colors.black87),
|
|
columns: [
|
|
DataColumn(label: MyText.labelSmall('Role', fontWeight: 600)),
|
|
...filteredDates.map((date) => DataColumn(
|
|
label: MyText.labelSmall(date, fontWeight: 600),
|
|
)),
|
|
],
|
|
rows: filteredRoles.map((role) {
|
|
return DataRow(
|
|
cells: [
|
|
DataCell(Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
child: _rolePill(role),
|
|
)),
|
|
...filteredDates.map((date) {
|
|
final key = '${role}_$date';
|
|
return DataCell(Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
child: MyText.labelSmall('${formattedDateMap[key] ?? 0}'),
|
|
));
|
|
}),
|
|
],
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _rolePill(String role) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: _getRoleColor(role).withOpacity(0.15),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: MyText.labelSmall(role, fontWeight: 500),
|
|
);
|
|
}
|
|
}
|