358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'
|
|
import { useState, useEffect, useRef } from 'react'
|
|
import { useAuth } from '@clerk/clerk-expo'
|
|
import { LinearGradient } from 'expo-linear-gradient'
|
|
import { Ionicons } from '@expo/vector-icons'
|
|
import { attendanceApi, Attendance } from '../../api/attendance'
|
|
import { theme } from '../../styles/theme'
|
|
import { Animated } from 'react-native'
|
|
import { GeofenceStatus } from '../../components/GeofenceStatus'
|
|
|
|
export default function AttendanceScreen() {
|
|
const { getToken, userId } = useAuth()
|
|
const [loading, setLoading] = useState(true)
|
|
const [activeCheckIn, setActiveCheckIn] = useState<Attendance | null>(null)
|
|
const [history, setHistory] = useState<Attendance[]>([])
|
|
const pulseAnim = useRef(new Animated.Value(1)).current
|
|
|
|
useEffect(() => {
|
|
if (activeCheckIn) {
|
|
const pulse = Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1.05,
|
|
duration: 1000,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1,
|
|
duration: 1000,
|
|
useNativeDriver: true,
|
|
}),
|
|
])
|
|
)
|
|
pulse.start()
|
|
return () => pulse.stop()
|
|
}
|
|
}, [activeCheckIn])
|
|
|
|
const fetchAttendance = async () => {
|
|
try {
|
|
setLoading(true)
|
|
const token = await getToken()
|
|
if (!token) return
|
|
|
|
console.log('Fetching attendance history')
|
|
const data = await attendanceApi.getHistory(token)
|
|
setHistory(data)
|
|
|
|
// Check if there's an active check-in (latest one has no checkOutTime)
|
|
if (data.length > 0 && !data[0].checkOutTime) {
|
|
setActiveCheckIn(data[0])
|
|
} else {
|
|
setActiveCheckIn(null)
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching attendance:', error)
|
|
Alert.alert('Error', 'Failed to load attendance data')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchAttendance()
|
|
}, [])
|
|
|
|
const handleCheckIn = async () => {
|
|
try {
|
|
const token = await getToken()
|
|
if (!token) return
|
|
|
|
await attendanceApi.checkIn('gym', token)
|
|
fetchAttendance()
|
|
Alert.alert('Success', 'Checked in successfully!')
|
|
} catch (error: any) {
|
|
console.error('Check-in error:', error)
|
|
Alert.alert('Error', error.response?.data || 'Failed to check in')
|
|
}
|
|
}
|
|
|
|
const handleCheckOut = async () => {
|
|
try {
|
|
const token = await getToken()
|
|
if (!token) return
|
|
|
|
await attendanceApi.checkOut(token)
|
|
fetchAttendance()
|
|
Alert.alert('Success', 'Checked out successfully!')
|
|
} catch (error: any) {
|
|
console.error('Check-out error:', error)
|
|
Alert.alert('Error', error.response?.data || 'Failed to check out')
|
|
}
|
|
}
|
|
|
|
if (loading && !history.length) {
|
|
return (
|
|
<View style={styles.centered}>
|
|
<ActivityIndicator size="large" color={theme.colors.primary} />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
|
<LinearGradient
|
|
colors={theme.gradients.primary}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={styles.header}
|
|
>
|
|
<Text style={styles.title}>Attendance</Text>
|
|
<Text style={styles.subtitle}>Track your gym visits</Text>
|
|
</LinearGradient>
|
|
|
|
{/* Geofencing Status Card */}
|
|
<GeofenceStatus />
|
|
|
|
<View style={styles.actionContainer}>
|
|
{activeCheckIn ? (
|
|
<LinearGradient
|
|
colors={['rgba(16, 185, 129, 0.15)', 'rgba(5, 150, 105, 0.1)']}
|
|
style={[styles.activeCard, theme.shadows.medium]}
|
|
>
|
|
<View style={styles.activeCardContent}>
|
|
<View style={styles.activeIconContainer}>
|
|
<LinearGradient
|
|
colors={theme.gradients.success}
|
|
style={styles.activeIcon}
|
|
>
|
|
<Ionicons name="checkmark-circle" size={32} color="#fff" />
|
|
</LinearGradient>
|
|
</View>
|
|
<View style={styles.activeTextContainer}>
|
|
<Text style={styles.activeText}>Currently Checked In</Text>
|
|
<Text style={styles.timeText}>
|
|
Since {new Date(activeCheckIn.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<TouchableOpacity onPress={handleCheckOut} activeOpacity={0.8}>
|
|
<LinearGradient
|
|
colors={theme.gradients.danger}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
style={[styles.checkOutButton, theme.shadows.medium]}
|
|
>
|
|
<Ionicons name="log-out-outline" size={20} color="#fff" style={{ marginRight: 8 }} />
|
|
<Text style={styles.buttonText}>Check Out</Text>
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
</LinearGradient>
|
|
) : (
|
|
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
|
<TouchableOpacity onPress={handleCheckIn} activeOpacity={0.8}>
|
|
<LinearGradient
|
|
colors={theme.gradients.primary}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
style={[styles.checkInButton, theme.shadows.glow]}
|
|
>
|
|
<Ionicons name="log-in-outline" size={24} color="#fff" style={{ marginRight: 8 }} />
|
|
<Text style={styles.checkInButtonText}>Check In</Text>
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
</Animated.View>
|
|
)}
|
|
</View>
|
|
|
|
<Text style={styles.sectionTitle}>Recent History</Text>
|
|
{history.map((item) => (
|
|
<LinearGradient
|
|
key={item.id}
|
|
colors={['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const}
|
|
style={[styles.historyItem, theme.shadows.medium]}
|
|
>
|
|
<View style={styles.historyLeft}>
|
|
<View style={styles.historyIconContainer}>
|
|
<LinearGradient
|
|
colors={item.checkOutTime ? theme.gradients.success : theme.gradients.primary}
|
|
style={styles.historyIcon}
|
|
>
|
|
<Ionicons
|
|
name={item.checkOutTime ? "checkmark" : "time-outline"}
|
|
size={16}
|
|
color="#fff"
|
|
/>
|
|
</LinearGradient>
|
|
</View>
|
|
<View>
|
|
<Text style={styles.dateText}>
|
|
{new Date(item.checkInTime).toLocaleDateString()}
|
|
</Text>
|
|
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text>
|
|
</View>
|
|
</View>
|
|
<View style={styles.timeContainer}>
|
|
<Text style={styles.historyTime}>
|
|
In: {new Date(item.checkInTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</Text>
|
|
{item.checkOutTime && (
|
|
<Text style={styles.historyTime}>
|
|
Out: {new Date(item.checkOutTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</LinearGradient>
|
|
))}
|
|
</ScrollView>
|
|
)
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.background,
|
|
},
|
|
content: {
|
|
paddingBottom: 20,
|
|
},
|
|
centered: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
header: {
|
|
paddingTop: 60,
|
|
paddingBottom: 24,
|
|
paddingHorizontal: 24,
|
|
marginBottom: 24,
|
|
borderBottomLeftRadius: theme.borderRadius.xl,
|
|
borderBottomRightRadius: theme.borderRadius.xl,
|
|
},
|
|
title: {
|
|
fontSize: theme.typography.fontSize['3xl'],
|
|
fontWeight: theme.typography.fontWeight.bold,
|
|
color: theme.colors.white,
|
|
marginBottom: 4,
|
|
},
|
|
subtitle: {
|
|
fontSize: theme.typography.fontSize.base,
|
|
color: 'rgba(255, 255, 255, 0.9)',
|
|
},
|
|
actionContainer: {
|
|
marginBottom: 32,
|
|
paddingHorizontal: 20,
|
|
},
|
|
checkInButton: {
|
|
paddingVertical: 20,
|
|
paddingHorizontal: 24,
|
|
borderRadius: theme.borderRadius.xl,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
checkInButtonText: {
|
|
color: theme.colors.white,
|
|
fontSize: theme.typography.fontSize.xl,
|
|
fontWeight: theme.typography.fontWeight.semibold,
|
|
},
|
|
checkOutButton: {
|
|
paddingVertical: 14,
|
|
paddingHorizontal: 20,
|
|
borderRadius: theme.borderRadius.lg,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginTop: 16,
|
|
},
|
|
buttonText: {
|
|
color: theme.colors.white,
|
|
fontSize: theme.typography.fontSize.base,
|
|
fontWeight: theme.typography.fontWeight.semibold,
|
|
},
|
|
activeCard: {
|
|
padding: 20,
|
|
borderRadius: theme.borderRadius.xl,
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(16, 185, 129, 0.2)',
|
|
},
|
|
activeCardContent: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 16,
|
|
},
|
|
activeIconContainer: {
|
|
marginRight: 16,
|
|
},
|
|
activeIcon: {
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
activeTextContainer: {
|
|
flex: 1,
|
|
},
|
|
activeText: {
|
|
fontSize: theme.typography.fontSize.lg,
|
|
fontWeight: theme.typography.fontWeight.semibold,
|
|
color: theme.colors.gray900,
|
|
marginBottom: 4,
|
|
},
|
|
timeText: {
|
|
fontSize: theme.typography.fontSize.sm,
|
|
color: theme.colors.gray600,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: theme.typography.fontSize.xl,
|
|
fontWeight: theme.typography.fontWeight.semibold,
|
|
marginBottom: 16,
|
|
paddingHorizontal: 20,
|
|
color: theme.colors.gray900,
|
|
},
|
|
historyItem: {
|
|
padding: 16,
|
|
borderRadius: theme.borderRadius.xl,
|
|
marginBottom: 12,
|
|
marginHorizontal: 20,
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(59, 130, 246, 0.1)',
|
|
},
|
|
historyLeft: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 12,
|
|
},
|
|
historyIconContainer: {
|
|
marginRight: 4,
|
|
},
|
|
historyIcon: {
|
|
width: 32,
|
|
height: 32,
|
|
borderRadius: 16,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
dateText: {
|
|
fontSize: theme.typography.fontSize.base,
|
|
fontWeight: theme.typography.fontWeight.semibold,
|
|
color: theme.colors.gray900,
|
|
},
|
|
typeText: {
|
|
fontSize: theme.typography.fontSize.xs,
|
|
color: theme.colors.gray600,
|
|
marginTop: 2,
|
|
},
|
|
timeContainer: {
|
|
alignItems: 'flex-end',
|
|
},
|
|
historyTime: {
|
|
fontSize: theme.typography.fontSize.sm,
|
|
color: theme.colors.gray700,
|
|
},
|
|
}) |