Attendance validation involving Location Detection + Facial Recoginition with Liveness Detection
This commit is contained in:
@@ -33,6 +33,7 @@ class _WhereaboutsScreenState extends ConsumerState<WhereaboutsScreen> {
|
||||
};
|
||||
|
||||
return ResponsiveBody(
|
||||
maxWidth: 1200,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -76,9 +77,13 @@ class _WhereaboutsScreenState extends ConsumerState<WhereaboutsScreen> {
|
||||
Map<String, Profile> profileById,
|
||||
GeofenceConfig? geofenceConfig,
|
||||
) {
|
||||
final markers = positions.map((pos) {
|
||||
final name = profileById[pos.userId]?.fullName ?? 'Unknown';
|
||||
final inPremise = pos.inPremise;
|
||||
// Only pin users who are in-premise (privacy: don't reveal off-site locations).
|
||||
final inPremisePositions = positions.where((pos) => pos.inPremise).toList();
|
||||
|
||||
final markers = inPremisePositions.map((pos) {
|
||||
final profile = profileById[pos.userId];
|
||||
final name = profile?.fullName ?? 'Unknown';
|
||||
final pinColor = _roleColor(profile?.role);
|
||||
return Marker(
|
||||
point: LatLng(pos.lat, pos.lng),
|
||||
width: 80,
|
||||
@@ -106,11 +111,7 @@ class _WhereaboutsScreenState extends ConsumerState<WhereaboutsScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.location_pin,
|
||||
size: 28,
|
||||
color: inPremise ? Colors.green : Colors.orange,
|
||||
),
|
||||
Icon(Icons.location_pin, size: 28, color: pinColor),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -167,46 +168,126 @@ class _WhereaboutsScreenState extends ConsumerState<WhereaboutsScreen> {
|
||||
) {
|
||||
if (positions.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 180),
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
itemCount: positions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final pos = positions[index];
|
||||
final p = profileById[pos.userId];
|
||||
final name = p?.fullName ?? 'Unknown';
|
||||
final role = p?.role ?? '-';
|
||||
final timeAgo = _timeAgo(pos.updatedAt);
|
||||
// Only include Admin, IT Staff, and Dispatcher in the legend.
|
||||
final relevantRoles = {'admin', 'dispatcher', 'it_staff'};
|
||||
final legendEntries = positions.where((pos) {
|
||||
final role = profileById[pos.userId]?.role;
|
||||
return role != null && relevantRoles.contains(role);
|
||||
}).toList();
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: pos.inPremise
|
||||
? Colors.green.shade100
|
||||
: Colors.orange.shade100,
|
||||
child: Icon(
|
||||
pos.inPremise ? Icons.check : Icons.location_off,
|
||||
size: 16,
|
||||
color: pos.inPremise ? Colors.green : Colors.orange,
|
||||
),
|
||||
if (legendEntries.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 220),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Role color legend header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
_legendDot(Colors.blue.shade700),
|
||||
const SizedBox(width: 4),
|
||||
Text('Admin', style: Theme.of(context).textTheme.labelSmall),
|
||||
const SizedBox(width: 12),
|
||||
_legendDot(Colors.green.shade700),
|
||||
const SizedBox(width: 4),
|
||||
Text('IT Staff', style: Theme.of(context).textTheme.labelSmall),
|
||||
const SizedBox(width: 12),
|
||||
_legendDot(Colors.orange.shade700),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Dispatcher',
|
||||
style: Theme.of(context).textTheme.labelSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
title: Text(name),
|
||||
subtitle: Text('${_roleLabel(role)} · $timeAgo'),
|
||||
trailing: Text(
|
||||
pos.inPremise ? 'In premise' : 'Off-site',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: pos.inPremise ? Colors.green : Colors.orange,
|
||||
),
|
||||
),
|
||||
// Staff entries
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
itemCount: legendEntries.length,
|
||||
itemBuilder: (context, index) {
|
||||
final pos = legendEntries[index];
|
||||
final p = profileById[pos.userId];
|
||||
final name = p?.fullName ?? 'Unknown';
|
||||
final role = p?.role ?? '-';
|
||||
final isInPremise = pos.inPremise;
|
||||
final pinColor = _roleColor(role);
|
||||
final timeAgo = _timeAgo(pos.updatedAt);
|
||||
// Grey out outside-premise users for privacy.
|
||||
final effectiveColor = isInPremise
|
||||
? pinColor
|
||||
: Colors.grey.shade400;
|
||||
|
||||
return ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: effectiveColor.withValues(alpha: 0.2),
|
||||
child: Icon(
|
||||
isInPremise ? Icons.location_pin : Icons.location_off,
|
||||
size: 16,
|
||||
color: effectiveColor,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
color: isInPremise ? null : Colors.grey,
|
||||
fontWeight: isInPremise
|
||||
? FontWeight.w600
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'${_roleLabel(role)} · ${isInPremise ? timeAgo : 'Outside premise'}',
|
||||
style: TextStyle(color: isInPremise ? null : Colors.grey),
|
||||
),
|
||||
trailing: isInPremise
|
||||
? Icon(Icons.circle, size: 10, color: pinColor)
|
||||
: Icon(
|
||||
Icons.circle,
|
||||
size: 10,
|
||||
color: Colors.grey.shade300,
|
||||
),
|
||||
onTap: isInPremise
|
||||
? () =>
|
||||
_mapController.move(LatLng(pos.lat, pos.lng), 17.0)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
),
|
||||
onTap: () => _mapController.move(LatLng(pos.lat, pos.lng), 17.0),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _legendDot(Color color) {
|
||||
return Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the pin color for a given role.
|
||||
static Color _roleColor(String? role) {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return Colors.blue.shade700;
|
||||
case 'it_staff':
|
||||
return Colors.green.shade700;
|
||||
case 'dispatcher':
|
||||
return Colors.orange.shade700;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _timeAgo(DateTime dt) {
|
||||
final diff = AppTime.now().difference(dt);
|
||||
if (diff.inMinutes < 1) return 'Just now';
|
||||
|
||||
Reference in New Issue
Block a user