262 lines
8.3 KiB
Dart
262 lines
8.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import '../../core/theme/app_theme.dart';
|
|
import '../../models/attendance.dart';
|
|
import '../../providers/attendance_provider.dart';
|
|
|
|
class AttendanceScreen extends ConsumerWidget {
|
|
const AttendanceScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
const filter = AttendanceFilter();
|
|
final attendanceAsync = ref.watch(attendanceProvider(filter));
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text('Attendance'),
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: () async {
|
|
ref.invalidate(attendanceProvider(filter));
|
|
await ref.read(attendanceProvider(filter).future);
|
|
},
|
|
child: attendanceAsync.when(
|
|
loading: () => const Center(child: CircularProgressIndicator()),
|
|
error: (err, _) => Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(24),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.error_outline_rounded, size: 48, color: AppTheme.errorColor),
|
|
const SizedBox(height: 12),
|
|
const Text('Failed to load attendance',
|
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
|
|
const SizedBox(height: 8),
|
|
Text(err.toString(),
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(color: AppTheme.textSecondary, fontSize: 13)),
|
|
const SizedBox(height: 16),
|
|
ElevatedButton(
|
|
onPressed: () => ref.refresh(attendanceProvider(filter)),
|
|
child: const Text('Retry'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
data: (data) => _buildAttendanceBody(context, data),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAttendanceBody(BuildContext context, AttendanceData data) {
|
|
final summary = data.summary;
|
|
final records = data.records;
|
|
|
|
return ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: [
|
|
// Attendance Overview Card
|
|
Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
children: [
|
|
const Text(
|
|
'Cumulative Attendance',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: AppTheme.textSecondary,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'${summary.percentage.toStringAsFixed(1)}%',
|
|
style: TextStyle(
|
|
fontSize: 44,
|
|
fontWeight: FontWeight.bold,
|
|
color: summary.percentage >= 75
|
|
? AppTheme.successColor
|
|
: (summary.percentage >= 60 ? AppTheme.warningColor : AppTheme.errorColor),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
summary.percentage >= 75
|
|
? 'Eligibility: Met (Eligible for exams)'
|
|
: 'Eligibility: Shortage (< 75% required)',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w500,
|
|
color: summary.percentage >= 75 ? AppTheme.successColor : AppTheme.errorColor,
|
|
),
|
|
),
|
|
const Divider(height: 32),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: [
|
|
_buildStatCol('Present', summary.present, AppTheme.successColor),
|
|
_buildStatCol('Absent', summary.absent, AppTheme.errorColor),
|
|
_buildStatCol('Late', summary.late, AppTheme.warningColor),
|
|
_buildStatCol('Leave', summary.medicalLeave, Colors.purple),
|
|
_buildStatCol('Duty', summary.onDuty, Colors.blue),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// Session History Header
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const Text(
|
|
'Attendance History',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
Text(
|
|
'${records.length} sessions recorded',
|
|
style: const TextStyle(
|
|
fontSize: 12,
|
|
color: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
if (records.isEmpty)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 40),
|
|
alignment: Alignment.center,
|
|
child: const Column(
|
|
children: [
|
|
Icon(Icons.event_busy, size: 40, color: AppTheme.textMuted),
|
|
SizedBox(height: 8),
|
|
Text(
|
|
'No attendance sessions recorded yet',
|
|
style: TextStyle(color: AppTheme.textSecondary),
|
|
),
|
|
],
|
|
),
|
|
)
|
|
else
|
|
...records.map((r) => _buildRecordCard(r)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStatCol(String label, int count, Color color) {
|
|
return Column(
|
|
children: [
|
|
Text(
|
|
'$count',
|
|
style: TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
color: color,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
label,
|
|
style: const TextStyle(
|
|
fontSize: 11,
|
|
color: AppTheme.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRecordCard(AttendanceRecord record) {
|
|
Color badgeColor;
|
|
Color textColor;
|
|
String label;
|
|
|
|
switch (record.status.toLowerCase()) {
|
|
case 'present':
|
|
badgeColor = AppTheme.successColor.withOpacity(0.12);
|
|
textColor = AppTheme.successColor;
|
|
label = 'PRESENT';
|
|
break;
|
|
case 'absent':
|
|
badgeColor = AppTheme.errorColor.withOpacity(0.12);
|
|
textColor = AppTheme.errorColor;
|
|
label = 'ABSENT';
|
|
break;
|
|
case 'late':
|
|
badgeColor = AppTheme.warningColor.withOpacity(0.12);
|
|
textColor = AppTheme.warningColor;
|
|
label = 'LATE';
|
|
break;
|
|
case 'medical_leave':
|
|
badgeColor = Colors.purple.withOpacity(0.12);
|
|
textColor = Colors.purple;
|
|
label = 'MEDICAL LEAVE';
|
|
break;
|
|
case 'on_duty':
|
|
badgeColor = Colors.blue.withOpacity(0.12);
|
|
textColor = Colors.blue;
|
|
label = 'ON DUTY';
|
|
break;
|
|
default:
|
|
badgeColor = Colors.grey.withOpacity(0.12);
|
|
textColor = Colors.grey;
|
|
label = record.status.toUpperCase();
|
|
}
|
|
|
|
return Card(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Icon(Icons.calendar_today_rounded, size: 16, color: AppTheme.textSecondary),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
record.date,
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: AppTheme.textPrimary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: badgeColor,
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
label,
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.bold,
|
|
color: textColor,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|