Attendance validation involving Location Detection + Facial Recoginition with Liveness Detection

This commit is contained in:
2026-03-07 23:46:43 +08:00
parent 52ef36faac
commit 3dbebd4006
25 changed files with 4840 additions and 361 deletions
+12
View File
@@ -11,6 +11,9 @@ class AttendanceLog {
this.checkOutAt,
this.checkOutLat,
this.checkOutLng,
this.justification,
this.verificationStatus = 'pending',
this.verificationPhotoUrl,
});
final String id;
@@ -22,8 +25,14 @@ class AttendanceLog {
final DateTime? checkOutAt;
final double? checkOutLat;
final double? checkOutLng;
final String? justification;
final String verificationStatus; // pending, verified, unverified, skipped
final String? verificationPhotoUrl;
bool get isCheckedOut => checkOutAt != null;
bool get isVerified => verificationStatus == 'verified';
bool get isUnverified =>
verificationStatus == 'unverified' || verificationStatus == 'skipped';
factory AttendanceLog.fromMap(Map<String, dynamic> map) {
return AttendanceLog(
@@ -38,6 +47,9 @@ class AttendanceLog {
: AppTime.parse(map['check_out_at'] as String),
checkOutLat: (map['check_out_lat'] as num?)?.toDouble(),
checkOutLng: (map['check_out_lng'] as num?)?.toDouble(),
justification: map['justification'] as String?,
verificationStatus: map['verification_status'] as String? ?? 'pending',
verificationPhotoUrl: map['verification_photo_url'] as String?,
);
}
}
+54
View File
@@ -0,0 +1,54 @@
import '../utils/app_time.dart';
class LeaveOfAbsence {
LeaveOfAbsence({
required this.id,
required this.userId,
required this.leaveType,
required this.justification,
required this.startTime,
required this.endTime,
required this.status,
required this.filedBy,
required this.createdAt,
});
final String id;
final String userId;
final String leaveType;
final String justification;
final DateTime startTime;
final DateTime endTime;
final String status;
final String filedBy;
final DateTime createdAt;
factory LeaveOfAbsence.fromMap(Map<String, dynamic> map) {
return LeaveOfAbsence(
id: map['id'] as String,
userId: map['user_id'] as String,
leaveType: map['leave_type'] as String,
justification: map['justification'] as String,
startTime: AppTime.parse(map['start_time'] as String),
endTime: AppTime.parse(map['end_time'] as String),
status: map['status'] as String? ?? 'pending',
filedBy: map['filed_by'] as String,
createdAt: AppTime.parse(map['created_at'] as String),
);
}
String get leaveTypeLabel {
switch (leaveType) {
case 'emergency_leave':
return 'Emergency Leave';
case 'parental_leave':
return 'Parental Leave';
case 'sick_leave':
return 'Sick Leave';
case 'vacation_leave':
return 'Vacation Leave';
default:
return leaveType;
}
}
}
+13
View File
@@ -5,6 +5,9 @@ class Profile {
required this.fullName,
this.religion = 'catholic',
this.allowTracking = false,
this.avatarUrl,
this.facePhotoUrl,
this.faceEnrolledAt,
});
final String id;
@@ -12,6 +15,11 @@ class Profile {
final String fullName;
final String religion;
final bool allowTracking;
final String? avatarUrl;
final String? facePhotoUrl;
final DateTime? faceEnrolledAt;
bool get hasFaceEnrolled => facePhotoUrl != null && faceEnrolledAt != null;
factory Profile.fromMap(Map<String, dynamic> map) {
return Profile(
@@ -20,6 +28,11 @@ class Profile {
fullName: map['full_name'] as String? ?? '',
religion: map['religion'] as String? ?? 'catholic',
allowTracking: map['allow_tracking'] as bool? ?? false,
avatarUrl: map['avatar_url'] as String?,
facePhotoUrl: map['face_photo_url'] as String?,
faceEnrolledAt: map['face_enrolled_at'] == null
? null
: DateTime.tryParse(map['face_enrolled_at'] as String),
);
}
}
+54
View File
@@ -0,0 +1,54 @@
import '../utils/app_time.dart';
/// A cross-device face verification session.
///
/// Created on web when no camera is detected. The web client generates a QR
/// code containing the session ID. The mobile client scans the QR, performs
/// liveness detection, uploads the photo, and marks the session completed.
class VerificationSession {
VerificationSession({
required this.id,
required this.userId,
required this.type,
this.contextId,
this.status = 'pending',
this.imageUrl,
required this.createdAt,
required this.expiresAt,
});
final String id;
final String userId;
final String type; // 'enrollment' or 'verification'
final String? contextId; // e.g. attendance_log_id
final String status; // 'pending', 'completed', 'expired'
final String? imageUrl;
final DateTime createdAt;
final DateTime expiresAt;
bool get isPending => status == 'pending';
bool get isCompleted => status == 'completed';
bool get isExpired =>
status == 'expired' || DateTime.now().toUtc().isAfter(expiresAt);
factory VerificationSession.fromMap(Map<String, dynamic> map) {
return VerificationSession(
id: map['id'] as String,
userId: map['user_id'] as String,
type: map['type'] as String,
contextId: map['context_id'] as String?,
status: (map['status'] as String?) ?? 'pending',
imageUrl: map['image_url'] as String?,
createdAt: AppTime.parse(map['created_at'] as String),
expiresAt: AppTime.parse(map['expires_at'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'user_id': userId,
'type': type,
if (contextId != null) 'context_id': contextId,
};
}
}