manual check in/check out

implemented, to be implemented: qrcode or/and geofence
This commit is contained in:
dimitar committed 2025-11-19 02:23:39 +01:00
1 parent 33a698066f
commit 39021dca35
18 files changed
+650 -72

No files matched your search

+223 -7
View File
@@ -1,28 +1,244 @@
import { View, Text, StyleSheet } from 'react-native'
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, ScrollView, Alert } from 'react-native'
import { useState, useEffect } from 'react'
import { useAuth } from '@clerk/clerk-expo'
import axios from 'axios'
import { API_BASE_URL, API_ENDPOINTS } from '../../config/api'
interface Attendance {
id: string
checkInTime: string
checkOutTime?: string
type: string
notes?: string
}
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 fetchAttendance = async () => {
try {
setLoading(true)
const token = await getToken()
const url = `${API_BASE_URL}${API_ENDPOINTS.ATTENDANCE.HISTORY}`
console.log('Fetching attendance from:', url)
const response = await axios.get(url, {
headers: { Authorization: `Bearer ${token}` }
})
const data: Attendance[] = response.data
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()
await axios.post(
`${API_BASE_URL}${API_ENDPOINTS.ATTENDANCE.CHECK_IN}`,
{ type: 'gym' },
{ headers: { Authorization: `Bearer ${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()
await axios.post(
`${API_BASE_URL}${API_ENDPOINTS.ATTENDANCE.CHECK_OUT}`,
{},
{ headers: { Authorization: `Bearer ${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="#000" />
</View>
)
}
return (
<View style={styles.container}>
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
<Text style={styles.title}>Attendance</Text>
<Text style={styles.subtitle}>Track your gym visits</Text>
</View>
<View style={styles.actionContainer}>
{activeCheckIn ? (
<View style={styles.activeCard}>
<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>
<TouchableOpacity style={styles.checkOutButton} onPress={handleCheckOut}>
<Text style={styles.buttonText}>Check Out</Text>
</TouchableOpacity>
</View>
) : (
<TouchableOpacity style={styles.checkInButton} onPress={handleCheckIn}>
<Text style={styles.buttonText}>Check In</Text>
</TouchableOpacity>
)}
</View>
<Text style={styles.sectionTitle}>Recent History</Text>
{history.map((item) => (
<View key={item.id} style={styles.historyItem}>
<View>
<Text style={styles.dateText}>
{new Date(item.checkInTime).toLocaleDateString()}
</Text>
<Text style={styles.typeText}>{item.type.toUpperCase()}</Text>
</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>
</View>
))}
</ScrollView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#f5f5f5',
},
content: {
padding: 20,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontSize: 28,
fontWeight: 'bold',
marginBottom: 8,
marginBottom: 4,
color: '#1a1a1a',
},
subtitle: {
fontSize: 16,
color: '#666',
marginBottom: 24,
},
actionContainer: {
marginBottom: 32,
},
checkInButton: {
backgroundColor: '#000',
paddingVertical: 16,
borderRadius: 12,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
checkOutButton: {
backgroundColor: '#ff3b30',
paddingVertical: 16,
borderRadius: 12,
alignItems: 'center',
marginTop: 16,
},
buttonText: {
color: '#fff',
fontSize: 18,
fontWeight: '600',
},
activeCard: {
backgroundColor: '#fff',
padding: 20,
borderRadius: 16,
borderWidth: 1,
borderColor: '#e0e0e0',
},
activeText: {
fontSize: 18,
fontWeight: '600',
color: '#2e7d32',
marginBottom: 4,
},
timeText: {
fontSize: 14,
color: '#666',
},
sectionTitle: {
fontSize: 20,
fontWeight: '600',
marginBottom: 16,
color: '#1a1a1a',
},
historyItem: {
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
marginBottom: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 2,
},
dateText: {
fontSize: 16,
fontWeight: '500',
color: '#1a1a1a',
},
typeText: {
fontSize: 12,
color: '#666',
marginTop: 2,
},
timeContainer: {
alignItems: 'flex-end',
},
historyTime: {
fontSize: 14,
color: '#444',
},
})