390 lines
13 KiB
JavaScript
390 lines
13 KiB
JavaScript
import React, { useState, useCallback, useEffect } from 'react';
|
|
import { View, Text, StyleSheet, PermissionsAndroid, Platform, Alert, ActivityIndicator, TouchableOpacity, FlatList, RefreshControl, Linking } from 'react-native';
|
|
import Geolocation from 'react-native-geolocation-service';
|
|
import { useFocusEffect } from '@react-navigation/native';
|
|
import api from '../services/api';
|
|
import Colors from '../constants/Colors';
|
|
|
|
const AttendanceScreen = () => {
|
|
const [loading, setLoading] = useState(false);
|
|
const [historyLoading, setHistoryLoading] = useState(true);
|
|
const [currentSession, setCurrentSession] = useState(null);
|
|
const [history, setHistory] = useState([]);
|
|
|
|
const getCurrentLocation = () => {
|
|
return new Promise((resolve, reject) => {
|
|
Geolocation.getCurrentPosition(
|
|
(position) => resolve(position.coords),
|
|
(error) => {
|
|
Alert.alert("Location Error", error.message);
|
|
reject(error);
|
|
},
|
|
{
|
|
enableHighAccuracy: false, // Switch to false for maximum stability
|
|
timeout: 15000,
|
|
maximumAge: 10000,
|
|
// FORCE using the standard Android Location Manager
|
|
// instead of the Google Fused Provider (which is crashing)
|
|
forceLocationManager: true
|
|
}
|
|
);
|
|
});
|
|
};
|
|
|
|
const fetchAttendanceData = async () => {
|
|
setHistoryLoading(true);
|
|
try {
|
|
const response = await api.get('/attendance/my-history');
|
|
const data = response.data;
|
|
setHistory(data);
|
|
|
|
if (data.length > 0 && !data[0].checkOutTime) {
|
|
setCurrentSession(data[0]);
|
|
} else {
|
|
setCurrentSession(null);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch history", error);
|
|
if (error.message === 'Network Error') {
|
|
Alert.alert("Network Error", "Could not connect to server. Please check your internet and if the API is running at " + api.defaults.baseURL);
|
|
}
|
|
} finally {
|
|
setHistoryLoading(false);
|
|
}
|
|
};
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
fetchAttendanceData();
|
|
}, [])
|
|
);
|
|
|
|
// Periodic Location Tracking when Checked In
|
|
/*
|
|
useEffect(() => {
|
|
let interval;
|
|
if (currentSession && !currentSession.checkOutTime) {
|
|
console.log("Starting periodic tracking...");
|
|
const sendLocation = async () => {
|
|
try {
|
|
const coords = await getCurrentLocation();
|
|
await api.post('/locations', {
|
|
lat: coords.latitude,
|
|
lng: coords.longitude
|
|
});
|
|
} catch (error) {
|
|
console.error("Periodic tracking failed", error);
|
|
}
|
|
};
|
|
sendLocation(); // Initial
|
|
interval = setInterval(sendLocation, 5 * 60 * 1000); // 5 mins
|
|
}
|
|
return () => {
|
|
if (interval) {
|
|
console.log("Stopping periodic tracking.");
|
|
clearInterval(interval);
|
|
}
|
|
};
|
|
}, [currentSession]);
|
|
*/
|
|
|
|
// Request Location Permission
|
|
const requestLocationPermission = async () => {
|
|
if (Platform.OS === 'android') {
|
|
try {
|
|
const granted = await PermissionsAndroid.requestMultiple([
|
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
|
PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION,
|
|
]);
|
|
|
|
const fine = granted?.[PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION] === PermissionsAndroid.RESULTS.GRANTED;
|
|
const coarse = granted?.[PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION] === PermissionsAndroid.RESULTS.GRANTED;
|
|
|
|
if (!fine || !coarse) {
|
|
Alert.alert("Permission Denied", "Attendance requires location access. Please enable it in settings.");
|
|
}
|
|
|
|
return fine && coarse;
|
|
} catch (err) {
|
|
console.warn(err);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
const getCurrentLocationReal = getCurrentLocation;
|
|
|
|
const handleCheckIn = async () => {
|
|
const hasPermission = await requestLocationPermission();
|
|
if (!hasPermission) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const coords = await getCurrentLocation();
|
|
await api.post('/attendance/check-in', {
|
|
latitude: coords.latitude,
|
|
longitude: coords.longitude
|
|
});
|
|
await fetchAttendanceData();
|
|
Alert.alert("Success", "Checked In!");
|
|
} catch (error) {
|
|
console.error(error);
|
|
Alert.alert("Error", "Check-in failed.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleCheckOut = async () => {
|
|
if (!currentSession?.id) return;
|
|
|
|
const hasPermission = await requestLocationPermission();
|
|
if (!hasPermission) return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const coords = await getCurrentLocation();
|
|
await api.patch(`/attendance/check-out/${currentSession.id}`, {
|
|
latitude: coords.latitude,
|
|
longitude: coords.longitude
|
|
});
|
|
await fetchAttendanceData();
|
|
Alert.alert("Success", "Checked Out Successfully!");
|
|
} catch (error) {
|
|
console.error(error);
|
|
Alert.alert("Error", "Check-out failed.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
if (!dateString) return '--:--';
|
|
const date = new Date(dateString);
|
|
const hours = date.getHours();
|
|
const mins = date.getMinutes();
|
|
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
const h12 = hours % 12 || 12;
|
|
return `${h12}:${mins < 10 ? '0' + mins : mins} ${ampm}`;
|
|
};
|
|
|
|
const getDayMonth = (dateString) => {
|
|
const date = new Date(dateString);
|
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
return `${date.getDate()} ${months[date.getMonth()]}`;
|
|
};
|
|
|
|
const openMap = (lat, lng) => {
|
|
const url = Platform.select({
|
|
ios: `maps:0,0?q=${lat},${lng}`,
|
|
android: `geo:0,0?q=${lat},${lng}(Attendance Location)`
|
|
});
|
|
Linking.openURL(url);
|
|
};
|
|
|
|
const renderHistoryItem = ({ item }) => (
|
|
<View style={styles.historyCard}>
|
|
<View style={styles.dateBox}>
|
|
<Text style={styles.dateText}>{getDayMonth(item.checkInTime)}</Text>
|
|
</View>
|
|
<View style={styles.timeBox}>
|
|
<View style={styles.timeRow}>
|
|
<Text style={styles.timeLabel}>In:</Text>
|
|
<Text style={styles.timeValue}>{formatDate(item.checkInTime)}</Text>
|
|
</View>
|
|
<View style={styles.timeRow}>
|
|
<Text style={styles.timeLabel}>Out:</Text>
|
|
<Text style={styles.timeValue}>{item.checkOutTime ? formatDate(item.checkOutTime) : 'Active'}</Text>
|
|
</View>
|
|
{(item.checkInLat && item.checkInLng) && (
|
|
<TouchableOpacity
|
|
onPress={() => openMap(item.checkInLat, item.checkInLng)}
|
|
style={styles.locationContainer}
|
|
>
|
|
<Text style={styles.locationText}>
|
|
📍 {Number(item.checkInLat || 0).toFixed(4)}, {Number(item.checkInLng || 0).toFixed(4)}
|
|
</Text>
|
|
<Text style={styles.mapLink}>View Map</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
<View style={[styles.statusIndicator, item.checkOutTime ? styles.statusCompleted : styles.statusActive]} />
|
|
</View>
|
|
);
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<Text style={styles.headerTitle}>My Attendance</Text>
|
|
|
|
<View style={styles.actionCard}>
|
|
<Text style={styles.cardTitle}>Today's Status</Text>
|
|
<Text style={[styles.statusText, currentSession ? styles.textActive : styles.textInactive]}>
|
|
{currentSession ? 'Currently Checked In' : 'Not Checked In'}
|
|
</Text>
|
|
|
|
{loading ? (
|
|
<ActivityIndicator size="large" color={Colors.primary} style={{ marginVertical: 10 }} />
|
|
) : (
|
|
<TouchableOpacity
|
|
style={[styles.actionButton, currentSession ? styles.btnCheckout : styles.btnCheckin]}
|
|
onPress={currentSession ? handleCheckOut : handleCheckIn}
|
|
>
|
|
<Text style={styles.btnText}>
|
|
{currentSession ? "Check Out Now" : "Check In Now"}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
|
|
<Text style={styles.sectionHeader}>Recent History</Text>
|
|
{historyLoading ? (
|
|
<ActivityIndicator size="large" color={Colors.primary} />
|
|
) : (
|
|
<FlatList
|
|
data={history}
|
|
keyExtractor={(item) => item.id}
|
|
renderItem={renderHistoryItem}
|
|
contentContainerStyle={styles.listContent}
|
|
ListEmptyComponent={<Text style={styles.emptyText}>No attendance records found.</Text>}
|
|
refreshControl={<RefreshControl refreshing={historyLoading} onRefresh={fetchAttendanceData} />}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: Colors.background,
|
|
padding: 20
|
|
},
|
|
headerTitle: {
|
|
fontSize: 28,
|
|
fontWeight: 'bold',
|
|
color: Colors.text,
|
|
marginBottom: 20
|
|
},
|
|
actionCard: {
|
|
backgroundColor: 'white',
|
|
borderRadius: 16,
|
|
padding: 20,
|
|
alignItems: 'center',
|
|
elevation: 3,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 5,
|
|
marginBottom: 30
|
|
},
|
|
cardTitle: {
|
|
fontSize: 16,
|
|
color: Colors.textMuted,
|
|
marginBottom: 5
|
|
},
|
|
statusText: {
|
|
fontSize: 20,
|
|
fontWeight: 'bold',
|
|
marginBottom: 20
|
|
},
|
|
textActive: { color: Colors.success },
|
|
textInactive: { color: Colors.textMuted },
|
|
actionButton: {
|
|
width: '100%',
|
|
paddingVertical: 15,
|
|
borderRadius: 12,
|
|
alignItems: 'center',
|
|
},
|
|
btnCheckin: { backgroundColor: Colors.primary },
|
|
btnCheckout: { backgroundColor: Colors.danger },
|
|
btnText: {
|
|
color: 'white',
|
|
fontSize: 18,
|
|
fontWeight: 'bold'
|
|
},
|
|
sectionHeader: {
|
|
fontSize: 18,
|
|
fontWeight: 'bold',
|
|
color: Colors.text,
|
|
marginBottom: 15
|
|
},
|
|
listContent: {
|
|
paddingBottom: 20
|
|
},
|
|
historyCard: {
|
|
flexDirection: 'row',
|
|
backgroundColor: 'white',
|
|
borderRadius: 12,
|
|
padding: 15,
|
|
marginBottom: 10,
|
|
alignItems: 'center',
|
|
elevation: 1
|
|
},
|
|
dateBox: {
|
|
width: 60,
|
|
justifyContent: 'center',
|
|
borderRightWidth: 1,
|
|
borderRightColor: Colors.borderLight,
|
|
marginRight: 15
|
|
},
|
|
dateText: {
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
color: Colors.text,
|
|
textAlign: 'center'
|
|
},
|
|
timeBox: {
|
|
flex: 1
|
|
},
|
|
timeRow: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 4
|
|
},
|
|
timeLabel: {
|
|
fontSize: 12,
|
|
color: Colors.textLight,
|
|
width: 30
|
|
},
|
|
timeValue: {
|
|
fontSize: 14,
|
|
color: Colors.text,
|
|
fontWeight: '500'
|
|
},
|
|
statusIndicator: {
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: 5,
|
|
marginLeft: 10
|
|
},
|
|
statusActive: { backgroundColor: Colors.success },
|
|
statusCompleted: { backgroundColor: Colors.textLight },
|
|
locationContainer: {
|
|
marginTop: 10,
|
|
padding: 8,
|
|
backgroundColor: '#f8f9fa',
|
|
borderRadius: 8,
|
|
borderWidth: 1,
|
|
borderColor: '#e9ecef'
|
|
},
|
|
locationText: {
|
|
fontSize: 12,
|
|
color: Colors.text,
|
|
fontFamily: 'monospace'
|
|
},
|
|
mapLink: {
|
|
fontSize: 10,
|
|
color: Colors.primary,
|
|
fontWeight: 'bold',
|
|
marginTop: 5
|
|
},
|
|
emptyText: {
|
|
textAlign: 'center',
|
|
marginTop: 30,
|
|
color: Colors.textLight
|
|
}
|
|
});
|
|
|
|
export default AttendanceScreen;
|